尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

SQL二次注入原理与防御:藏在数据库里的潜伏攻击

发布时间:2026/9/25 6:12:16

资讯中心
01
ARTICLE

SQL二次注入原理与防御:藏在数据库里的潜伏攻击

SQL二次注入原理与防御:藏在数据库里的潜伏攻击
网站开发这行做得久了你会发现一个挺扎心的现实很多人在面试时能把SQL注入的十大手法背得滚瓜烂熟但一回到真实业务代码里连二次注入长什么样都认不出来。它不像union select那样一眼就能看出payload痕迹也不像布尔盲注那样需要疯狂跑脚本而是安安静静地把攻击载荷存进数据库等到某一天另一个功能模块把它读出来、拼进SQL语句才瞬间引爆。说白了二次注入就是SQL注入里的潜伏者也是我在代码审计时最头疼、也最觉得有意思的一种漏洞形态。这篇文章我想把它的原理、利用思路和防御手段整个拆开讲一遍尽量把那些纸上谈兵的东西落到具体场景里适合正在学Web安全、准备攻防演练或者单纯想把自己代码写得更安全的开发者。1. 二次注入的成因拆解为什么第一次注入没反应先解释一下什么是二次注入。常规SQL注入是一次成型的你提交的payload直接进入SQL语句立刻触发立刻生效。二次注入则要绕一个弯它的完整链路至少包含两次SQL操作第一次操作攻击者的恶意输入被写入数据库但这一次写入时参数已经被转义、过滤或做类型强转导致恶意代码无法直接执行第二次操作某个业务功能把数据库里**已经存储的脏数据**取出来在完全没有继续防护的情况下拼接进了新的SQL语句此时恶意代码才真正被执行。你可以把它理解成埋雷和踩雷的关系。一次注入是当场点炮二次注入是先把火药藏进仓库等哪天有人拿它搓了个鞭炮。1.1 为什么第一次写入拿不下数据库我在很多次项目复盘里被人问过同一个问题既然输入能写进数据库为什么不在写的时候就注入成功这就要回到数据库交互层最常见的两种假防护上转义函数和类型限制。以PHP时代的mysql_real_escape_string()为例它会把单引号、双引号、反斜杠等特殊字符转义比如变成。如果注册功能接收的是用户名和密码用户名字段被转义后拼进INSERT语句$username mysql_real_escape_string($_POST[username]); $sql INSERT INTO users (username, password) VALUES ($username, $password);假设攻击者提交的用户名是admin--转义之后在SQL里实际长这样INSERT INTO users (username, password) VALUES (admin\-- , pass)这里的\在MySQL里会被解析为一个字面意义上的单引号所以它并不会闭合SQL中的字符串整条INSERT语句结构没有变化数据库安安稳稳地把admin--存了进去。这就是第一次注入没反应的根本原因——特殊字符并没有消失只是被降级成了普通数据。还有一种情况是后端用intval()或强类型转化方式处理数字参数加载参数进SQL前就把字母和符号拍死了。但二次注入玩的花样就在于存储时是数据读取后拼进某个功能点它又变成了SQL代码的一部分。1.2 关键差异注入载荷需要在第二次拼接中复活要理解二次注入的精髓必须抓住一个点恶意输入在数据库里保存的是原始字符而不是转义后的字符。继续用上面的例子。admin--被转义后往数据库里实际写入的内容其实是去掉转义反斜杠后的原始字符串也就是带单引号的admin--。很多开发者误以为存进去的一定是安全的实际上转义只发生在SQL语句组装那一刻数据库表里存的是用户提交的原始文本。等第二个功能上线比如修改个人资料后端代码可能是这样写的$username $_GET[username]; // 通常从会话或URL参数中取值 $newemail $_POST[email]; $sql UPDATE users SET email $newemail WHERE username $username;这里如果开发没有对从数据库里读出来的$username再做转义而是直接拼进UPDATE语句那admin--中的单引号就成功闭合了字符串而后面的--会注释掉WHERE子句剩余部分。整个语句变成UPDATE users SET email attackerevil.com WHERE username admin-- 这意味着所有username等于admin的记录都会被更新攻击者可能直接改掉管理员邮箱然后走忘记密码流程重置管理员账号。这就是一种教科书级的二次注入利用。有一个经验值得记住凡是从数据库取出来的数据默认都不可信。这是二次注入防御的真正起点。2. 攻击链路推演从注册到提权的完整利用纸上谈兵讲完原理还是抽象。我干脆搭一个常见的业务场景把二次注入一次完整的攻击链走一遍。假设现在有一个简单的用户系统注册、改资料、找回密码三个功能后端用PHPMySQL没上任何框架纯拼SQL。2.1 场景搭建与表结构用户表结构如下CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL, email VARCHAR(100) NOT NULL, is_admin TINYINT DEFAULT 0 );注册页面的核心代码$username mysql_real_escape_string($_POST[username]); $password md5($_POST[password]); $email mysql_real_escape_string($_POST[email]); $sql INSERT INTO users (username, password, email) VALUES ($username, $password, $email); mysql_query($sql);修改邮箱功能的核心代码$id intval($_GET[id]); $newemail mysql_real_escape_string($_POST[email]); $sql UPDATE users SET email $newemail WHERE id $id; mysql_query($sql);这里的id参数经过intval显然不能直接注入。但注意修改邮箱这个过程通常是先查询用户把当前信息展示在页面上再提交更新。而这个查询用户的语句很可能用了用户名当条件$user $_SESSION[username]; $sql SELECT * FROM users WHERE username $user AND is_admin 0;如果$_SESSION[username]就直接取自数据库而且登录时没再处理这里就存在二次拼接的机会。攻击者只需要把注册时的用户名从一开始就设计成能够反杀的样式。另一个常见触发点是修改昵称接口。注册时写入特殊昵称后续任何显示最近在线用户列表之类的功能都可能把昵称拼进查询语句里。这类漏洞的可怕之处在于所有功能模块都有可能是那颗雷的引爆器。2.2 实战形式的攻击步骤我们让时间线一步步走一遍第一步注册一个恶意用户名攻击者提交的用户名设置为test AND 11 UNION SELECT 1,2,3,4--如果后端只做了转义INSERT语句会被安全执行数据库里存储的username字段内容依旧是这串原始字符。登录时后端可能会重新把用户名拼进SQL查询做认证也可能用预编译去查所以第一步一般不会出问题。第二步触发涉及该用户名的查询/更新接口攻击者访问修改邮件、查看好友列表、兑换积分或者其他涉及查询当前用户信息的页面。比如有些站点的逻辑是$username $_COOKIE[username]; $sql SELECT is_admin FROM users WHERE username .$username.;我见过不少系统为了实现记住我功能直接把用户名放进Cookie又没有在取出后二次校验这就会导致原本存在数据库里的恶意用户名以原样形式出现在SQL中。库里的用户名是test AND 11 UNION SELECT...拼进SQL后变成SELECT is_admin FROM users WHERE username test AND 11 UNION SELECT 1-- 攻击者可以通过控制后面UNION SELECT返回的数据来操纵判断逻辑或者结合报错回显逐步推断库里的其他数据。第三步利用UPDATE型二次注入修改身份如果某个功能存在UPDATE语句拼接伤害更加直接。例如$sql UPDATE users SET email$email WHERE username$username;攻击者构造用户名admin--注册一个账号再访问任意会执行这条UPDATE的功能就能造成全表更新。更狠一点有些系统会把用户的积分、余额等字段也拼进UPDATE语句中配合数据库的报错信息或时间盲注慢慢拖出所有数据。2.3 常见误区:二次注入的载荷并不是只能放在用户名里不少人一提到二次注入就只想到用户名字段。实战中你会发现几乎所有能被存进数据库、之后又可能被拼进SQL的用户可控字段都可以成为注入点包括用户昵称、头像URL、自我介绍收货地址里的省市区字段搜索历史、评论内容、评论区昵称客户填写的备注、发票抬头上传文件的原始文件名有些系统会把文件名存库之后按文件名做查询或删除这些字段的特点是需要长时间存储、可能被多个模块复用。一个放在收货人手机号里的payload如果后续的订单导出功能把手机号拼进了SELECT查询照样能被打穿。我在实际审过的系统里就看到过某管理后台上传文件功能把原始文件名存库管理员点删除后台执行DELETE FROM files WHERE filename $filename瞬间全表文件信息被清掉。这种杀伤力往往比直接注入读数据还猛。3. 哪些代码习惯在养雷从开发者视角找问题源头知道攻击怎么打了再回过头看代码你就能识别出那些迟早出事的写法。二次注入能成立前提是存在至少两个操作数据的模块且它们对数据处理不一致。我总结了几类最容易养雷的代码习惯每一条都是踩过坑的血泪总结。3.1 层层设防或层层失守转义、魔法引号与字符集混乱老一代PHP项目最常见的问题是入口处对GPC数据做了整体addslashes或magic_quotes_gpc到了具体业务层开发又基于数据已经安全的心理直接在查询语句里拼字符串。更致命的是如果数据库连接没有设置UTF-8而PHP页面设置了UTF-8字符串在多个环节转换过程中GBK编码可能让转义符\被吃掉形成宽字节注入。这类问题虽然严格说不算二次注入但它们暴露的其实是同一个根因对数据流经的每个节点是否可信完全没有明确的边界意识。二次注入的特殊之处在于它经常在防御相对完整、甚至过了WAF的系统中存活。因为攻击者提交时payload可能根本没有触发WAF规则它是作为一个普通字符串入库的。WAF检测的是第一次通信内容没法知道你数据库里将来哪条数据会被拼进SQL。3.2 习惯性用字符串拼接SQL且在读出来再用环节不做防护如果把代码里所有SQL出现的地方拉个清单你会发现很多写着表里字段已经安全了的地方恰恰是二次注入的爆发点。比如function getUserOrders($username) { global $conn; $sql SELECT * FROM orders WHERE username . $username . ; $result mysqli_query($conn, $sql); }这个函数可能被多个页面引用登陆后才调用传入的$username来自MySQL查询结果里的字段。开发者的原始设想是存进去的时候已经过滤了啊取出来应该没问题。但正如前面分析的存进去的是原始字符过滤只发生在那一次INSERT的拼接环节并没有修改数据库里的真实值。这种读出来即拼接的模式在Excel导入、报表导出、邮件群发、消息通知、后台数据管理等模块里尤其常见。这些模块往往从库里批量取出记录再拼进SQL做进一步查询一旦其中某条记录是可被用户控制的字符串整批操作就会被污染。3.3 白盒审计时如何快速定位二次注入点给初学者一个我自己惯用的排查思路。拿到一份源码先别急着从入口看改用逆向回溯法三层筛查第一层搜索所有直接拼接用户可控参数的SQL。重点关注$_GET、$_POST、$_COOKIE、$_SERVER变量在SQL语句中的出现位置。这类属于一次注入更容易发现。第二层筛出所有从数据库取出数据再拼进另一个SQL的代码段。搜索关键词可以是$row[、$result-fetch、$data[进出mysql_query/PDO/prepare的传参过程。第三层对候选点做污染源追踪数据库里哪个表的哪个字段可能被普通用户注册或编辑。如果字段可控、且存在第二步的拼接点基本就能确定这条数据流已经可以被污染。我在做代码审计时习惯把每个取数据库字段拼SQL的地方按业务优先级排序能影响管理后台的优先能影响UPDATE/DELETE的优先能影响货币/权限字段的优先。这样能在有限时间里找到最高危的二次注入点。4. 在本地靶场里复现二次注入从DVWA到SQLiLabs的踩坑记录纯文字还是不过瘾。我建议每个学这块的人都在本地搭个靶场亲手打一遍因为只有你亲手看到明明没报错库里的数据却已经被改了才能真正建立起对二次注入的敏感度。分享下我常用的靶场和具体操作路径。4.1 SQLiLabs的Less-24经典二次注入关SQLiLabs是SQL注入绕不过去的靶场。Less-24这一关专门演示二次注入操作流程是这样的先注册一个账号用户名写成admin--注册页面会做一定的输入校验或转义所以这一步只是把这个用户名存进user表登录这个账号打开修改密码功能输入新密码并提交。修改密码的SQL代码大概是UPDATE users SET password newpass WHERE username admin-- 单引号闭合了前面的字符串--把后面的注释掉最终被执行时等同于UPDATE users SET password newpass WHERE username admin这样一来攻击者就把admin账号的密码给改掉了然后直接用admin身份登录后台。整个过程中第一次注册没有任何报错第二次修改密码也没有任何报错但权限变化却是实打实的。建议你在自己环境里动手跑一遍注册时故意输入带单引号和注释符的用户名然后观察修改密码逻辑的执行效果。不要只看我给的答案自己把admin--的变体试个几遍比如admin#、admin AND 11体会不同注释符对结果的影响。4.2 DVWA与Pikachu换个姿势验证同样的原理DVWA的medium级别以上有些模块也存在类似二次数据处理问题但更典型的训练环境是Pikachu靶场的二次注入模块。它的设计思路更贴近真实业务用户注册一个带有恶意字段的账号然后在修改信息或其他功能里触发。Pikachu二次注入模块的操作大概分三步注册账号把payload放在比如用户名里进入编辑个人资料或类似页面提交修改观察SQL执行结果。我在第一次玩Pikachu时犯过一个低级错误没有先看数据库表结构就在注册时把payload写成 OR 11#结果UPDATE语句把所有用户的邮箱全部改成同一个值直接把自己的靶场数据搞乱了。这里也提醒一下在靶场里乱注没成本但养成先看目标SQL结构再构造payload的习惯对你以后打真实系统非常有帮助。如果不想装整套靶场也可以自己用Docker起一个MySQL和一个最简单的PHP应用画个二十行代码模拟注册、更新、查询三条路径直接观察数据库里的变化。二十分钟就能把这个漏洞的机理吃透。4.3 黑盒视角下如何发现二次注入如果站在渗透测试角度没有源码要怎么去发现二次注入我给一个常用的投石问路式思路先在注册/评论/资料编辑等入口提交一串带有标志性的字符串例如zzz AND 11顺利入库代表这个字段可控等待片刻或在站内正常操作触发读库功能比如搜索自己的昵称、导出订单、后台查询等观察报错信息、页面回显是否有异常或者用 AND SLEEP(5)#之类的时间型载荷做盲测看响应时间是否拉长如果系统出现SQL语法错误回显往往说明这个功能点存在拼接接下来就是普通的注入利用流程了。这个思路的关键是先埋点再触发。黑盒测试二次注入比一次注入费力因为你需要对业务逻辑有足够的理解知道哪些功能会读取你之前提交的数据。所以很多自动化扫描器对二次注入的检出率并不高这也是它能在真实系统中长期存活的原因。5. 防御体系搭建别只靠转义要从架构上断绝二次注入铺垫了这么多最后聊聊防御。二次注入的防御本质上不是某一个函数能解决的而是要在整个数据流里建立起任何输入都不可信的边界意识。我用一个表格先把防御层次列出来再逐个展开防御层次具体措施解决的根本问题查询层全部使用预编译语句/参数化查询从根上消除SQL拼接输入层白名单校验、类型强转、长度限制缩小恶意载荷可存储空间存储层区分原始输入与可信数据敏感字段加密或编码降低脏数据二次利用价值输出层在拼接入库前再次校验读出的字段堵住历史脏数据的利用架构层数据库最小权限、代码与数据分离限制注入后的影响范围5.1 参数化查询是唯一可信的免死金牌不管你是用MySQLi还是PDO核心原则只有一个任何SQL语句的无论是值还是条件都必须通过绑定参数传入禁止拼接。PDO的写法示例$stmt $pdo-prepare(UPDATE users SET email :email WHERE username :username); $stmt-execute(array( :email $email, :username $username ));这样写之后即使$username是从数据库里读出来的、里面真的带了admin--它也只会被当作一个普通的字符串参与WHERE条件比较因为预编译机制告诉数据库这个参数是数据不是SQL逻辑。它不会闭合任何引号也不会让注释符生效。很多人觉得这道理我早就知道但真到写代码时还是会图省事用字符串拼接。我自己的习惯是代码评审时只要出现.$variable或.$variable.出现在SQL语句相关代码里直接打回没有商量余地。时间长了团队形成条件反射二次注入自然没有生存空间。不过有一点要注意参数化查询扛得住普通的二次注入扛不住存储过程内部的动态SQL拼接。如果项目里大量使用存储过程并且在存储过程内用EXEC拼接字符串那么参数的免疫效果就打折扣了这种情况下要直接把日常查询也迁移到参数化模式并让存储过程的逻辑保持参数即数据的原则。5.2 输入校验与输出转义的组合拳严格来说参数化查询已经能解决90%的注入问题。但真实业务里总会有一些历史遗留SQL无法短时间全部改造这时候就需要配合输入校验、输出转义来降低风险。输入侧能定义类型的字段尽量走类型检查比如ID用intval邮箱用正则校验手机号限定数字位数。凡是自由文本做白名单字符集校验比如只允许中文、字母、数字和少量符号远比黑名单过滤可靠。长度限制也很重要把用户名限制在20个字符以内很多长payload直接写不进去。输出侧从数据库取出的字段如果要拼进SQL、Shell命令或文件路径必须重新做一次转义或校验。这不是重复劳动而是边界再次校验的必要动作。我在很多安全规范里看到过一句话参数化查询是第一道闸门但边界校验是最后一道救命的闸门。别指望单靠一道防线拦住所有攻击。5.3 数据库权限与日志监控给攻击者设置障碍即使漏洞真的被利用成功如果数据库账号权限控制得当攻击者也拿不到太多东西。常见做法是业务代码连接数据库的账号只授予该业务库必要的SELECT/INSERT/UPDATE/DELETE权限不要给FILE、PROCESS、SUPER等高权限。这样即使注入成功也没法直接读文件、写文件或者执行系统命令。另外把数据库的general_log或者审计插件打开。二次注入往往要经历注册脏数据触发拼接点两次操作日志里串联起来看会有明显特征比如同一个IP在多个接口反复提交特殊字符串。我在一次护网行动中就是靠数据库审计日志反推出某个旧接口存在二次拼接才及时把补丁打上的。安全不仅是防住也要能发现。5.4 框架层面的最佳实践ORM与预编译是默认选项如果你用的是Laravel、ThinkPHP、Spring Boot这类主流框架基本都内置了ORM和预编译机制。比如Laravel的Eloquent$user User::where(username, $request-username)-first();它底层就是参数化查询。团队成员只要避免使用whereRaw、selectRaw这些原生拼接方法二次注入基本可以被拦死在框架这一层。所以我给团队定的规矩是除了写复杂报表需求其余任何SQL操作禁止绕过ORM直接用raw query。遇到写原生SQL的情况必须经过代码评审且强制使用占位符。另外为数据库的字段类型做约束也是性价比很高的手段。比如用MySQL的CHAR/NVARCHAR并设置合理的长度限制可以在数据存储层提前截断过长的payload对用户可控字段统一用VARCHAR类型避免滥用TEXT因为TEXT字段存长引用载荷的余地更大。虽然这不能替代参数化查询但等于给攻击者增加了一层阻碍。5.5 安全编码检查表上线前过一遍能省很多事最后送上一份我每次上线前都会检查的安全编码清单也算是对整篇文章的一个实操收尾。你可以直接把它贴到团队评审流程里所有SQL语句是否使用了参数化查询或ORM有没有绕过ORM的原生拼串从数据库读出来的字段是否在进入下一个SQL之前再次做了校验比如用户名从Cookie或Session取出时是否重新查库校验对用户可控字段是否做了类型、长度、白名单字符集限制数据库连接账号是否遵循最小权限原则是否使用root或高权限账号连接业务库是否开启了SQL错误信息脱敏生产环境绝对不能把SQL错误回显给浏览器。对于评论、昵称、收货信息等可能被多个模块复用的字段有没有额外的存储编码策略比如JSON编码再存取出来时需要解码才会还原特殊字符我见过太多上线前测一下注入没测出问题就放心了的项目结果漏洞埋在三个月后新加的报表功能里。二次注入最阴险的地方就在这里——它可以是昨天注册时的普通文本今天是打入管理员账号的钥匙。只有把安全编码变成肌肉记忆才能真正堵住这条隐蔽的攻击路径。按照我个人的习惯现在每看到一个从数据库里取数据再拼SQL的代码第一反应都是先问一句这个字段用户能不能控制如果能哪怕它之前已经走过一百次转义我也会直接改成参数化查询绝不给雷任何引爆的机会。这套思路分享出来就是希望大家以后看到二次注入不再只停留在听说过的层面而是真正能在代码里认出它、拦住它。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。