PHP Phar包的签名篡改:修改Stub与Manifest绕过哈希校验的技巧

PHP Phar 包签名篡改:修改 Stub 与 Manifest 绕过哈希校验的技巧

大家好,今天我们要深入探讨一个安全领域的话题:PHP Phar 包的签名篡改。Phar 包是 PHP 中一种便捷的打包和分发应用程序的方式,它允许将多个文件打包成一个单独的可执行文件。为了保证 Phar 包的完整性和安全性,Phar 提供了签名机制。然而,如果签名机制配置不当或存在漏洞,攻击者就有可能篡改 Phar 包的内容,并绕过签名校验,从而执行恶意代码。

本次讲座我们将重点关注如何通过修改 Phar 包的 Stub 和 Manifest 来绕过哈希校验。我们将深入理解 Phar 包的结构,分析签名校验的流程,并提供实际的代码示例来演示攻击过程,最后给出防御建议。

Phar 包结构剖析

理解 Phar 包的结构是进行任何攻击的前提。一个标准的 Phar 包主要由以下几个部分组成:

  1. Stub: 这是 Phar 包的引导代码,通常是一段 PHP 代码,用于在 Phar 包被执行时加载和初始化 Phar 环境。Stub 必须以 __HALT_COMPILER(); 结尾,表明 Stub 的结束和 Phar 数据的开始。
  2. Manifest: Manifest 包含了 Phar 包中所有文件的元数据,例如文件名、大小、创建时间、压缩方式以及每个文件的内容哈希值。Manifest 本身也会被签名。
  3. File Contents: 这是 Phar 包中实际的文件内容,例如 PHP 脚本、图片、样式表等。
  4. 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 包时,会进行如下的签名校验流程:

  1. 读取 Stub: 首先,PHP 读取 Phar 包的 Stub 部分,并执行其中的 PHP 代码。
  2. 定位 Manifest: PHP 解析 Stub 结尾的 __HALT_COMPILER();,并从其后开始读取 Manifest。
  3. 读取 Manifest: PHP 读取 Manifest,并解析其中的文件元数据,包括文件名、大小和哈希值。
  4. 计算哈希值: PHP 根据 Manifest 中记录的算法,重新计算每个文件的哈希值。
  5. 验证哈希值: 将重新计算的哈希值与 Manifest 中存储的哈希值进行比较,如果所有哈希值都匹配,则表示文件内容未被篡改。
  6. 验证签名: 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.phpconfig.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.readonlyphar.require_hash 等安全配置,防止 Phar 包被篡改。
  • 代码审查: 对 Phar 包的代码进行定期审查,发现潜在的安全漏洞。
  • 最小权限原则: 运行 Phar 包的用户应该只具有执行所需的最少权限。

通过采取这些防御措施,我们可以大大提高 Phar 包的安全性,防止恶意攻击。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注