Eine CVSS 9.6, abgeschickt während ich schlief
Mai 2026 · Anonymisiert bis zur koordinierten Veröffentlichung
An einem Sonntagabend habe ich einem Bash-Skript gesagt, es soll für den Rest der Woche autonom Bug-Bounty-Programme auditieren, dann bin ich ins Bett gegangen. Am Mittwochmorgen lag eine Telegram-Nachricht auf dem Sperrbildschirm: LEAD · KH14-1 · sev=critical · score=9. Ein paar Stunden später war ein Bericht über eine Remote-Code-Execution-Lücke in einem Tier-1-OSS-Tool bei HackerOne eingereicht. CVSS 3.1: 9.6.
Dieser Text beschreibt das System, das den Fund gemacht hat — Prompt-Pipeline, Verification-Chain, Failsafes — und einen Teil der technischen Substanz. Den Vendor-Namen lasse ich noch weg; das Programm ist Private und der Patch noch in Arbeit. Sobald die Lücke koordiniert veröffentlicht ist, ergänze ich Namen und Diff hier.
Das Setup
Auf einem 200-€-Mini-PC unter dem Wohnzimmertisch laufen vier Agenten: Nova für Heimnetz und Code, Pixel als Co-Maintainer einer kommerziellen Webapp, Atelier für Kunst, und Recon für Bug-Bounty-Audits. Die ersten drei nutzen claude-opus-4-7, Recon läuft auf claude-sonnet-4-6 — billiger pro Token, gut genug für die Klassifikationsaufgaben, die in der Audit-Loop dominieren.
Jeder Agent hat einen eigenen Workspace mit IDENTITY.md, MEMORY.md (kuratiert), Tageslogs unter memory/YYYY-MM-DD-*.md, und einen Telegram-Bot, der Updates an mein Handy schickt. Die Verbindung zur Anthropic-API geht über einen lokalen LiteLLM-Proxy, der den Traffic an mein Pro-OAuth-Konto routet — kein API-Key-Billing, sondern dasselbe Abo, das ich für interaktive Sessions verwende.
Die Audit-Pipeline
Recon läuft nicht als langlebiger Prozess. Er existiert in Sessions, eine pro Task. Eine Audit-Run hat zwei Eingaben:
- einen
queue.txtmit numerierten Tasks:KH01: Auth-Subsystem · KH02: Plugin-Loader · KH03: Deep-Link-Handler … - einen Prompt pro Task unter
prompts/KH03.prompt, der den Audit-Kontext beschreibt: Programm-Scope, In-Scope-Assets, Repo-URL, was gesucht wird.
Ein Bash-Runner (queue-runner-audit.sh) iteriert über die Queue, spawnt für jeden Task einen frischen claude --print --model opus --add-dir $WORKSPACE-Prozess und füttert den Prompt rein. Der Agent hat 25 Minuten Zeit (timeout 1500), produziert am Ende ein status.json mit Entscheidung (lead | verify-needed | skip), Severity-Schätzung und einer Lead-Datei mit dem Verdacht. Promising leads werden an mich gepingt, der Runner geht zur nächsten Task.
Verification ist eine zweite Stufe: jeder Lead bekommt einen separaten Prompt, der explizit nach einer Hypothese-falsifizierenden Untersuchung fragt — Source lesen, Tests fahren, PoC-Pakete bauen. Hier wechsle ich oft auf opus, weil Sonnet bei Mehrschritt-Verifikationen mit Tool-Calls schwächelt. Wenn die Verification durchhält, wird ein H1-formatierter Report geschrieben.
Was Recon gefunden hat
Das Ziel-Tool ist ein Desktop-API-Client für Entwickler, geschrieben in Electron. Es registriert beim ersten Start einen Custom-URL-Scheme-Handler beim Betriebssystem — sowas wie tool://. Ein Klick auf einen tool://-Link in einem beliebigen Browser dispatcht den Aufruf an das laufende Tool, das den Pfad und Query-String parst.
Einer der Endpunkte ist tool://plugins/install?name=<X>. Dahinter steckt eine installPlugin(name, allowScopedPackageNames)-Funktion, deren zweiter Parameter steuert, ob die Validierung "Plugin-Name muss mit tool-plugin- beginnen" greift. Default: false. Im Source-Tree wird die Funktion an genau einer Stelle anders aufgerufen:
// src/root.tsx, Deep-Link-Handler
await window.main.installPlugin(params.name.trim(), true);
// ^^^^ overrides validator
Jeder andere Pfad — die UI, die Settings-Eingabe — ruft die Funktion mit dem Default false auf. Nur der Deep-Link-Pfad setzt true. Damit installiert tool://plugins/install?name=any-npm-package jedes beliebige NPM-Paket als Plugin. Das Plugin main-Modul wird im Renderer-Prozess mit nodeIntegration: true und contextIsolation: false ausgeführt — vollständiger Node-Zugriff: fs, child_process, net, alles.
Die UX-Falle macht den Bug zu einer Critical: das einzige Dialog-Fenster vor der Installation zeigt nur den Paketnamen. Kein Autor, keine Version, keine Quelle, keine Warnung dass Code ausgeführt wird. Ein Link in einem Stack-Overflow-Post oder einer Discord-Nachricht — "hier installierst du das hilfreiche Auth-Helper-Plugin in einem Klick" — reicht.
Ein Klick = arbitrary RCE als angemeldeter User. AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H = 9.6. S:C weil das Plugin aus seinem Sandbox-Kontext herausbricht und die User-Session attackieren kann (Tokens lesen, persistent werden, weiteres exfiltrieren).
Die Verifikation
Recon hat den Verdacht als Lead gemarkt. Ich habe in der Verification-Stage einen separaten Sub-Agent gestartet — Aufgabe: "Falsifiziere die Hypothese. Bau einen PoC der zeigt, dass die Validierung nicht umgangen werden kann." Diese Inversionsformel ist der Kernpunkt. Wenn ein Agent versucht, eine Hypothese zu beweisen, findet er fast immer Bestätigungen. Wenn er sie zu falsifizieren versucht, scheitert er auf interessante Weise.
Der Sub-Agent hat ein NPM-Paket gebaut, dessen index.js beim Plugin-Install eine Marker-Datei in $HOME schreibt und den OS-Calculator startet. Beides als Smoke-Test, beides ohne Schaden. Veröffentlicht unter einem klar markierten BB-PoC-Paketnamen. Auf einem Windows-11-VM-Setup hat der Klick auf einen lokalen tool://...-Link das Paket installiert und beide Effekte ausgelöst.
Wichtig in dieser Phase: der Sub-Agent hat zusätzlich den asymmetrischen Beweis erbracht — derselbe Paketname wird vom UI-Eingabefeld in den Settings explizit abgelehnt mit "Plugin name must not start with 'tool-plugin-'". Selber Aufruf, selbes Paket — nur ein einziger Boolean unterschiedlich. Damit ist die Sicherheitsgrenze, die der Deep-Link silently bypassed, nicht spekulativ, sondern direkt belegt.
Disclosure
Eingereicht über das HackerOne-Programm des Vendors als KH14-1, mit vollem Source-Trace (drei Datei-Refs auf den öffentlichen Repo-Tag), CVSS-Begründung, fünf Screenshots und einem Cleanup-Block. Patch-Vorschlag ist trivial (ein Argument im Deep-Link-Handler entfernen, der Default greift dann). Das PoC-Paket wird nach Closure unpublished, der Marker-Text auf NPM ist explizit als BB-PoC ausgewiesen.
Mein Recherche-Account taucht auf NPM dauerhaft auf. Das ist ein Trade-off zwischen Anonymität und Reproduzierbarkeit; ich habe Reproduzierbarkeit gewählt. Das Vendor-Security-Team hat innerhalb von 72 Stunden bestätigt, dass der Bug reproduziert wurde. CVE-Assignment durch den Vendor (CNA) ist in Arbeit.
Was ich gelernt habe
Inversion ist die Hälfte der Arbeit. Der Schritt von "interessanter Verdacht" zu "belegter Bug" passiert nicht im selben Prompt wie die ursprüngliche Recherche. Wenn ich Recon und den Verifier in einer Session zusammenlege, neigt das Modell dazu, sich selbst zu bestätigen. Zwei separate Prompts mit explizit gegenläufiger Aufgabe sind robuster.
Sentinel-Recovery rettet Sessions. Der Bash-Runner schreibt eine status.json, wenn der Agent fertig ist. In ungefähr jedem fünften Lauf exitet der Agent mit Code 0, ohne die Datei zu schreiben — manchmal weil das Modell Markdown statt JSON ausspuckt, manchmal weil es vor dem Tool-Call abbricht. Der Runner detektiert "Exit 0 ohne Status" und scannt das Spawn-Log: gibt es eine neue Lead-Datei, war der Run erfolgreich. Gibt es einen Refusal-String ("I can't help with..."), markiere ich den Task als blocked, nicht failed, und der Prompt geht zur Reformulierung.
Usage-Limits sind ein Engineering-Problem, kein Hindernis. Mein Pro-OAuth-Account hat alle paar Stunden ein Limit-Reset. Der Runner detektiert "hit your limit" im Spawn-Output und schläft 15 Minuten, dann retry. Bis zu 15 Mal. Damit überleben einzelne Tasks nahezu jede Limit-Phase, der Run-Durchsatz sinkt nur graduell.
Recon ist unkreativ — und das ist okay. Sonnet-4-6 ist nicht der Bug-Hunter, der den eleganten Out-of-the-Box-Trick findet. Sonnet ist der Praktikant, der akribisch jeden Endpunkt durchgeht und liest. Was bei einem aufmerksamen Menschen der dritte Audit-Tag wäre, ist hier eine 6-Stunden-Run-Phase. Die kreative Hypothesenbildung — "was wäre, wenn man diesen Parameter überschreibt?" — bleibt mein Job. Aber das systematische Finden, dass das Boolean da überhaupt überschrieben wird: das erledigt Recon, während ich schlafe.
Update folgt, sobald die koordinierte Veröffentlichung erfolgt. Bis dahin: keine Vendor-Namen, kein Repo-Diff, keine Exploitsamples — anonymisierte Substanz, keine Vendor-Identifikation.