PHP Phar 包签名篡改:修改 Stub 与 Manifest 绕过哈希校验的技巧
大家好,今天我们要深入探讨一个安全领域的话题:PHP Phar 包的签名篡改。Phar 包是 PHP 中一种便捷的打包和分发应用程序的方式,它允许将多个文件打包成一个单独的可执行文件。为了保证 Phar 包的完整性和安全性,Phar 提供了签名机制。然而,如果签名机制配置不当或存在漏洞,攻击者就有可能篡改 Phar 包的内容,并绕过签名校验,从而执行恶意代码。
本次讲座我们将重点关注如何通过修改 Phar 包的 Stub 和 Manifest 来绕过哈希校验。我们将深入理解 Phar 包的结构,分析签名校验的流程,并提供实际的代码示例来演示攻击过程,最后给出防御建议。
Phar 包结构剖析
理解 Phar 包的结构是进行任何攻击的前提。一个标准的 Phar 包主要由以下几个部分组成:
- Stub: 这是 Phar 包的引导代码,通常是一段 PHP 代码,用于在 Phar 包被执行时加载和初始化 Phar 环境。Stub 必须以
__HALT_COMPILER();结尾,表明 Stub 的结束和 Phar 数据的开始。 - Manifest: Manifest 包含了 Phar 包中所有文件的元数据,例如文件名、大小、创建时间、压缩方式以及每个文件的内容哈希值。Manifest 本身也会被签名。
- File Contents: 这是 Phar 包中实际的文件内容,例如 PHP 脚本、图片、样式表等。
- Signature: 签名是对 Manifest 和 Stub 的哈希值进行加密的结果,用于验证 Phar 包的完整性和真实性。
可以用下面的表格来更清晰地表示:
| 部分 | 描述 |
|---|---|
| Stub | PHP 引导代码,用于加载和初始化 Phar 环境。必须以 __HALT_COMPILER(); 结尾。 |
| Manifest | 包含 Phar 包中所有文件的元数据,包括文件名、大小、创建时间、压缩方式以及每个文件的内容哈希值。 |
| File Contents | Phar 包中实际的文件内容,例如 PHP 脚本、图片、样式表等。 |
| Signature | 对 Manifest 和 Stub 的哈希值进行加密的结果,用于验证 Phar 包的完整性和真实性。签名算法可以是 MD5, SHA1, SHA256, SHA512 或 OpenSSL 的非对称加密算法 (需要 openssl 扩展支持) |
Phar 包签名校验流程
在 PHP 尝试执行 Phar 包时,会进行如下的签名校验流程:
- 读取 Stub: 首先,PHP 读取 Phar 包的 Stub 部分,并执行其中的 PHP 代码。
- 定位 Manifest: PHP 解析 Stub 结尾的
__HALT_COMPILER();,并从其后开始读取 Manifest。 - 读取 Manifest: PHP 读取 Manifest,并解析其中的文件元数据,包括文件名、大小和哈希值。
- 计算哈希值: PHP 根据 Manifest 中记录的算法,重新计算每个文件的哈希值。
- 验证哈希值: 将重新计算的哈希值与 Manifest 中存储的哈希值进行比较,如果所有哈希值都匹配,则表示文件内容未被篡改。
- 验证签名: PHP 使用公钥(如果使用非对称加密)或预共享密钥(如果使用对称加密)来验证签名,确保 Manifest 和 Stub 没有被篡改。
如果任何一个步骤失败,PHP 将拒绝执行 Phar 包,并抛出异常。
篡改 Stub 绕过哈希校验
一种绕过哈希校验的方法是篡改 Stub。由于签名是对 Stub 的哈希值进行加密的结果,如果我们可以修改 Stub,同时仍然保证 Phar 包能够正常执行,那么就可以绕过签名校验。
攻击原理:
- 由于 PHP 在执行 Stub 时会忽略
__HALT_COMPILER();之后的所有内容,因此我们可以将恶意代码添加到 Stub 的末尾,__HALT_COMPILER();之前。 - 如果签名校验只校验了 Stub 的部分内容,或者使用了较弱的哈希算法,那么我们可以通过修改 Stub 的填充数据,使得修改后的 Stub 的哈希值与原始哈希值相同。
攻击示例:
假设我们有一个名为 test.phar 的 Phar 包,其 Stub 如下:
<?php
Phar::mapPhar('test.phar');
include 'phar://test.phar/index.php';
__HALT_COMPILER();
我们可以将恶意代码添加到 Stub 的末尾,例如:
<?php
Phar::mapPhar('test.phar');
include 'phar://test.phar/index.php';
// 恶意代码
file_put_contents('evil.txt', 'This is evil code');
__HALT_COMPILER();
在这个例子中,我们在 __HALT_COMPILER(); 之前添加了一段恶意代码,用于创建一个名为 evil.txt 的文件。由于 PHP 在执行 Stub 时会忽略 __HALT_COMPILER(); 之后的所有内容,因此这段恶意代码会被执行,而不会影响 Phar 包的正常功能。
代码示例:
以下是一个 PHP 脚本,用于修改 Phar 包的 Stub:
<?php
$pharFile = 'test.phar';
$evilCode = "n// 恶意代码nfile_put_contents('evil.txt', 'This is evil code');n";
// 读取 Phar 包
$phar = new Phar($pharFile);
// 获取 Stub
$stub = $phar->getStub();
// 添加恶意代码到 Stub
$newStub = str_replace('__HALT_COMPILER();', $evilCode . '__HALT_COMPILER();', $stub);
// 设置新的 Stub
$phar->setStub($newStub);
echo "Phar 包的 Stub 已被修改。n";
?>
执行结果:
当我们执行这个脚本后,test.phar 的 Stub 会被修改,恶意代码会被添加到 Stub 的末尾。当我们执行 test.phar 时,恶意代码会被执行,并创建一个名为 evil.txt 的文件。
防御方法:
- 使用强哈希算法(例如 SHA256 或 SHA512)来计算 Stub 的哈希值。
- 对整个 Stub 进行签名,而不是只对部分内容进行签名。
- 限制 Phar 包的执行权限,例如只允许执行来自受信任来源的 Phar 包。
篡改 Manifest 绕过哈希校验
另一种绕过哈希校验的方法是篡改 Manifest。如果我们可以修改 Manifest,同时仍然保证 Phar 包能够正常执行,那么就可以绕过签名校验。
攻击原理:
- 如果签名校验只校验了 Manifest 的部分内容,或者使用了较弱的哈希算法,那么我们可以通过修改 Manifest 的填充数据,使得修改后的 Manifest 的哈希值与原始哈希值相同。
- 我们可以通过修改 Manifest 中文件的哈希值,使得 PHP 认为文件内容未被篡改。
攻击示例:
假设我们有一个名为 test.phar 的 Phar 包,其 Manifest 如下:
a:2:{s:9:"index.php";a:5:{s:4:"name";s:9:"index.php";s:4:"size";i:123;s:9:"compressed";i:0;s:8:"crc32";i:1234567890;s:8:"hash";s:40:"e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4";}s:8:"config.php";a:5:{s:4:"name";s:8:"config.php";s:4:"size";i:456;s:9:"compressed";i:0;s:8:"crc32";i:987654321;s:8:"hash";s:40:"6b86b273ff34fce19d6b804eff5a3f5747ada4aa";}}
我们可以修改 Manifest 中 index.php 的哈希值,例如:
a:2:{s:9:"index.php";a:5:{s:4:"name";s:9:"index.php";s:4:"size";i:123;s:9:"compressed";i:0;s:8:"crc32";i:1234567890;s:8:"hash";s:40:"0000000000000000000000000000000000000000";}s:8:"config.php";a:5:{s:4:"name";s:8:"config.php";s:4:"size";i:456;s:9:"compressed";i:0;s:8:"crc32";i:987654321;s:8:"hash";s:40:"6b86b273ff34fce19d6b804eff5a3f5747ada4aa";}}
在这个例子中,我们将 index.php 的哈希值修改为 0000000000000000000000000000000000000000。如果我们同时修改 index.php 的内容,并使其哈希值也为 0000000000000000000000000000000000000000,那么 PHP 会认为 index.php 的内容未被篡改。
代码示例:
以下是一个 PHP 脚本,用于修改 Phar 包的 Manifest:
<?php
$pharFile = 'test.phar';
$evilCode = "<?php file_put_contents('evil.txt', 'This is evil code'); ?>";
// 读取 Phar 包
$phar = new Phar($pharFile);
// 修改 index.php 的内容
$phar['index.php'] = $evilCode;
// 获取 Manifest
$manifest = $phar->getMetadata();
// 修改 Manifest 中 index.php 的哈希值
$manifest['index.php']['hash'] = sha1($evilCode);
// 设置新的 Manifest
$phar->setMetadata($manifest);
echo "Phar 包的 Manifest 已被修改。n";
?>
执行结果:
当我们执行这个脚本后,test.phar 的 Manifest 会被修改,index.php 的哈希值会被修改为恶意代码的哈希值。当我们执行 test.phar 时,恶意代码会被执行,并创建一个名为 evil.txt 的文件。
防御方法:
- 使用强哈希算法(例如 SHA256 或 SHA512)来计算文件的哈希值。
- 对整个 Manifest 进行签名,而不是只对部分内容进行签名。
- 在验证签名之前,先验证 Manifest 的结构是否合法。
- 使用数字签名验证 Phar 包的来源,确保 Phar 包来自受信任的开发者。
代码示例: 创建和签名 Phar 包
为了更好地理解上述攻击,我们先来看一个创建和签名 Phar 包的例子。
<?php
try {
$phar = new Phar('myphar.phar');
$phar->startBuffering();
// 添加文件
$phar->addFromString('index.php', '<?php echo "Hello from myphar!"; ?>');
$phar->addFromString('config.php', '<?php return ["db_host" => "localhost"]; ?>');
// 设置 Stub
$stub = '<?php
Phar::mapPhar("myphar.phar");
include "phar://myphar.phar/index.php";
__HALT_COMPILER(); ?>';
$phar->setStub($stub);
// 生成私钥和公钥 (仅用于演示,实际应用中请使用更安全的方式)
$privateKey = openssl_pkey_new(array(
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
));
openssl_pkey_export($privateKey, $privateKeyString);
$publicKeyDetails = openssl_pkey_get_details($privateKey);
$publicKeyString = $publicKeyDetails["key"];
// 签名 Phar 包
$phar->setSignatureAlgorithm(Phar::OPENSSL, $privateKeyString);
$phar->stopBuffering();
echo "Phar 包已创建和签名。n";
// 保存公钥到文件,用于验证签名
file_put_contents('public.key', $publicKeyString);
} catch (Exception $e) {
echo "创建 Phar 包失败: " . $e->getMessage() . "n";
}
?>
这段代码创建了一个名为 myphar.phar 的 Phar 包,包含 index.php 和 config.php 两个文件,并使用 OpenSSL 生成的私钥对其进行签名。同时,将公钥保存到 public.key 文件中,用于验证签名。
代码示例: 验证 Phar 包的签名
以下代码演示了如何验证 Phar 包的签名:
<?php
try {
$phar = new Phar('myphar.phar');
// 读取公钥
$publicKeyString = file_get_contents('public.key');
// 验证签名
$result = $phar->verifySignature(Phar::OPENSSL, $publicKeyString);
if ($result) {
echo "Phar 包签名验证成功。n";
} else {
echo "Phar 包签名验证失败。n";
}
} catch (Exception $e) {
echo "验证 Phar 包签名失败: " . $e->getMessage() . "n";
}
?>
这段代码读取 public.key 中的公钥,并使用 Phar::verifySignature() 函数验证 myphar.phar 的签名。
深入分析:利用 Hash Collision 攻击
虽然强哈希算法如 SHA256 和 SHA512 在抵御篡改方面比 MD5 和 SHA1 更强大,但理论上仍然存在哈希碰撞的可能性。虽然找到实际的碰撞非常困难,但在某些特定场景下,攻击者可能会尝试利用哈希碰撞来绕过签名校验。
攻击原理:
攻击者尝试构造两个不同的文件,它们的哈希值相同。然后,攻击者将恶意文件替换原始文件,并修改 Manifest 中的哈希值,使其与恶意文件的哈希值相同。由于哈希值相同,因此签名校验会通过,攻击者就可以成功执行恶意代码。
现实难度:
- 找到 SHA256 或 SHA512 的哈希碰撞在计算上极其困难,需要大量的计算资源和时间。
- Phar 包通常包含多个文件,攻击者需要找到多个文件的哈希碰撞,这进一步增加了攻击的难度。
防御方法:
- 使用带盐的哈希算法:在计算哈希值时,添加一个随机的盐值,使得攻击者难以构造哈希碰撞。
- 使用数字签名:使用数字签名可以验证 Phar 包的来源,确保 Phar 包来自受信任的开发者。
- 代码签名证书:使用代码签名证书对 Phar 包进行签名,可以提供更高的安全保障。
针对不同签名算法的攻击难度
不同的签名算法具有不同的安全强度。攻击者选择攻击方法时,会考虑到攻击成本和成功率。下面是一个简单的对比表格:
| 签名算法 | 攻击难度 | 防御建议 |
|---|---|---|
| MD5 | 容易受到碰撞攻击,不安全。 | 绝对不要使用 MD5 作为签名算法。 |
| SHA1 | 容易受到长度扩展攻击和碰撞攻击,不再安全。 | 绝对不要使用 SHA1 作为签名算法。 |
| SHA256 | 目前认为相对安全,但理论上存在碰撞攻击的可能性。 | 使用带盐的哈希算法,并结合数字签名。 |
| SHA512 | 目前认为非常安全,但理论上存在碰撞攻击的可能性。 | 使用带盐的哈希算法,并结合数字签名。 |
| OpenSSL RSA | 使用非对称加密,安全性较高,但需要妥善保管私钥。 | 使用足够长的密钥长度(至少 2048 位),妥善保管私钥,并定期更换密钥。 |
| OpenSSL ECDSA | 基于椭圆曲线加密,安全性较高,密钥长度更短,但对 OpenSSL 扩展依赖性强。 | 使用足够强的曲线算法(例如 secp256r1),妥善保管私钥,并定期更换密钥。 |
关于 Phar 相关的配置和安全最佳实践
除了签名算法,PHP 的 Phar 相关配置也会影响安全性。以下是一些重要的配置项和安全最佳实践:
phar.readonly: 此配置项控制 Phar 包是否只读。设置为1可以防止 Phar 包被修改,提高安全性。应该始终在生产环境中启用此选项。phar.require_hash: 此配置项控制是否强制要求 Phar 包必须包含哈希值。设置为1可以防止 Phar 包被篡改。同样应该在生产环境中启用。- 代码审查: 对 Phar 包的代码进行定期审查,可以发现潜在的安全漏洞。
- 最小权限原则: 运行 Phar 包的用户应该只具有执行所需的最少权限。
- 输入验证: 对 Phar 包的输入进行严格的验证,防止恶意输入导致安全问题。
- 定期更新: 定期更新 PHP 和 Phar 扩展,可以修复已知的安全漏洞。
- 使用安全的文件存储: 将 Phar 包存储在安全的文件系统上,防止未经授权的访问。
案例分析:真实世界的 Phar 漏洞
虽然公开披露的 Phar 漏洞相对较少,但并不意味着它们不存在。以下是一些可能存在的 Phar 漏洞场景:
- 不安全的 Stub: 如果 Stub 代码存在漏洞,攻击者可以利用该漏洞执行任意代码。例如,Stub 代码可能存在文件包含漏洞或命令执行漏洞。
- 弱签名算法: 如果使用弱签名算法(例如 MD5 或 SHA1),攻击者可以更容易地绕过签名校验。
- 签名校验逻辑错误: 如果签名校验逻辑存在错误,攻击者可以利用该错误绕过签名校验。例如,签名校验可能只校验了 Stub 的部分内容,或者没有正确验证 Manifest 的结构。
- 不安全的 Phar 配置: 如果 Phar 相关的配置不安全(例如
phar.readonly设置为0),攻击者可以更容易地篡改 Phar 包。
在实际应用中,需要综合考虑各种安全因素,并采取相应的防御措施,以确保 Phar 包的安全性。
安全总结:强化 Phar 包的安全防护
通过本次讲座,我们深入了解了 PHP Phar 包的结构和签名校验流程,并探讨了通过修改 Stub 和 Manifest 绕过哈希校验的技巧。理解这些攻击方法有助于我们更好地保护 Phar 包的安全性。
以下是一些关键的防御建议:
- 使用强签名算法: 选择 SHA256 或 SHA512 等强哈希算法,并结合数字签名。
- 严格验证签名: 在执行 Phar 包之前,务必严格验证签名,确保 Phar 包的完整性和真实性。
- 启用安全配置: 启用
phar.readonly和phar.require_hash等安全配置,防止 Phar 包被篡改。 - 代码审查: 对 Phar 包的代码进行定期审查,发现潜在的安全漏洞。
- 最小权限原则: 运行 Phar 包的用户应该只具有执行所需的最少权限。
通过采取这些防御措施,我们可以大大提高 Phar 包的安全性,防止恶意攻击。