Flask, flask, flask...

Что вообще представляет из себя Flask и с чем его едят?

Flask - это микрофреймворк для создания веб-приложений на языке Python. Он предоставляет минимальный набор инструментов для создания веб-приложений, но при этом оставляет достаточно свободы для выбора архитектуры и инструментов, которые будут использоваться в проекте.

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

Одной из главных особенностей Flask является его простота и легковесность. Он не навязывает определенную архитектуру и не требует большого количества настроек, что делает его идеальным выбором для быстрого прототипирования и разработки небольших проектов.

Кроме того, Flask имеет отличную документацию и активное сообщество разработчиков, что делает его очень доступным для изучения и использования.

Отладка приложений

Отладка приложений на Flask может быть выполнена с помощью встроенного в Flask отладчика, который позволяет отслеживать ошибки и исключения в приложении. Для включения которого, необходимо установить параметр debug=True в объекте приложения Flask. Это позволит отображать подробную информацию об ошибках в браузере.

Когда режим отладки включен, Flask будет выводить подробную информацию об ошибках и исключениях, что может помочь разработчику быстрее и легче находить и исправлять ошибки в приложении. Однако, включение режима отладки на продакшен-сервере может представлять угрозу безопасности, поэтому debug pin обычно используется только во время разработки и тестирования приложения. Данный пин может быть использован разработчиком для выполнения python комманд на сервере, и если забыть его отключить, а потом залить приложение в продакшен на сервер, мы получим уязвимость.

Процесс отладки приложения на Flask может быть выполнен следующим образом:

  1. Включите режим отладки, установив параметр debug=True в объекте приложения Flask.

  2. Запустите приложение Flask.

  3. Откройте браузер и перейдите на страницу приложения.

  4. Если возникнет ошибка, Flask отобразит подробную информацию об ошибке в браузере.

  5. Используйте эту информацию для исправления ошибки в коде приложения.

  6. После исправления ошибки перезапустите приложение Flask и проверьте, что ошибка больше не возникает.

  7. Повторяйте этот процесс до тех пор, пока все ошибки не будут исправлены.

Как искать такие приложения?

Отличительной чертой приложений на Flask является возможность получения доступа к консоли отладки, что может быть опасно для безопасности приложения, если оно находится в открытом доступе. Также данные приложения с включенным режимом отладки обычно работают на 5000 порту. Консоль отладки в таких приложениях находится по следующему пути: http(s)://IP/console

Простейший сервер

Давайте напишем свой простейший сервер на данном микрофреймворке.

from flask import *

app = Flask(__name__)

@app.route('/')
def hello_world():
	return 'Hello, World!'


if __name__ == '__main__':
	app.run(debug=True)

Разберем построчно данный код:

  1. from flask import * - импортируем все классы из библиотеки Flask.

  2. app = Flask(__name__) - создаем экземпляр класса Flask и передаем ему имя текущего модуля в качестве аргумента. Это нужно для того, чтобы Flask мог правильно определить путь к шаблонам и статическим файлам.

  3. @app.route('/') - декоратор, который связывает URL-адрес с функцией, которая будет обрабатывать запросы на этот адрес. В данном случае, мы связываем корневой URL-адрес с функцией hello_world().

  4. def hello_world(): - определяем функцию hello_world(), которая будет обрабатывать запросы на корневой URL-адрес.

  5. return 'Hello, World!' - возвращаем строку "Hello, World!" в качестве ответа на запрос.

  6. app.run(debug=True) - Запускаем наше приложение в режиме отладки

Этот код создает простейший сервер на Flask, который будет отвечать на запросы на корневой URL-адрес строкой "Hello, World!".

Как выглядит запуск сервера из консоли:

Рендеринг нашей страницы:

При запуске нашего сервера также мы получили Debugger PIN, который нужен разработчику для входа в панель отладки. Злоумышленник, который зайдет на сайт и попробует войти в консоль столкнется с следующим сообщением:

Сложность получения данного пина злоумышленником заключается в том, что для его генерации нунжны некоторые значения, которые нельзя получить не имея доступа к целевой системе.

В свою очередь разработчик(ну или любой другой человек), зная данный пин, может исполнять любые команды на python, которые после будут исполнены. Причем ограничения на ввод команд никаких нет, то есть вы можете хоть просто создавать переменные и выводить их, хоть импортировать другие модули для дальнейшей эксплуатации. В этом и кроется опасность, мы не можем запретить никому вводить какие-либо вводить команды. Защитой отладчика от других пользователей является только пин!

Анализ исходного кода

Давайте разберемся как генеруется PIN. Какие у него есть дефолтные значения, а после попробуем уже самостоятельно сгенерировать его.

Далее будет рассматриваться исходный код werkzeug, ссылка на исходники: ссылка

Первым делом происходит вызов функции get_pin_and_cookie_name.

В данной функции и происходит процесс генерации имени куки и PIN'а. В процессе генерации ключа участвуют 2 набора байт:

  • Публичный набор
    Эта информация существует только для того, чтобы сделать файл cookie уникальным на компьютере, а не в качестве функции безопасности.

  • Приватный набор
    Эта информация предназначена для того, чтобы злоумышленнику было сложнее угадать эти значения. Маловероятно, что они будут содержаться где-либо на неаутентифицированной странице отладки, поэтому для того чтобы их узнать придется потрудится.

Публичный набор состоит из:

  1. Username

  2. Modname

  3. `getattr(app, "name", type(app).name),

  4. `getattr(mod, "file", None),

Приватный набор состоит из:

  1. MAC адреса интерфейса в десятичной системе

  2. machine_id

Далее эти два набора байт проходят через цикл и хешируются алгоритмом sha1.

Сразу после этого цикла на основе результата хеширования генерируется имя куки, которое будет присваиваться пользователю после входа в консоль. Важно заметить, что генерируется именно имя куки, а не значение. Ее имя будет постоянное, а с генерацией значения мы разберемся чуть позже. Строка, в которой происходит назначение имени куки:

Значения переменной h после хеширования, зеленым цветом выделено значение, которое берется как значение имени куки:

Далее идет заключающий цикл, где пину придается окончательный вид.

Результат цикла:

Пройдемся по каждому значению из наборов, чтобы лучше понимать какие данные нам необходимо будет ввести.

Имя пользователя

Имя текущего пользователя можно получить несколькими способами:

  1. Чтение файла /proc/self/cgroup

  2. Чтение файла /etc/passwd

modname

  • По дефолту стоит <flask.app>, скорее всего менять не придется

getattr(app, "name", type(app).name) и getattr(mod, "file", None)

  1. Стандартное значение содержащее значение Flask

  2. Путь до flask

Скрин на котором видны названные выше переменные:

Получение пути до Flask

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

MAC или тотже UUID

При получении интерфейса могут возьникнуть небольшие проблеммы, если этих интерфейсов несколько. Тут стоит пропробовать все интерфейсты, которые имеют доступ к интернету. Поэтому если у вас не подходит deubg pin, то попробуйте сменить интерфейс. Список доступных интерйесов вы можете увидеть с помощью чтения файла /proc/net/dev. А вот MAC адрес интерфейста можно увидеть с помощью файла /sys/class/net/INT/address, где INT - любой интерфейс.

Machine_id

Этот байт состоит из нескольких частей:

  1. Полное содержимое файла /etc/machine-id

  2. Часть файла после последнего слеша /proc/self/cgroup

Первая часть:
cat /etc/machine_id
// b643ebdac5ee44789d21e98a03ce4bb5

Вторая часть:
cat /proc/self/cgroup 
// 0::/user.slice/user-1000.slice/session-2.scope

> Из всего вывода файла /proc/self/cgroup вам понадобится лишь следующая часть:
session-2.scope

Конечное значение переменной:
// b643ebdac5ee44789d21e98a03ce4bb5session-2.scope

Итого в конце герации цикла мы имеем следующие переменные:

Генерации куки

Начало генерации куки идет еще с функции get_pin_and_cookie_name, которая в конечном результате возвращает нам значения имени куки, а также пин.

  1. Генерация имени куки.

  2. Генерация значения куки.
    Генерация значения куки просходит в фалйе __init__.py и состоит из хеша пина и времени в формате UNIX time.

PIN хешируется алгоритом sha1 в функции hash_pin, но стоит заметить, что хешируется не только PIN, а строка содержащая его: "<YOUR PIN> added salt".

Значение сгенерированное в данной функции: 2d97afcb1caf

После соединения всего воедино получаем следующее:

Имя: __wzdb0b4cd30605c88d2470b
Значение: 1681556351|2d97afcb1caf

Хеадер, который нужно будет вставить в BurpSuite:
Cookie: __wzdb0b4cd30605c88d2470b=1681556351|2d97afcb1caf

Генерируем пин

Давайте представим, что код данной страницы выглядит чуть сложнее, чем в прошлом примере. Практически ничего не изменилось, но появилась уязвимость, позволяющая читать любые файлы, так как нет никакой фильтрации ввода:

from flask import *

app = Flask(__name__)

@app.route('/')
def hello_world():
	file = request.args.get('file')
	return open(file).read()


if __name__ == '__main__':
	app.run(debug=True)

Запускаем наш сервер и видим, что в данный момент пином является следующее значение(633-204-910):

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

Для более удобной генерации пина, я написал небольшой скрипт, может кому-нибудь облегчит жизнь) ссылка:

В данный момент нам необходимо прочитать череду файлов, для получения нужных значений, используемых в процессе генерации. Еще раз приведу ниже значения, которые нам понадобяться:

Публичный набор:

  • Username

  • Modname

  • `getattr(app, "name", type(app).name),

  • `getattr(mod, "file", None),

Приватный набор:

  • MAC

  • machine_id

Получаем данные

Всего 6 значений, но некоторые из них имеют стандартные значения, или их очень легко получить, например:

1) Appname

getattr(app, "__name__", type(app).__name__), по умолчанию имеет значение Flask

2) PATH

getattr(mod, "__file__", None), путь до Flask, который мы может легко получить, вызвав любую ошибку, например: в функции чтения файлов вместо ожидаемого на вход файла введем директорию /. В результате получаем ошибку и еще одно значение(/home/user/.local/lib/python3.11/site-packages/flask/app.py), которое нам понадобится для дальнейшей генерации пина.

3) Modname

Modname тоже имеет стандартное значение: flask.app

4) Username

Для получения дальнейших значений будет необходим доступ к чтению файлов

Тут в первую очередь стоит попробовать пользователя www-data, ну или же попробовать всех пользователей, которые есть в файле /etc/passwd. В данном случае в файле /etc/passwd есть только 3 пользователя, которые могут запустить данный сервер:

Суперпользователь
root:x:0:0:root:/root:/usr/bin/zsh

Системный пользователь, который используется веб-сервером Apache для выполнения процессов веб-сервера. 
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

Обычный пользователь(от него и был запущен сервер)
user:x:1000:1000:user,,,:/home/user:/usr/bin/zsh

Тут стоит обратить внимание на значение, которое мы получили в предыдущем этапе(/home/**user**/.local/lib/python3.11/site-packages/flask/app.py). В нем уже есть намек от имени какого пользователя запущен сервер.

5) MAC

В этом и следующем этапе получение данных не такое простое, их нельзя получить из открытых источников, а так же они обычно всегда уникальные.

Получение MAC'а делиться на 2 этапа:

  1. Получение имени сетевого интерфейса

  2. Получение MAC'а для данного интерфейса

1. Получение имени сетевого интерфейса

Получить имена всех сетевых интерфейсов можно прочитав файл /proc/net/dev

На скрине ниже видно, что трафик идет только на интерфейсе wlan0, следовательно скорее всего он нам и нужен

2. Получение MAC'а для данного интерфейса

Получить MAC для любого интерфеса можно прочитав файл /sys/class/net/INT/address, где INT - имя интерфейса

6) Machine_id

Последняя часть, необходимая для генерации пина это machine_id

Его получение так же делится на 2 этапа:

  1. Прочитать файл /etc/machine_id

  2. Прочитать файл /proc/self/cgroup

На самом деле нам нужно не все содержимое этих файлов(это было описано в части "Анализ исходного кода"), но для корректной работы моего скрипта требуется ввести полное содержимое файлов.

Получение значений:

Итого имеем:

1) Appname
Flask

2) PATH
/home/user/.local/lib/python3.11/site-packages/flask/app.py

3) Modname
flask.app

4) Username
user

5) MAC 
fc:44:82:9d:ba:02

6) Machine id
b643ebdac5ee44789d21e98a03ce4bb5
cgroup 0::/user.slice/user-1000.slice/session-2.scope

Теперь нам остется только подствить данные значения в скрипт и сгенерировать этот пин. Команда, которая сгенерирует данный пин по данным выше значениям:
`python3 gen.py --appname Flask --path /home/user/.local/lib/python3.11/site-packages/flask/app.py --modname flask.app --username user --mac fc:44:82:9d:ba:02 --machine_id b643ebdac5ee44789d21e98a03ce4bb5 --cgroup 0::/user.slice/user-1000.slice/session-2.scope

Результат:

Вуаля! Пин, который выдал сервер разработчику и наш сгенерированный пин одинаковые, а это значит, что теперь мы можем получить доступ к отладочной консоли и выполнить произвольный код! Как говорилось ранее, обычно консоль находится по адресу: http://127.0.0.1:5000/console и при входе в консоль нас встречает окошко с просьбой ввести PIN. После ввода которого мы и получим выполнение произвольного кода.

Пример исполнения команд с помощью среды python:

Исполним следующее выражение, где вместо CMD может быть любая команда:
`a = import('os').popen('CMD').read().split('\n')[:-1]; print(*a)

Команды на сервере успешно выполняются! А это значит, что мы из чтения файлов получили полноценный удаленный доступ к системе.

Избегаем заблокированной консоли

При введении некорректного PIN'а, мы обычно получаем следующий ответ:

Но если попыток неправльного входа будет слишком много, то переменная exhausted изменяется на true:

Сообщение, которое вы увидите в браузере:

Даже если мы после попробуем ввести корректный пин, то нам выдаст ошибку:

Чтобы обойти это мы должны использовать ранее сгенерированную куку:
Cookie: __wzdb0b4cd30605c88d2470b=1681556351|2d97afcb1caf. Также нужно изменить параметр &pin=... на &frm=0. После этого мы сможем успешно выполнить код.

Резюме

В данной статье мы разобрали как можно с помощью уязвимости чтения файлов получить доступ к интерактивной консоли Python.

Как итог: включение режима отладки в приложении Flask может открыть уязвимости, так как в этом режиме приложение будет выводить подробную информацию об ошибках, включая трассировку стека и значения переменных. Это может быть полезно для разработки и отладки приложения, но может также предоставить злоумышленникам ценную информацию о структуре приложения и его уязвимостях. Злоумышленнику может получить доступ к интерактивной консоли Python и выполнить произвольный код в контексте приложения. Это может привести к компрометации данных пользователей, утечке конфиденциальной информации и другим серьезным последствиям.

Поэтому рекомендуется отключать режим отладки в приложениях Flask на продакшен-серверах и использовать его только для разработки и отладки на локальной машине.

На этом у меня все, если есть вопросы или предложения, то прошу не стесьняться и писать мне в телеграм или на почту.
telegram: @SidneyJob
mail: SidneyJob13@gmail.com

Комментарии (0)