PHP 代码审计实战:常见漏洞原理与绕过技巧详解

程一诺
程一诺
发布时间:2026-10-16| 7人浏览过
原创

本文通过六个经典的 PHP CTF 示例,详细解析了代码审计中常见的漏洞原理与利用方法。内容包括利用 extract 进行变量覆盖获取 Flag,使用 %00 空字节截断绕过 is_numeric 检查,还原被压缩序列化的 Token 实现反序列化绕过,以及利用 WITH ROLLUP 特性绕过关键词限制的 SQL 注入。此外,还演示了如何利用 PHP 弱类型特性,通过传入数组使 strcmp 和 sha1 函数返回 null 或 false 来绕过严格比较。这些案例为初学者提供了从原理到实操的完整路径。

变量覆盖漏洞的利用

在 PHP 代码审计中,extract() 函数是变量覆盖漏洞的常见来源。该函数将数组中的键值对导入到当前符号表中。如果攻击者能够控制输入参数,即可覆盖原有变量。

考虑以下代码逻辑:

<?php
$flag = 'xxx';
extract($_GET);
if(isset($shiyan)) {
    $content = trim(file_get_contents($flag));
    if($shiyan == $content) {
        echo 'ctf{xxx}';
    } else {
        echo 'Oh.no';
    }
}
?>

程序首先初始化 $flag 为字符串 'xxx',随后调用 extract($_GET) 将 URL 参数转为变量。接着判断 $shiyan 是否存在,若存在则读取 $flag 指向的文件内容赋值给 $content,最后比对 $shiyan 与 $content。由于 'xxx' 并非有效文件路径,file_get_contents 将返回 false 或空字符串(经 trim 处理后为空)。因此,只需构造 URL 参数使 $shiyan 也为空,即可满足相等条件。

操作如下:

  • 准备条件:访问目标页面。
  • 构造 Payload:构造 URL ?shiyan=&flag=1。其中 ?shiyan= 确保 $shiyan 值为空;&flag=1 是为了触发 extract,虽然本题中只要覆盖 $shiyan 即可。
  • 完成状态:浏览器返回 ctf{xxx}。

绕过空白字符与数字校验

此题旨在考察对输入过滤函数的深入理解。代码要求用户输入一个“回文数”,但进行了多层校验:

  1. is_numeric($_REQUEST['number']) 必须为 false(即不能是纯数字)。
  2. $req['number'] != strval(intval($req['number'])) 必须为 false(即必须等于其整数值,意味着不能有浮点小数部分)。
  3. $value1 == $value2,即原数反转后整数值相等(回文)。
  4. is_palindrome_number($req["number"]) 必须返回 false,从而执行 $info=$flag。

针对第一个条件,直接输入数字会被拦截。可以使用 %00(空字节)或 +号配合空格 等技巧绕过 is_numeric 的检测,因为某些旧版 PHP 环境下 is_numeric 对包含 %00 的字符串处理存在差异,或者利用 + 号在算术环境下的特殊性。但在本题特定的 PHP 版本逻辑下,直接输入非数字会导致第二个条件失败。这里的关键在于利用 intval 和字符串处理的边界情况。

突破口:
最终的条件 is_palindrome_number 是一个自定义函数,它通过逐字符比对来判断对称性。如果传入的参数无法被正确转换为字符串,或者包含特殊字符导致遍历异常,函数可能返回 false。结合前序条件,我们可以尝试构造一个在数值上被视为回文,但在字符串形式下不对称的Payload。

一种常见的绕过方式是利用 is_numeric 的宽松性与后续严格字符串处理的矛盾。例如,输入 0xABCDEF 在某些环境中被视为数字,但在字符串比较中不同。然而,最稳健的解法通常涉及 %00 截断或利用 NaN (Not a Number) 的特性。在本例中,参考常见 CTF 题库逻辑,往往需要构造类似 %00+454 这样的结构,利用 + 号在 URL 解码或解析时的行为,以及 intVal 对前缀的处理来实现绕过。

当满足上述整数层面的回文判断后,关键在于如何让 is_palindrome_number 失败。如果传入的数据包含 %00,在后续的字符串遍历中可能会产生截断或非预期行为,导致左右不对称的判断结果与数值回文不一致,从而触发 return false,输出 Flag。

多重加密下的反序列化绕过

此类题目通常涉及 unserialize(gzuncompress(base64_decode(...))) 的多重编码和解码流程。目标是逆向工程,构造出符合业务逻辑的原始数据。

代码逻辑摘要:

<?php
class db {
    public $where;
    function __wakeup() {
        if(!empty($this->where)) {
            $this->select($this->where);
        }
    }
    // ...
}
// ... 接收 token 并解密
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
$db = new db();
// 使用 $login['user'] 查询数据库
if($login['user'] === 'ichunqiu') { echo $flag; }
// ...
?>

分析:
1. $login 是从 Token 反序列化得到的数组。
2. 只有当 $login['user'] === 'ichunqiu' 时才会输出 Flag。
3. db 类的 __wakeup 会在反序列化时被自动调用。如果 $this->where 不为空,会执行 SQL 查询。但这部分主要作为背景,核心逻辑在于直接满足 $login['user'] 的检查。

操作步骤:
我们需要构造一个数组,其 user 字段为 'ichunqiu',然后依次进行序列化、压缩、Base64 编码,得到 token 值提交。

本地生成 Payload 的代码如下:

<?php
$arr = array('user' => 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
?>

运行后得到的 token 值即为 eJxLtDK0qs60MrBOAuJaAB5uBBQ=(具体值因压缩算法细节可能微调,但原理一致)。将此 Token 作为 GET 参数 ?token=eJxLt... 提交,程序将成功验证用户并输出 Flag。

WITH ROLLUP SQL 注入绕过

当传统的 SQL 注入关键字(如 union, select, from)被 preg_match 过滤时,可以利用 MySQL 的聚合函数特性进行绕过。

代码逻辑:

$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql);
$key = mysql_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) { print "CTF{...}"; }

我们需要使 $key['pwd'] 的值等于我们提交的 $_POST['pwd'] 值。通常 admin 用户的密码是未知的。但如果能让查询结果只返回一行,且这一行的 pwd 字段为 null,我们就可以提交空的 pwd 来匹配。

利用 WITH ROLLUP:
GROUP BY ... WITH ROLLUP 会在分组结果后追加一个汇总行。如果查询条件唯一且使用 GROUP BY,MySQL 可能会在特定情况下生成包含 NULL 值的行。结合 ORDER BY 和 LIMIT,我们可以精确控制返回哪一行。

操作步骤:
1. 尝试使用 GROUP BY pwd WITH ROLLUP 改变查询结果的行数结构。
2. 使用 ORDER BY pwd ASC/DESC 确定 NULL 值的位置。
3. 使用 LIMIT 1 OFFSET 1 跳过第一行,获取第二行(通常为 NULL 行)。

注入语句如下:

' GROUP BY pwd WITH ROLLUP LIMIT 1 OFFSET 1-- -

提交用户名:' GROUP BY pwd WITH ROLLUP LIMIT 1 OFFSET 1-- -
提交密码:(留空)

此时,查询结果集中的一行其 pwd 字段被赋值为 NULL,与我们提交的空密码匹配,从而成功获取 Flag。这种技术常用于绕过简单的黑名单过滤。

strcmp 与 sha1 的类型绕过

这两个案例展示了 PHP 在处理不同类型参数传递给内置哈希或比较函数时的历史行为或特定陷阱。

strcmp 数组绕过

if (strcmp($_GET['a'], $flag) == 0) die('Flag: '.$flag);

标准情况下,strcmp 用于比较两个字符串。在较新的 PHP 版本中,传入数组会引发 Warning 并返回 1。但在某些老旧环境或未开启严格报错的模式下,或者利用 PHP 的弱类型特性,如果 strcmp 遇到非字符串类型(如数组),其行为可能导致不可预期的结果。更常见的考点是,如果 == 比较的一侧是 0,而另一侧是某种能转换为 0 的值,或者在特定版本中 strcmp 对数组返回 null,而 null == 0 为 true。**注意:** 现代 PHP (7.0+) 已修复此问题,数组传入 strcmp 会报错。此题多见于老版本的 CTF 场景,解题思路为构造 ?a[]=1,利用 null == 0 的特性绕过(需确认环境版本)。

sha1 数组绕过

if ($_GET['name'] == $_GET['password'])
    echo 'Your password can not be your name!';
else if (sha1($_GET['name']) === sha1($_GET['password']))
    die('Flag: '.$flag);

条件一要求 name 不等于 password。
条件二要求两者的 SHA1 哈希值完全相同(类型和内容)。

绕过逻辑:
如果传入的参数是数组,sha1() 函数将无法处理字符串,通常会返回 false(在老版本或特定配置下)或产生警告。如果使用 === 比较,false === false 为真。同时,两个不同的数组变量 $a = [1] 和 $b = [2] 在 == 比较中是不相等的。因此,构造 ?name[]=1&password[]=2,可以满足 name != password,且两者的 sha1 均无法计算(返回 false 或等效的非字符串错误值),从而触发 die。

Payload:?name[]=1&password[]=2

以上就是 PHP 代码审计基础漏洞详解的详细内容,更多关于 PHP 安全与代码审计的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。