PHP 按位取反绕过字符过滤实现 RCE
记录 CTF 中利用 PHP 字符串按位取反还原函数名和命令、绕过 ASCII 字母过滤并完成动态函数调用的原理与版本限制。
一、题目背景
这类 CTF 题通常会把用户提交的参数交给 eval() 执行,同时使用正则表达式拦截 ASCII 字母。例如:
preg_match('/[a-zA-Z]/i', $w1lm);
因此无法直接提交 system("cat /f*");。解题思路是先把目标字符串逐字节取反,再通过 URL 百分号编码传输;服务端取得参数后,再利用 PHP 的 ~ 运算符把这些字节还原成原字符串。
本文仅记录授权 CTF/靶场环境中的解题方法。实际是否可用,仍取决于完整过滤规则、参数获取方式、PHP 版本以及
eval()的调用位置。
二、绕过原理
1. 字符串按位取反
当 ~ 的操作数是字符串时,PHP 会对字符串中的每个字节执行按位取反,结果仍是字符串。
例如,字母 s 的 ASCII 值是 0x73:
0x73 取反后为 0x8C
0x8C 再次取反后还原为 0x73
所以对一个字符串连续取反两次,可以恢复原内容:
~~"system" === "system"
2. URL 编码发生在传输层
取反后的字节通常不可见,也不适合直接放进 URL,因此要写成 %8C、%86 这样的百分号编码。
这里真正绕过检查的不是地址栏中“只有十六进制字符”,而是处理顺序:PHP 读取 $_GET 时会先对参数值进行 URL 解码。若过滤器检查的是解码后的参数,它看到的是 0x8C、0x86 等高位字节,而不是原来的 ASCII 字母。
处理流程可以概括为:
URL 中的 %XX → PHP 解码为原始字节 → 通过字母过滤 → ~ 再次取反 → 还原目标字符串
三、Payload 构造
目标是在题目环境中还原并调用:
system("cat /f*");
1. 生成函数名
对 system 的每个字节取反:
system → %8C%86%8C%8B%9A%92
服务端解码后再次执行 ~,便会还原出字符串 system。
2. 生成命令参数
对 cat /f* 进行相同处理:
cat /f* → %9C%9E%8B%DF%D0%99%D5
其中,空格取反后是 0xDF,斜杠取反后是 0xD0。
3. 组合动态函数调用
根据截图中的参数名,最终形式为:
?w1lm=(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%D5);
URL 解码并执行取反后,等价于:
("system")("cat /f*");
第一个括号表达式得到函数名,第二个括号表达式作为参数,从而形成 PHP 的动态函数调用。
末尾的分号不能遗漏。eval() 接收的内容必须是合法 PHP 代码,函数调用语句也需要使用分号结束,否则会产生语法错误。
四、容易写错或忽略的地方
1. 参数名应保持一致
原始笔记中同时出现了 $w1lm 和 ?wllm=。从截图看,参数名应为 w1lm,其中第二个字符是数字 1,因此本文统一写成:
?w1lm=
实际使用时仍应以题目源码中的参数名为准。
2. 不能笼统地说所有特殊符号都被过滤
这个 Payload 本身需要 ~、(、) 和 ;。如果这些字符真的全部被过滤,当前构造就无法通过。
URL 中的 ? 只是路径与查询字符串之间的分隔符,并不是 w1lm 参数值的一部分。是否被参数过滤器禁止,应与参数值中的字符分开讨论。
3. /i 在该正则中是多余的
[a-zA-Z] 已经同时包含大小写字母,再加不区分大小写修饰符 i 不会扩大匹配范围。它不影响题目逻辑,但可以简化为:
preg_match('/[a-zA-Z]/', $w1lm);
4. 不加引号的写法存在 PHP 版本限制
截图里的取反对象没有加引号。其高位字节可以被旧版 PHP 当作未定义常量名的一部分,而 PHP 8.0 以前会把未定义常量兼容性地解释为同名字符串;PHP 7.2 起会同时产生警告。
PHP 8.0 起,访问未定义常量会直接抛出 Error,意外的源码字节也可能导致解析错误。因此,这个 Payload 不能视为适用于所有 PHP 版本,复现前应先确认题目环境。
5. 编码必须逐字节计算
这里处理的是原始字节,不是把 %8C 这些可见字符再次取反。若客户端、代理或应用发生二次 URL 解码,服务端最终收到的内容也可能与预期不同。
五、防御建议
- 不要把用户输入传入
eval();这是问题的根源。 - 不要依靠字符黑名单判断一段 PHP 代码是否安全,编码、运算符和解析差异都可能产生绕过。
- 若业务需要按名称选择功能,应使用固定白名单,把允许的输入映射到预先定义的函数。
- 执行系统命令时应避免拼接用户输入,并限制运行账户权限与可访问文件范围。
六、总结
这道题的核心链路是:将函数名和命令逐字节取反,用 URL 编码传输高位字节,服务端解码后通过 ASCII 字母过滤,再由 ~ 还原字符串,最后借助动态函数调用触发命令执行。
两段编码本身计算正确,但原笔记需要修正参数名、过滤范围和 URL 解码顺序的表述,并补充 PHP 版本限制。判断这类 Payload 时,不能只看最终字符串,还要同时确认输入解码、正则过滤、PHP 解析和 eval() 执行的先后顺序。