☰
SQL注入绕过实战:从联合查询被过滤到预处理语句与换表读取
2026/9/28 5:36:14 网站建设 项目流程

没记错的话,这道题在攻防世界的Web区里算是一道很经典的"看着简单、入口明确、但过滤能把人卡死"的题目。页面干净得离谱:一个输入框,参数名叫inject,摆明了就是让你注。但真上手以后你会发现,前面几步走得很顺,等你习惯性地把union select打上去,一整条路直接给你焊死。这篇writeup不打算只给一串payload,我会把每一步的判断依据、中途踩过的坑、以及为什么最终能绕过去的原理都摊开讲清楚。适合正在刷Web题、或者想把SQL注入绕过思路彻底理一遍的选手参考。

1. 开场三连测:确认注入类型和注入点

1.1 先看页面和参数,再做黑盒试探

打开靶机以后,页面就是一个输入框,提交之后URL里会出现?inject=xxx。没有源码提示,也没有任何前端注释偷藏线索,所以这就是一个标准的黑盒注入题。

我的习惯是拿到这种单参数页面以后,先做三个最基础的请求,把注入类型摸清楚:

?inject=1 ?inject=1' ?inject=1' --+

三次请求的结果非常典型:

  • 1:页面正常返回一行数据,看起来像某个ID对应的记录;
  • 1':页面直接报SQL语法错误,说明单引号被拼进SQL并且打破了原本的语句结构;
  • 1' --+:恢复正常,说明用了单引号闭合,注释符把后面多余的内容吃掉了。

到这里基本可以断定:后端SQL是字符串拼接,我的输入被直接放进了一个带引号的查询里。大概率是类似这种结构:

select id,data from words where id='$inject'

这个猜测后面非常重要。因为整个题的第二条路——换表名让后台自己查——就是建立在"后台查询逻辑写死了words表"这个前提上的,先记着。

1.2 测列数:order by 是唯一没有第一时间被拦的东西

确定是字符型注入之后,下一步自然是猜列数。用order by逐项试:

?inject=1' order by 1 --+ ?inject=1' order by 2 --+ ?inject=1' order by 3 --+

结果:order by 1和order by 2都正常,order by 3直接报错。说明查询结果只有两列。这个信息也很有用,因为两列结构通常对应id, data或者id, flag之类的简单表。

到这里为止,一切看起来都是标准流程。我也很自然地准备上联合查询了,然后就被教育了。

2. 联合查询当场报废:一条 preg_match 挡住半张地图

2.1 union select 第一次被拦时的完整报错

当我往注入点里提交:

?inject=1' union select 1,2 --+

页面返回的不是SQL错误,而是出现了类似:

return preg_match("/select|union|from|where|order|by|and|or/i", $inject);

这条提示等于是把过滤规则直接甩你脸上了:后端用preg_match做了关键词黑名单,select、union、from、where这些查询类关键词全被拉黑,而且带/i修饰符,大小写绕过直接作废。

第一次看到这种提示反而安心,因为至少知道过滤在哪、过滤了什么。接下来要做的就是拿不同姿势去试边界。

2.2 常规绕过轮番试一遍:全灭

我先把平时常用的几种绕过方式都丢进去试了一圈,结果如下:

尝试方式示例结果
大小写混写UnIoN SeLeCt被拦,因为/i忽略大小写
内联注释/*!50000union*/ select被拦,注释没法绕过正则关键词匹配
空格替换为tabunion%09select被拦,正则不看空白字符
注释替代空格union/**/select被拦,union和select本身还在
编码绕过%55nion无效,服务端不一定解码两次

结论很明确:这题的过滤不是针对某个特定payload,而是把union和select这两个查询链条上的核心词全毙了。联合查询这条路在这道题里不是难走,是直接锁死。

2.3 顺手把没被过滤的词也摸一遍

既然有黑名单,那就顺便把所有可能用到的SQL关键词都过了一遍。排查结果大概是:

  • select、union、from、where、order、by、and、or:全部被过滤
  • show、set、prepare、execute、rename、alter、handler、update:没被过滤

这个差异是决定性的。它意味着联合查询和直接select被堵死,但管理类、预处理类、存储引擎类的SQL语句仍然可以自由使用。这为后面的堆叠注入和预处理语句绕过提供了完整的前提。

3. 堆叠注入:分号打开的另一个世界

3.1 为什么联合查询死了,堆叠注入还能活

很多Web安全入门教程会把"注入"局限在"在一条查询里改变它的逻辑"——比如用union select追加查询,用or 1=1改变判断条件。这种方式无论怎么变形,最终都受限于当前这一条SQL语句的语法结构。

堆叠注入是另一条路:它利用的是数据库协议允许一次提交多条语句的特性。你输入1';show tables;--+,后端执行的时候就变成了:

select id,data from words where id='1';show tables;--+'

分号把前一条查询结束掉,show tables成了一整条全新的独立语句。只要后端没有限制连接会话的"多语句执行"权限,堆叠注入就能绕开所有基于单查询结构的过滤。

打个不严谨的比方:联合查询相当于你在被审查的申请单里夹带私货,关键词被拦就夹不了;堆叠注入则是把申请单撕了,直接另写一张纸递给窗口。preg_match过滤的是"纸上的关键词",它管不到MySQL解析器后面执行什么。

3.2 用 show 拿库名和表名

先确认堆叠注入可用:

?inject=1';show databases;--+

正常返回,能看到当前库supersqli以及其他系统库。这就说明MySQL执行了分号后面的语句。继续:

?inject=1';show tables;--+

结果里出现两个表:

words 191981093111451514

看到这张表名的时候,我的想法是:名字都不装了。一个是标准的words,一个是纯数字乱命名,flag大概率在191981093111451514这张表里。

3.3 新问题:堆叠只能执行,不能直接读

知道表名之后,第一反应自然是:

?inject=1';select * from `191981093111451514`;--+

结果仍然被preg_match拦下。注意这里是个容易迷惑人的点:堆叠注入能执行新语句,但新语句的内容照样要被过滤器检查。你只是绕过了"单查询结构"的限制,并没有绕过"关键词黑名单"的限制。

所以这题的下一个核心问题变成了:在不能直接写出 select 关键词的情况下,如何让MySQL执行一条查询。这时浮现出两条路,一条是预处理语句,一条是让后台替我们查。

4. 预处理语句绕过:把select藏进字符串再让它复活

4.1 PREPARE 怎么就成了绕过利器

MySQL的预处理语句语法是这样:

PREPARE stmt_name FROM '任意SQL字符串'; EXECUTE stmt_name;

PREPARE接收的是一个字符串,这个字符串可以由变量拼接而来,也可以直接写字面量。关键在于:PREPARE本身不是查询,它只是把字符串编译成SQL;真正的执行发生在EXECUTE。过滤器的正则扫描的是你发送的整个HTTP请求文本,它会看到PREPARE、EXECUTE、CONCAT这些词,但看不到一个完整的select查询语句。

比如这样的payload:

?inject=1';PREPARE s from concat('select * from `191981093111451514`');EXECUTE s;--+

在正则扫描时,select并没有作为一个SQL关键词出现在注入点里,它只是concat()函数的一个字符串参数。MySQL拿到之后,由预处理机制在服务器内部解析并执行,过滤器管不到这一步。

4.2 最终的干净payload与执行结果

我最后打通的payload长这样:

?inject=1';PREPARE s from concat('select * from `191981093111451514`');EXECUTE s;--+

提交之后,页面把191981093111451514表的内容完整显示出来,flag就在其中。

几个细节值得记住:

  • 纯数字表名必须用反引号包起来。如果不加反引号,MySQL会把191981093111451514当成一个数字字面量而不是表名,直接报语法错误。
  • concat()在这里不是必须的,直接用PREPARE s from 'select * from ...'也能过,因为字符串字面量里的内容不触发正则。用concat()的主要好处是后续可以接变量,灵活度更高。
  • 注释符必须生效。--+在URL里实际是把加号解析成空格,注释符能吞掉后面遗留的单引号和SQL片段,保证EXECUTE之后没有多余的尾巴。

4.3 另一种预处理写法:十六进制藏select

如果担心字符串里出现select字面量也会被某个更严格的正则抓走,可以用十六进制编码绕:

?inject=1';set @a=0x73656c656374202a2066726f6d206031393139383130393331313134353135313460;PREPARE s from @a;EXECUTE s;--+

0x73656c656374...是select * from ...的十六进制表示。这样整个请求文本里连select这四个连续字母都不存在了,属于更彻底的绕过方式。当时我在本地尝试过这种写法,直接可用,后来在一些其他注入题里也经常靠这一招救命。

4.4 这个解法成立的前提

堆叠注入 + 预处理能用,依赖两个条件:

  1. 后端连接数据库的账号对当前会话开放了多语句执行能力;
  2. MySQL版本支持PREPARE/EXECUTE,这个语法从MySQL 5.0往后都支持,老环境也能用。

如果第一个条件不满足(比如PHP的mysqli->query()默认就不允许多语句执行,需要multi_query才行),那堆叠注入直接失效,预处理也就无从而起。这题的环境显然是允许的。

5. 不用select也能读表:换表名让后台替我干活

5.1 思路:后台那句写死的查询,才是最好的工具

在做题过程中我想到了第二条路,而且这条路的思路很值得玩味。还记得开场判断的后台SQL吗?

select id,data from words where id='$inject'

后台是直接把表名写在代码里的。那么问题来了:我们能不能不查191981093111451514,而是把191981093111451514变成words?这样后台那句写死的select ... from words就在替我们干活,根本不需要注入select关键词了。

这个思路的本质是:注入点不只是用来拼接SQL语句的,它还能用来操作数据库对象本身。当查询关键词被过滤干净的时候,对象级别的操作往往能绕出一个新天地。

5.2 三步交换表名

实际操作需要三步:

?inject=1';rename table words to word;rename table `191981093111451514` to words;--+

先执行:

?inject=1';alter table words change id id varchar(100) character set utf8;--+

再提交一个恒真条件:

?inject=1'or '1'='1

最终效果:

select id,data from words where id='1' or '1'='1'

where条件恒真,后台会把words表的第一行记录返回,而这时的words实际上就是原来的191981093111451514表,flag直接输出。

5.3 为什么要执行alter table

很多人做到rename那两步就去提交了,结果报字段不认识。原因在于新表191981093111451514的字段名和原words表不一样。原后台查询写的是:

select id,data from words

如果新words表里没有叫id和data的字段,查询执行就会出错。所以alter table ... change id id varchar(100)的核心目的是保证换表之后查询仍能成立,把字段名和类型改成后台需要的形状。这一步不是可有可无的,是整套逻辑的闭合点。

5.4 两种解法的定位差异

到这里这题其实有两条可达路径了,我整理了一下它们的定位:

对比维度预处理语句解法换表名解法
核心操作PREPARE + EXECUTERENAME + ALTER
依赖条件堆叠注入可用堆叠注入可用 + 后台查询表名可预测
过滤绕过点关键词藏在字符串里根本不出现查询关键词
通用性高,换一道题也能套低,依赖具体后台SQL结构
踩坑点纯数字表名要反引号换表后字段名必须对齐

个人做这类题的经验是:两条路都走一遍很有价值。预处理解法更通用,换表名解法更考验对整个查询逻辑的掌控力。实际出题人想考的往往是后者,因为它的绕法更贴近真实数据库的攻击面。

6. 从这道题里提炼的注入绕过思路

6.1 过滤探测别只盯关键词,要盯"语句类型"

这道题最值得复盘的一点是:preg_match过滤了select/union/from/where,看起来封锁面很全,但show、prepare、rename这些非查询类关键词全部漏掉了。这提醒我,做过滤绕过时不要只在关键词列表里打转,要往语法树上去想。哪些语句类型能读取数据?select、show、handler、prepare+execute、load_file……当一类被锁死的时候,剩下好几类可能还活着。

MySQL的HANDLER语句也是一条没被过滤的备选:

HANDLER `191981093111451514` OPEN; HANDLER `191981093111451514` READ FIRST;

它以存储引擎接口的方式直接读取表数据,和select完全无关。我在本地环境试过这个思路,部分MySQL版本可用。如果在赛场上PREPARE/EXECUTE的路也被断了,HANDLER值得一试。

6.2 注意反引号这个"逃生通道"

这道题里纯数字表名差点坑到我。表名是191981093111451514,如果直接写进SQL,MySQL会把它当成数字字面量,什么也查不出来。必须加反引号才能被识别为标识符。这个细节在平时写项目SQL时几乎不会遇到,但在CTF的拼凑式payload里极其常见。凡是目标表名带特殊字符、纯数字、或者和函数名重名,都要第一时间想到用反引号包裹。

6.3 preg_match的"看得见"与"看不见"

这题的过滤器是直接返回了报错信息,所以探测成本很低。遇到不返回过滤信息的题,判断过滤规则就需要靠盲试和响应差异对比了,难度上一个台阶。但不管过滤器藏不藏,有一个规律是不变的:它一定基于文本扫描。那么绕过思路就可以统一归结为——让目标关键词不以"明文关键词"的形式出现在请求文本里。常见手段有:十六进制编码、字符串拼接、变量存储、注释符切割。掌握这一个原则,能覆盖大多数关键词过滤场景。

7. 复现建议和几个容易踩的坑

7.1 环境准备

这题可以直接在攻防世界的在线环境里复现,不需要本地搭建。如果要在本地自建一个模拟环境,建议用PHP 5.x + MySQL 5.x 的组合,因为新版PHP和MySQL在多语句执行和报错细节上会有差异。我在本地复现时用的就是经典的 PHP + MySQL 组合,payload基本可以直接迁移。

7.2 提交payload时的细节

用浏览器直接在URL里提交时,注意几个细节:

  1. --+里的+在URL中是空格的意思。如果直接用Burp或Python请求,可以写成--,也就是两个减号加一个空格,效果相同。
  2. 如果提交后页面空白或报错,优先检查表名的反引号有没有遗漏。
  3. 堆叠注入中间的分号必须和后面的语句连在一段URL里,不要被URL编码弄丢。

7.3 实际做题时的时间分配建议

我在复现时先走的PREPARE/EXECUTE,一条payload三分钟解决战斗。后来为了验证思路才去试了rename表名路线。建议做题时先上堆叠注入探测,确认可用后直接预处理语句拿flag,时间充裕再试其他解法。因为换表名那套流程稍长,步骤一多,出错率也高,赛场上时间是硬成本。

另外多说一句:这类Web题的Writeup,重点看的不是那串payload本身,而是它背后的决策树。为什么联合查询失败后马上转堆叠?为什么堆叠可行但select还被拦?为什么最终落在PREPARE?每一步都是对SQL执行机制理解的产物。把这条决策链吃透,比背一百个payload都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询