baby todo or not todo
4 minutes to read
We are provided with this webpage:
Source code analysis
We are also provided with the source code, which is a Flask application (Python). This is application/app.py
:
from flask import Flask, session, g
from flask.json import JSONEncoder
from application.blueprints.routes import main, api
from application.util import generate
from application.database import get_db
from application.models import todo
import time
class toJSON(JSONEncoder):
def default(self, obj):
if isinstance(obj, todo):
return {
'id' : obj.id,
'name' : obj.name,
'assignee': obj.assignee,
'done' : obj.done
}
return super(toJSON, self).default(obj)
class HTB(Flask):
def process_response(self, response):
response.headers['Server'] = 'made with <3 by makelarides'
super(HTB, self).process_response(response)
return response
app = HTB(__name__)
app.config.from_object('application.config.Config')
app.json_encoder = toJSON
app.register_blueprint(main, url_prefix='/')
app.register_blueprint(api, url_prefix='/api')
@app.before_first_request
def wake_bots():
with app.open_resource('schema.sql', mode='r') as f:
get_db().cursor().executescript(f.read() % (generate(15)))
time.sleep(0.2)
@app.before_request
def is_authenticated():
g.user = session.get('authentication')
if not g.user:
username = f'user{generate(8)}'
todo.create_user(username, generate(15))
g.user = session['authentication'] = username
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None: db.close()
@app.errorhandler(404)
def not_found(error):
return {'error': 'Not Found'}, 404
@app.errorhandler(403)
def forbidden(error):
return {'error': 'Not Allowed'}, 403
@app.errorhandler(400)
def bad_request(error):
return {'error': 'Bad Request'}, 400
Something interesting are these endpoints:
app.register_blueprint(main, url_prefix='/')
app.register_blueprint(api, url_prefix='/api')
Which are defined in application/blueprints/routes.py
:
from flask import Blueprint, request, jsonify, session, render_template, g
from application.util import verify_integrity
from application.models import todo
main = Blueprint('main', __name__)
api = Blueprint('api', __name__)
@main.route('/')
def index():
context = {
'list_access': g.user,
'secret': todo.get_secret_from(g.user)
}
return render_template('index.html', **context)
@api.before_request
@verify_integrity
def and_then(): pass
# TODO: There are not view arguments involved, I hope this doesn't break
# the authentication control on the verify_integrity() decorator
@api.route('/list/all/')
def list_all():
return jsonify(todo.get_all())
@api.route('/list/<assignee>/')
def list_tasks(assignee):
return jsonify(todo.get_by_user(assignee))
@api.route('/add/', methods=['POST'])
def add():
todo.add(g.name, g.user)
return {'success': f'Successfuly added {g.name} by user {g.user}'}
@api.route('/rename/<int:todo_id>/<new_name>/')
def rename_task(todo_id, new_name):
g.selected.rename(new_name)
return {'success': f'Successfuly edited {todo_id} to {new_name}'}
@api.route('/delete/<int:todo_id>/', methods=['DELETE'])
def delete(todo_id):
g.selected.delete()
return {'success': f'Successfuly deleted {todo_id}'}
@api.route('/complete/<int:todo_id>/')
def complete(todo_id):
g.selected.complete()
return {'success': f'Successfuly completed {todo_id}'}
@api.route('/assign/<int:todo_id>/<new_assignee>/')
def assign(todo_id, new_assignee):
g.selected.reassign(new_assignee)
return {'success': f'Successfuly reassigned {todo_id} to {new_assignee}'}
Here it uses a lot of functions of todo
, which is defined at application/models.py
:
from application.database import query_db
class todo(object):
def __init__(self, id, name, assignee, done=int(0)):
self.name = name
self.id = id
self.assignee = assignee
self.done = done
def __str__(self):
return f'({self.id}, {self.name}, {self.assignee}, {self.done})'
def asdict(self):
return {
'name': self.name,
'id': self.id,
'assignee': self.assignee,
'done': self.done
}
def __iter__(self):
return iter(self.__dict__.items())
@staticmethod
def get_bot_password():
return query_db('SELECT password FROM bot', one=True)['password']
@staticmethod
def check_bot(password):
return query_db('SELECT * FROM bot WHERE password=?', (password,), one=True)
@staticmethod
def create_user(name, secret):
return query_db('INSERT INTO users (name, secret) VALUES (?, ?)', (name, secret))
@staticmethod
def get_secret_from(name):
from flask import session
try:
return query_db('SELECT secret FROM users WHERE name=?', (name,), one=True)['secret']
except:
session.clear()
pass
@staticmethod
def verify_secret(name, secret):
return query_db('SELECT secret FROM users WHERE name=?', (name), one=True) == secret
@staticmethod
def add(name, assignee):
return query_db('INSERT INTO todos (name, assignee, done) VALUES (?, ?, ?)', (name, assignee, int(0)))
def complete(self):
self.done = not self.done
return query_db('UPDATE todos SET done=? WHERE id=?', (int(self.done), self.id))
def delete(self):
return query_db('DELETE FROM todos WHERE id=?', (self.id,))
def reassign(self, new_assignee):
return query_db('UPDATE todos SET assignee=? WHERE id=?', (new_assignee, self.id))
def rename(self, new_name):
return query_db('UPDATE todos SET name=? WHERE id=?', (new_name, self.id))
@classmethod
def get_all(cls):
cls.todo = []
for task in query_db('SELECT * FROM todos'):
cls.todo.append(todo(task['id'], task['name'], task['assignee'], bool(task['done'])))
return cls.todo
@classmethod
def get_by_id(cls, todo_id):
task = query_db('SELECT * FROM todos WHERE id=?', (todo_id,), one=True)
if task is not None:
return todo(task['id'], task['name'], task['assignee'], task['done'])
return []
@classmethod
def get_by_user(cls, assignee):
cls.todo = []
for task in query_db('SELECT * FROM todos WHERE assignee=?', (assignee,)):
cls.todo.append(todo(task['id'], task['name'], task['assignee'], bool(task['done'])))
return cls.todo
There are a lot of queries to the database. We have the schema for the SQL database (application/schema.sql
):
DROP TABLE IF EXISTS `users`;
DROP TABLE IF EXISTS `todos`;
CREATE TABLE `users` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`secret` TEXT NOT NULL
);
INSERT INTO `users` (`name`, `secret`) VALUES
('admin', '%s');
CREATE TABLE `todos` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`done` INTEGER NOT NULL,
`assignee` TEXT NOT NULL
);
INSERT INTO `todos` (`name`, `done`, `assignee`) VALUES
('HTB{f4k3_fl4g_f0r_t3st1ng}', 0, 'admin');
At the bottom we see that the user called admin
has a “todo” with the flag. Therefore, the objective of this challenge is to somehow read this “todo” to get the flag.
Finding the vulnerability
The flaw is here:
# TODO: There are not view arguments involved, I hope this doesn't break
# the authentication control on the verify_integrity() decorator
@api.route('/list/all/')
def list_all():
return jsonify(todo.get_all())
Because todo.get_all
does not check if we have permissions to read other users’ information:
@classmethod
def get_all(cls):
cls.todo = []
for task in query_db('SELECT * FROM todos'):
cls.todo.append(todo(task['id'], task['name'], task['assignee'], bool(task['done'])))
return cls.todo
But first, we need to create a new account. Otherwise, we won’t be able to use the app. A new account is created by inserting a new “todo”:
Notice that the browser makes a request to /api/list/<assignee>/?secret=<secret>
:
So, we will exploit a Broken Access Control vulnerability going to /api/list/all/?secret=<secret>
, and we will see some information for user admin
:
Flag
And the flag is also there: HTB{l3ss_ch0r3s_m0r3_h4ck1ng...right?!!1}
.