PHP Phar 签名伪造攻击:利用 Metadata 块的哈希值绕过文件完整性校验
大家好,今天我们来深入探讨一个关于 PHP Phar 档案的安全问题:Phar 签名伪造攻击,特别是利用 Metadata 块的哈希值绕过文件完整性校验的方法。 Phar 档案为 PHP 提供了一种便捷的文件打包和分发机制,但同时也引入了一些安全风险。理解这些风险并掌握防御方法至关重要。
Phar 档案结构概览
首先,我们需要了解 Phar 档案的基本结构。一个 Phar 档案主要由以下几个部分组成:
-
Stub: 一段 PHP 代码,通常以
<?php开头,用于在 Phar 档案被直接执行时引导程序。 -
Manifest: 包含了 Phar 档案中所有文件信息的列表,例如文件名、文件大小、文件权限等。
-
File Content: 实际的文件内容,例如 PHP 脚本、图片、文本文件等。
-
Metadata: 一个可选的数据块,可以包含任何自定义数据,例如版本信息、版权声明等。
-
Signature: 用于验证 Phar 档案完整性的签名,通常使用 MD5、SHA1、SHA256 或 OpenSSL 等算法进行签名。
用表格来展示:
| 部分 | 描述 |
|---|---|
| Stub | PHP 代码片段,用于引导 Phar 档案的执行。 |
| Manifest | 文件列表,包含每个文件的信息(名称、大小、权限等)。 |
| File Content | Phar 档案中包含的实际文件数据。 |
| Metadata | 可选数据块,用于存储自定义信息。 |
| Signature | 签名数据,用于验证 Phar 档案的完整性。 |
Phar 签名机制
Phar 档案的签名机制旨在确保档案在传输过程中没有被篡改。签名通常基于 Manifest 和 Metadata 的哈希值生成。验证过程如下:
- 计算 Manifest 和 Metadata 的哈希值。
- 使用存储在 Signature 部分的公钥(如果是 OpenSSL 签名)或密钥(如果是 MD5/SHA1/SHA256 签名)验证签名。
- 如果验证成功,则认为 Phar 档案是完整的。
签名伪造的潜在风险
尽管签名机制的存在,Phar 档案仍然可能受到攻击,例如签名伪造。一种常见的攻击方式是利用 Metadata 块的哈希值进行绕过。
攻击原理:
如果 Phar 档案使用的签名算法是基于哈希值的(例如 MD5、SHA1、SHA256),并且 Metadata 块的内容可以被攻击者控制,那么攻击者可以构造一个恶意的 Metadata 块,使得其哈希值与原始的哈希值相同。这样,即使 Phar 档案的内容被修改,签名验证仍然会成功,从而绕过完整性校验。
攻击场景:
假设一个应用程序允许用户上传 Phar 档案。应用程序在接收到 Phar 档案后,会验证其签名。如果应用程序使用的签名算法是 MD5,并且 Metadata 块的内容可以被篡改,那么攻击者就可以构造一个恶意的 Phar 档案,使得其 Metadata 块的 MD5 哈希值与原始的哈希值相同。这样,应用程序就会认为 Phar 档案是完整的,并执行其中的恶意代码。
利用 Metadata 块的哈希值绕过文件完整性校验
让我们通过一个具体的例子来说明如何利用 Metadata 块的哈希值绕过文件完整性校验。
示例:
假设我们有一个名为 test.phar 的 Phar 档案,其签名算法是 MD5,并且 Metadata 块的内容如下:
<?php
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('index.php', '<?php echo "Hello, world!"; ?>');
$phar->setMetadata(['version' => '1.0']);
$phar->setSignatureAlgorithm(Phar::MD5);
$phar->stopBuffering();
现在,我们想要修改 index.php 的内容,但是又不想让签名验证失败。我们可以通过以下步骤来实现:
-
计算原始 Metadata 块的 MD5 哈希值。
可以使用以下 PHP 代码来计算:
<?php $phar = new Phar('test.phar'); $metadata = $phar->getMetadata(); $metadata_serialized = serialize($metadata); $original_md5 = md5($metadata_serialized); echo "Original MD5: " . $original_md5 . "n"; ?>假设输出的原始 MD5 哈希值为
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6。 -
修改
index.php的内容。例如,我们可以将
index.php的内容修改为:<?php echo "Hello, malicious world!"; ?> -
构造一个新的 Metadata 块,使得其 MD5 哈希值与原始的哈希值相同。
这是一个难点,需要一定的技巧。 我们需要找到一个字符串,当它被序列化后,其 MD5 哈希值等于
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6。 由于 MD5 是一种哈希算法,理论上存在无限个字符串可以生成相同的哈希值。 我们可以使用碰撞算法来寻找这样的字符串,或者使用暴力破解的方式。 这里为了简化演示,我们假设我们已经找到了一个字符串evil_metadata,当它被序列化后,其 MD5 哈希值等于a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6。重要提示: 实际上,寻找具有特定 MD5 哈希值的字符串在计算上是极其困难的。 这个例子只是为了演示攻击原理,并不代表实际场景中可以轻松找到这样的字符串。
-
更新 Phar 档案的 Metadata 块和
index.php的内容。可以使用以下 PHP 代码来更新 Phar 档案:
<?php try { $phar = new Phar('test.phar'); $phar->startBuffering(); $phar['index.php'] = '<?php echo "Hello, malicious world!"; ?>'; $phar->setMetadata(unserialize('evil_metadata')); // Use the evil metadata $phar->stopBuffering(); } catch (Exception $e) { echo "Exception: " . $e->getMessage() . "n"; } ?>注意: 在实际操作中,
evil_metadata必须是一个序列化后的字符串,并且这个字符串反序列化后,其序列化形式的 MD5 哈希值必须等于原始的哈希值。 -
验证签名。
现在,我们可以验证修改后的 Phar 档案的签名。 由于 Metadata 块的 MD5 哈希值与原始的哈希值相同,因此签名验证将会成功。
<?php try { $phar = new Phar('test.phar'); // Verify the signature (this will likely succeed) echo "Signature is valid!n"; } catch (Exception $e) { echo "Signature verification failed: " . $e->getMessage() . "n"; } ?>即使
index.php的内容已经被修改,签名验证仍然会成功,这意味着我们成功地绕过了 Phar 档案的完整性校验。
完整攻击代码示例(仅用于演示,请勿用于非法用途):
<?php
// 1. Create the initial Phar archive
$phar = new Phar('original.phar');
$phar->startBuffering();
$phar->addFromString('index.php', '<?php echo "Hello, world!"; ?>');
$phar->setMetadata(['version' => '1.0']);
$phar->setSignatureAlgorithm(Phar::MD5);
$phar->stopBuffering();
unset($phar);
// 2. Calculate the original metadata hash
$phar = new Phar('original.phar');
$metadata = $phar->getMetadata();
$metadata_serialized = serialize($metadata);
$original_md5 = md5($metadata_serialized);
echo "Original MD5: " . $original_md5 . "n";
unset($phar);
// 3. This is the *very* hard part - finding a metadata string
// that, when serialized, produces the same MD5 hash.
// In reality, this requires significant computing power and is not trivial.
// For demonstration purposes, we'll use a placeholder.
// In a real attack, you would replace this with the actual evil metadata.
// WARNING: THIS IS A PLACEHOLDER. IT WILL NOT WORK.
$evil_metadata = 'O:8:"stdClass":0:{}'; // VERY likely to *not* have the same MD5 hash!
// 4. Modify the index.php content and inject the evil metadata
try {
$phar = new Phar('original.phar');
$phar->startBuffering();
$phar['index.php'] = '<?php echo "Hello, malicious world! System compromised!"; ?>';
//WARNING: In a real attack, the unserialize() call MUST produce a string that,
// when reserialized, produces the ORIGINAL metadata hash.
$phar->setMetadata(unserialize($evil_metadata));
$phar->stopBuffering();
rename('original.phar', 'evil.phar');
} catch (Exception $e) {
echo "Exception during modification: " . $e->getMessage() . "n";
}
unset($phar);
// 5. Verify the signature
try {
$phar = new Phar('evil.phar');
echo "Signature is valid (but the archive is evil!)n";
// IMPORTANT: NEVER execute a Phar archive without proper validation, even
// if the signature appears valid. Check the contents!
include "phar://evil.phar/index.php"; // Demonstrates execution of malicious code!
} catch (Exception $e) {
echo "Signature verification failed: " . $e->getMessage() . "n";
}
// Cleanup (remove the phar archives)
unlink('original.phar');
if (file_exists('evil.phar')) {
unlink('evil.phar');
}
?>
此代码仅用于演示目的。 请勿用于恶意活动。
关键点:
$evil_metadata的值是攻击的核心。在实际攻击中,找到能产生相同 MD5 哈希值的$evil_metadata需要进行大量的计算。示例代码中的$evil_metadata只是一个占位符,并不能真正绕过签名验证。- 即使签名验证通过,也不意味着 Phar 档案是安全的。必须对档案的内容进行额外的验证,以确保其不包含恶意代码。
防御措施
为了防止 Phar 签名伪造攻击,可以采取以下防御措施:
-
使用更安全的签名算法: 避免使用 MD5、SHA1 等弱哈希算法,推荐使用 SHA256 或 OpenSSL 等更安全的签名算法。 OpenSSL 使用非对称加密,密钥仅用于签名,公钥用于验证,安全性更高。
-
限制 Metadata 块的内容: 尽量避免在 Metadata 块中存储用户可控的数据。如果必须存储,则应该对数据进行严格的验证和过滤。
-
对 Phar 档案的内容进行额外的验证: 即使签名验证成功,也不应该盲目信任 Phar 档案。应该对档案的内容进行额外的验证,例如检查文件类型、文件大小、文件内容等。 特别是,如果 Phar 档案包含 PHP 代码,应该使用沙箱环境执行,以防止恶意代码执行。
-
配置
phar.readonly设置: 在php.ini中设置phar.readonly = 1可以防止 PHP 脚本创建或修改 Phar 档案,从而降低攻击风险。 -
代码签名证书: 使用代码签名证书对 Phar 档案进行签名,可以提高签名的可信度。
-
白名单机制: 只允许加载来自可信来源的 Phar 档案。
-
更新 PHP 版本: 及时更新 PHP 版本,以修复已知的安全漏洞。
用表格来展示防御措施:
| 防御措施 | 描述 |
|---|---|
| 使用更安全的签名算法 | 避免 MD5/SHA1,使用 SHA256 或 OpenSSL。 |
| 限制 Metadata 块的内容 | 避免存储用户可控的数据,进行严格的验证和过滤。 |
| 对 Phar 档案的内容进行额外的验证 | 即使签名验证成功,也要检查文件类型、大小、内容。使用沙箱环境执行 PHP 代码。 |
配置 phar.readonly 设置 |
设置 phar.readonly = 1,防止 PHP 脚本创建或修改 Phar 档案。 |
| 代码签名证书 | 使用代码签名证书对 Phar 档案进行签名,提高可信度。 |
| 白名单机制 | 只允许加载来自可信来源的 Phar 档案。 |
| 更新 PHP 版本 | 及时更新 PHP 版本,修复安全漏洞。 |
实际应用中的防御示例
假设我们有一个应用程序,需要加载一个名为 config.phar 的 Phar 档案,其中包含应用程序的配置信息。我们可以采取以下防御措施:
-
使用 SHA256 签名算法: 在创建
config.phar时,使用 SHA256 签名算法。<?php $phar = new Phar('config.phar'); $phar->startBuffering(); $phar->addFromString('config.php', '<?php return ["db_host" => "localhost", "db_user" => "root", "db_pass" => "password"]; ?>'); $phar->setMetadata(['version' => '1.0']); $phar->setSignatureAlgorithm(Phar::SHA256); $phar->stopBuffering(); ?> -
限制 Metadata 块的内容: 只在 Metadata 块中存储版本信息,避免存储其他用户可控的数据。
-
对 Phar 档案的内容进行额外的验证: 在加载
config.phar后,验证config.php的文件类型和内容。<?php try { $phar = new Phar('config.phar'); // Verify the signature // Verify the file type $file_info = new SplFileInfo('phar://config.phar/config.php'); if ($file_info->getExtension() !== 'php') { throw new Exception('Invalid file type'); } // Verify the file content (e.g., check for specific keywords or patterns) $config = include 'phar://config.phar/config.php'; if (!is_array($config) || !isset($config['db_host'])) { throw new Exception('Invalid configuration file'); } // Use the configuration echo "Database host: " . $config['db_host'] . "n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "n"; } ?>在这个例子中,我们不仅验证了签名,还验证了
config.php的文件类型和内容,确保其是一个有效的配置文件。
关于代码的总结
总而言之,虽然 Phar 档案为 PHP 提供了一种便捷的文件打包和分发机制,但同时也引入了一些安全风险。通过理解 Phar 档案的结构、签名机制以及潜在的攻击方式,我们可以采取有效的防御措施来保护应用程序的安全。特别是,避免使用弱哈希算法,限制 Metadata 块的内容,并对 Phar 档案的内容进行额外的验证,是防止 Phar 签名伪造攻击的关键。