做安全这一行,既看过不少只把“SQL注入”停留在or 1=1层面的朋友,也看过把一条数据库告警查到服务器权限的较真选手。真正把“SQL注入”玩明白的人,几乎都会把三件事放在一起琢磨:二阶注入、堆叠查询、UDF提权。它们的攻击思路天然是一条链路——先用常规或非常规手法拿到入口,再用堆叠查询扩大战果,最后通过UDF把权限从数据库拔高到操作系统,三种形态环环相扣,难度一个比一个高,危害也一级级放大。
这篇内容适合正在学习渗透测试的初学者、需要写安全检测规则的研发同学,也适合做代码审计或SDLC的负责人。我会把“这不是怎么绕过WAF的教程”这句话先说清楚,重点放在原理、判定思路、复现路径和修复方案上。因为大家迟早会发现:没有原理支撑的payload,换个场景就失灵;没有运维层配合的修复,代码写得再严也拦不住提权。
1. 为什么说“二阶注入、堆叠查询、UDF提权”是一条完整链路
1.1 从一次OA系统告警讲起
前阵子你如果刷过安全资讯,大概率会看到某电子文档安全管理系统的一个接口存在SQL注入漏洞的消息,接口名就叫cdgauthorisetempleteservice1之类。很多人扫一眼就过去了,我的第一反应却是:这类系统一旦被SQL注入撕开一个口子,攻击者要的是后续动作而不是简单读写表。
这类管理系统的数据库权限配置往往偏高,早期的部署还可能直接沿用低版本的MySQL,secure_file_priv都没被限制。也就是说,一个接口的注入点,真正连起来的可能是:注入点获取管理员口令 → 后台文件上传或直接执行堆叠查询写文件 → UDF提权拿到主机shell。这就像你看到一扇窗户没锁,专业的人不是只探头进去拿东西,而是会去想怎么打开大门让搬运车进来。二级标题里的三个关键词,本质上是同一条入侵路径的不同节点。
1.2 把需求翻译成技术语言
如果有人问你“学高级SQL注入到底学什么”,我一般给一个最精简的需求拆解:
- 输入点能不能被当成SQL代码解析?
- 如果当前SQL语句的后缀是固定的,我能不能用堆叠查询开启第二条语句?
- 当前数据库账号有没有文件读写权限?
- 数据库安装目录、插件目录是否可写,能不能通过UDF把函数变成命令执行接口?
这四个问题,正好对应了“注入能力 → 执行能力 → 写能力 → 系统权限”的逐级放大。很多人只关心第一个问题,觉得能注出来数据就算完事。实际上,安全测试里最值钱的是证明“这个漏洞能对系统造成什么真实影响”,UDF提权就是SQL注入危害从“数据库”跃迁到“服务器”的关键证据。
1.3 影响面不只是“脱库”
SQL注入的常规危害大家都懂:登录绕过、敏感数据泄露、后台操作等。但一旦进入高级阶段,影响面会扩大到这三层:
- 应用层:任意数据被读写,业务逻辑被绕过,账号体系被控制。
- 数据库层:数据库配置被篡改,文件系统被读写,存储过程被调用。
- 服务器层:如果最终走到
sys_exec这类UDF函数,数据库用户就可以直接执行操作系统命令,反弹shell、植入后门、读取服务器内存中的秘密都可能发生。
这也是为什么我做代码审计时,只要看到这里有个SQL注入,不管是POST参数还是User-Agent头,都会下意识再判断一下数据库账号权限,而不能只停留在“修掉这个注入点”。因为你根本不知道攻击者背后还有多少招数等着你。
2. 三种攻击形态的原理拆解,以及常见的理解误区
2.1 SQL注入登录绕过:从一个if条件开始拆
先复习一个最简单的场景,也是搜索引擎里最常看到的热词:“万能密码绕过登录”和“CTF SQL注入绕过登录”。它们本身不算高级,但理解透了,能帮我们把后面两个概念彻底想明白。
假设网站的登录查询是:
SELECT * FROM users WHERE username = '$u' AND password = '$p'普通用户输入admin和123456,拼出来就是:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'可如果$p被输入成:
' OR '1'='1拼出来就是:
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'最终整个条件变成(username='admin' AND password='') OR ('1'='1'),恒真,绕过成功。
到这里大部分人都会说“哦懂了,闭合引号然后注释掉后面”。但对做防御的人来说,关键是理解这个地方的“语法边界”被打破了:用户输入本来是“数据”,却被拼接成了“代码”。后面我们看到的堆叠查询、二阶注入,本质都是同一种错误在不同场景下的变体。
所以别再只问“为什么加了addslashes还会注入”,因为转义永远只解决了一部分字符串边界问题,解决不了像intval之后拼接数字、二次读取导致数据进入新SQL上下文这类问题。
2.2 二阶注入:危害被数据库“存档”了
这是很多人理解得最模糊的一个点。我常这样跟朋友打比方:一阶注入就像当场吵架,输入什么立刻爆发;二阶注入却像是把你的恶意写进了档案,系统当时没察觉,等哪天档案被调出来用的时候才炸。
经典例子如下:
- 用户注册时,在用户名里填写
admin' --。 - 注册SQL也许用的是预编译或者过滤参数,整个字符串被原样存进数据库,没报错,也没影响注册。
- 第二天,管理员在后台通过用户名来修改资料,拼接了带用户名的SQL,比如
UPDATE users SET email='xxx' WHERE username='admin' -- '。这里--把后面的条件注释掉,导致整张表所有人的资料都被修改了。
很多人第一次听到会愣一下:“明明第一步是安全的啊。”对,第一步如果单独看可能没问题,问题在于你把“不可信输入”原样存进了库,而且没记录“这个字段是外部输入”的元信息。下次有些聚合并操作或检索流程直接把数据库里的字符串当成可拼接SQL的一部分,就触发了。
防御心态如果没有建立起来,最典型的错误是:只在数据库入口做转义,认为只要入库时“干净了”就万事大吉。实际上,二次注入需要系统在所有用到该字段的SQL中都保持参数化,同时在输出和拼接不同语言环境时做对应编码。只要你把数据从MySQL里读出来,再到PHP/Java/其他语言里拼SQL,这条链路上任何一个连接点没有边界防护,都可能被利用。
2.3 堆叠查询:一个数据库连接执行N条语句
常规注入下,很多人靠UNION SELECT来拼接列或读取数据,但它有一个硬限制:要求原SQL和目标查询的结果列数一致,而且能力有限。堆叠查询不同,它不需要“列数对齐”,只需要数据库连接支持同时执行多条语句,并且分号后允许注入方追加完整SQL。
还是同一个例子:
SELECT * FROM users WHERE id = '1'如果代码允许在同一个连接上一次执行多条语句,且输入点处理不当,就会变成:
SELECT * FROM users WHERE id = '1'; DROP TABLE secret;这意味着攻击者不局限于读取当前查询的内容,而是可以执行任意增删改查。它比常规注入危险得多,因为它把“注入”从“操纵一条查询”升级成“在数据库里自由执行”。
不过我这里要说一下实际限制。很多语言和数据库驱动,默认不允许执行堆叠查询:
- PDO MySQL默认的
PDO::MYSQL_ATTR_MULTI_STATEMENTS通常是关闭的。 - MySQL的服务器端虽然支持一条协议发多条语句,但驱动不开,也白搭。
- 有些老版本MySQL或某些管理系统使用的连接层比较宽松,才会真正暴露风险。
堆叠查询的常见用途不是删库——那样太没技术含量,而且很容易被发现。它真正让人头疼的是配合写文件、调用存储过程和后续的权限提升动作。比如你注入点后面没法接INTO OUTFILE,因为前面的SELECT结构已经锁死了,但堆叠查询可以另起一条写文件的SQL,瞬间打开新局面。
2.4 UDF提权:数据库账号到系统shell的临界点
UDF,全称是User Defined Function,即用户自定义函数。MySQL允许用户把一段动态库注册成函数,比如sys_exec()可以直接执行系统命令。很多场景下,攻击者在“数据库内”已经拿到了足够高的权限,比如可以读写MySQL的plugin目录,但还缺一个“操作系统命令执行”的能力,于是上传一个编译好的UDF库文件,再用SQL创建函数,最终通过SQL调用达到命令执行的提权效果。
提到UDF提权,有三个前提条件缺一不可:
- 数据库以较高权限运行,最常见是root或mysql用户,并且Plugin目录可写。
- 当前数据库账号具备
FILE权限,也就是能使用SELECT ... INTO OUTFILE写文件。 secure_file_priv没有限制为NULL,或者允许写入插件目录。
满足这些条件后,大概流程就是把写好的UDF动态库通过SQL文本或十六进制方式写入MySQL的插件目录,然后注册自定义函数。到了这一步,数据库就不再是个“存储系统”了,它变成了一台能执行系统命令的小型跳板。对防守方来说,敏感目录的可写权限和secure_file_priv设置,几乎是打包在一起检查的。
这里需要特别强调,不要指望攻击者会按部就班。我在项目里见过的情况是:明明数据库账号不是root,但堆叠查询配合已有的存储过程、计划任务、通用日志路径修改等手法,照样能实现命令执行或写webshell。所以UDF提权不是终点,它只是“数据库权限 → 系统权限”的一种代表路径,关键是思维上有没有建立“数据库账号也是一种系统账号”的意识。
我在实际渗透测试和应急响应里发现,很多研发对“数据库权限过高”没有概念,认为“库在服务器里跑着,phpMyAdmin开着也正常”。可真正发生安全事件时,日志里往往会出现一堆sys_exec调用记录,事后看非常明显,但因为没人提前去关心,系统已经沦陷很久了。
3. 三种攻击形态的技术对比与判定清单
3.1 先用一张表看懂三者差异
| 维度 | 一阶注入 | 二阶注入 | 堆叠查询 | UDF提权 |
|---|---|---|---|---|
| 触发点 | 输入参数直接被拼到SQL | 数据库里已有的数据被二次拼接进SQL | 同一连接执行多条语句 | 数据库插件目录写入UDF库 |
| 前置条件 | 未参数化查询 | 数据存储后再次被使用 | 驱动开启多语句支持 | FILE权限、Plugin目录可写 |
| 核心危害 | 绕过认证、拖库 | 绕过入口防护,后置触发 | 执行任意增删改 | 获得系统命令执行能力 |
| 排查重点 | 所有外部输入点 | 全链路追踪对数据库字段的使用 | 连接串和框架配置 | 权限、路径、监控告警 |
这张表我一直当成速查卡用。很多时候,一个漏洞被通报出来时,企业内部不同团队会因为术语不一致吵半天:开发说“这是二次注入”,安全说“这是越权”,运维说“数据库被入侵了”。其实他们都在描述同一条链上的不同位置,把表摆出来,责任边界就清楚了。
3.2 识别一个输入点是否可能触发二阶注入
我最常用的判断方法很笨,但有效:跟着数据走。从入口参数出发,看它会落到哪张表,之后会不会有其他SQL再把该字段读出来拼接使用。
技术执行时可以这样拆:
- 列出所有“接收用户输入并入库”的字段。
- 搜代码里所有
WHERE条件、ORDER BY、GROUP BY等位置,是否使用过这些字段。 - 重点看后台列表页、搜索页、管理页,这些页面特别容易直接把数据库字符串拼进SQL。
- 还要看管理后台是不是对“当前登录用户的昵称”之类的字段敏感,如果管理操作会反查用户信息,用户名如果经过二次拼接,就可能成为触发点。
另外要叮嘱一句,很多系统喜欢在前端拼SQL条件,比如导出Excel时把筛选字段传给后端。这种逻辑最适合做防御措施:宁可改造为白名单加预编译,也不要把筛选字段名直接映射到数据库列名。
3.3 判定当前环境是否具备UDF提权条件
如果你在做合法授权范围内的测试,或者做企业内部加固,遇到一个MySQL数据库,可以按下面顺序检查:
- 当前用户:
SELECT user(); - 是否具备
FILE权限:SELECT * FROM mysql.user WHERE Grant_priv='Y';,老版本可以直接看File_priv。 - 插件目录:
SHOW VARIABLES LIKE 'plugin%'; - 文件安全限制:
SHOW VARIABLES LIKE 'secure_file_priv'; - 数据库版本:不同版本写UDF库的路径、字段名差异很大,尤其是MySQL 8.0之后,部分安全机制变化明显。
有两条经验值得分享:第一,判断“能不能提权”比“要不要复现提权”更重要。很多复盘场景,只要证明权限配置允许UDF写入,实际上就该直接启动应急预案了,没必要真把命令执行环境搭出来给生产库添乱。第二,我见过不少系统里plugin_dir真的可写,但数据库是以低权限服务账号运行的,这时即使UDF上传成功,命令执行权限也有限,反而会走计划任务等手段。要结合环境综合看待,不要把UDF当成唯一答案。
4. 靶场环境下的一次完整动手复盘
4.1 登录绕过和堆叠查询的实操对比
这里我不建议直接拿公网真实系统练手,更推荐用Pikachu这类开放在本地的漏洞靶场。自己搭一个docker或者在虚拟机里跑起来,完全合法且重复性高。
我习惯的复现路径是:
- 先测试最基础的SQL注入是否闭合引号。单引号报错、双引号不报错这类现象,能帮你快速确定是哪种类型。
- 用
order by判断列数,再用union select查看回显位置。这一步的核心是“理解原查询的字段结构”。 - 测试分号后面能不能继续执行语句。比如在参数值后加上
;select sleep(3),观察响应时间有没有异常。如果延时不明显,很有可能连接层禁止多语句。 - 如果多语句可行,再考虑具体的影响,比如修改管理员密码、往某张表里写日志、调用存储过程等。
以Pikachu的SQL注入关卡为例,它的登录框直接体现了经典问题。输入admin' or '1'='1和一些简单绕过写法后返回正常登录,立刻就能定位出SQL拼接问题。很多CTF题里也出现过类似套路,但题目往往会加过滤——把空格、or、--都替换为空,这时就要考虑用注释符/**/、十六进制编码、大小写变形来绕过。我建议在训练时把每种过滤都记录在案,因为它们能帮助你理解WAF的配置思路,真正工作时才能既会打也会防。
4.2 二次注入和UDF提权在靶场里的连接
打开Pikachu后,除了常规的SQL注入闯关,更值得注意的是“数据库层攻击”相关的模块。你可以把表结构简单改一下,模拟一个带用户名回查的后台功能:
- 注册页面创建一个用户,用户名故意写
test' or '1'='1。 - 用后台另一个正常功能点查询该用户。
- 如果页面里没有做参数化,查询逻辑就会把用户名原样拼进去,触发全表查到的问题。
很多人做这一步容易犯迷糊,总想着“后台就不能用预编译吗”,其实靶场真正的价值不是展示业务做得多糙,而是让你体会:一个数据一旦被存储为字符串,后续所有消费它的地方都可能是攻击面。
UDF提权不建议在Pikachu内部直接完成,因为它更偏数据库运维层。我通常是在一台全新的MariaDB/MySQL容器里做实验。实验前先确认版本和插件目录,然后使用管理员账号上传UDF库文件并注册自定义函数。整个动作其实很短,但因为步骤少,恰恰是很多人最想跳过细节的地方。不同系统的UDF库格式不能混用,Linux下要用.so,Windows下要用.dll,版本还需要跟MySQL主版本匹配,否则函数一调用就是崩溃。
再强调一次:UDF实验一定放在隔离的测试容器里完成。不要试图在自己的工作库或某个客户的测试库里顺手验证,因为UDF库的残留很麻烦,一旦函数注册成功,删除时要先删函数再删文件,顺序反了数据库会直接报错。容器实验的最大好处是可以随时重建,把战场打扫得非常干净。
4.3 每一层攻击对应的防御动作
打靶不能只打不修。我在复现完一个点后,会强制自己和团队成员把“防御动作”也写在同一份笔记里:
- 登录绕过:把SQL改成参数化查询,参数绑定后,无论输入什么字符,都只会被当作字符串字面量,不会进入SQL语法层。
- 堆叠查询:在应用层关闭驱动多语句支持。PDO连接串明确不开启
MULTI_STATEMENTS,Java的JDBC连接去掉allowMultiQueries=true。 - 二阶注入:不仅要参数化,还要对新进入数据库的所有字段都保持同样标准,对所有“从库里取出后再次拼接”的逻辑做审计。不要相信数据库里存的内容是绝对安全的。
- UDF提权:数据库账号不能随便给
FILE权限,secure_file_priv=NULL尽量打开;数据库不能以root系统权限运行;插件目录做写保护;定时检测是否有异常.so/.dll文件被新增。
我在实际加固里还发现一个关键点:很多人修完一个注入点后不做回归测试,结果攻击者换个参数又打进来了。正确做法是拿同一份测试用例跑全量回归,把上一个漏洞的所有已知payload集合覆盖一遍,确保修复没有留下变体。
5. 常见问题排查与修复落地
5.1 WAF“高级过滤”为何经常失效
网上很多文章叫“SQL注入高级过滤”,看多了之后你会得到一个结论:黑名单过滤永远滞后。因为过滤规则本质是“猜攻击者会怎么绕”,而攻击者只要多一种编码方式就多一次机会。
真正有效的思路是“分离数据与代码输入”,也就是把所有SQL分为两类:结构性SQL预编译,动态表名/排序字段等实在无法预编译的那部分,用白名单校验。表名不在允许范围内就直接拒绝,比正则过滤可靠得多。
很多公司总想把过滤放在WAF或代码最外层,结果WAF看一眼参数、代码再过滤一遍,看似双保险,实际上只要两边规则不一致,攻击者就有很大的绕行空间。真正要做的是进行参数化查询,WAF只当辅助,让业务代码从一开始就不具备被SQL注入的基础。
5.2 如何判断一个接口是否已经被人尝试过UDF提权
在做应急响应时,我一般会看下面几个痕迹:
- MySQL插件目录下有没有新增的
.so或.dll文件,注意按文件的创建时间排序找出新文件。 - MySQL系统库
mysql.func表有没有异常函数记录,常见异常函数有sys_exec、sys_eval或自定义名称。 - 数据库错误日志里有没有加载动态库失败的信息。
- 系统目录如
/tmp或软件安装目录有没有可疑的共享库文件残留。 - 服务器上有没有异常的系统计划任务、新增的启动项或监听端口。
只要出现前两项,基本可以判定攻击者已经尝试或完成了UDF提权,接下来要做的就不是“修一个SQL注入点”的事,而是按“服务器已被控制”的剧本处理。把机器断网、保留现场、通知安全团队,这比继续做代码级修复优先级高得多。
5.3 一套可以抄作业的MySQL加固建议
先给一段直接可执行的检查SQL示例,用来快速核对基础状态:
SHOW VARIABLES LIKE 'secure_file_priv'; SHOW VARIABLES LIKE 'plugin_dir'; SELECT user, host, File_priv FROM mysql.user; SELECT * FROM mysql.func;如果secure_file_priv的值是空的或某个具体的目录,说明数据库允许文件导出,建议设为NULL,禁止任何SQL层面的文件读写。如果你必须用文件导出功能,把它指向一个业务专用目录,并确保数据库账号无法写入任何代码执行相关目录。
插件目录的权限也建议从系统和数据库两层同时限制:系统层去掉写权限,数据库层也别给它开放动态库上传路径。UDF提权的前提是“能往插件目录写文件”,只要这一步断掉,整条链路就断在最关键的地方。
应用层代码层面,我会给出一个老生常谈但最管用的例子。用PHP的PDO写登录查询时,参数化应该长这样:
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?'); $stmt->execute([$username, $password]);Java的PreparedStatement同理。凡是写SQL的代码库里出现字符串拼接,不管拼接内容来自客户端还是数据库字段,都应该触发一次Code Review。尤其是那些把用户昵称、搜索关键词、排序字段直接拼进去的代码,往往是SQL注入高发区。
5.4 排查清单速查表
| 检查角度 | 排查思路 | 高可用结论 |
|---|---|---|
| 应用代码 | 是否存在拼接SQL的接口? | 参数化覆盖全部持久层 |
| 框架配置 | 是否开启多语句支持? | 默认关闭,特殊场景必须单独论证 |
| 数据库配置 | secure_file_priv和local_infile设置 | 前者设NULL,后者关闭 |
| 数据库账号 | 业务账号是否存在DBA或FILE权限 | 按最小权限拆分 |
| 插件目录 | 是否可被数据库进程写文件 | 系统层移除写权限 |
| 存储字段 | 有没有把外部输入直接当SQL片段 | 输出/拼接时必须重新校验 |
| 运维日志 | 是否有异常UDF加载或故障记录 | 联动告警并定期审查 |
排查顺序很重要,我从实战中得到的建议是“先看数据库账号权限,再看应用层”。因为如果账号权限太高,即使应用层堵住了这个接口,别的接口可能还会漏;只有把所有业务账号与数据库权限缩到最小,SQL注入后果才被限制在可控范围内。
6. 个人实操复盘与几个值得记住的坑
先从踩得最重的坑说起。我刚从前端切到安全时,想当然地把“防SQL注入”等同于“每个地方都拼一个过滤函数”。后来一次代码审计中,我发现一个系统确实对输入参数做了很多转义,只要外部传参都会过滤,可是当数据被写进数据库后,后台另一处功能用该数据做排序和查询条件时,没有做任何过滤。那次被确认存在二次注入后,我才真正体会到:安全不是入口一个函数能解决的,它要贯穿数据的全生命周期。
另一个坑发生在做堆叠查询测试时。当时我明明在MySQL命令行可以一次执行多条语句,但通过业务接口测试时发现分号后的内容总是无效,排查了半天才发现是中间层数据库连接配置里关闭了多语句支持。这个经历告诉我:防御方只要做对“驱动配置”这一件事,就能挡掉一大半堆叠注入攻击,并不需要特别复杂的算法。
关于UDF提权,我最想说的一点是“别小看环境一致性”。你在测试环境复现出UDF提权,不代表生产环境也能复现,因为数据库版本、插件路径、操作系统位数、加载库的格式都可能不同。但这不等于生产环境就安全。我曾经在一个老系统里发现,业务账号能读mysql库的user表,还能向插件目录写文件,虽然最终因为系统版本限制没能加载UDF,但已经足够证明这台数据库存在横向移动风险。所以做安全评估时,不要只以“能不能打通”作为唯一指标,“具备几个高危前置条件”同样值得写进报告。
如果你也在研究SQL注入,我建议把精力从收集花哨payload转向“搭一套自己的训练环境”,从登录绕过的参数化改造开始,再到二次注入的数据流追踪、堆叠查询的驱动配置,最后用提权实验亲手感受“数据库权限 → 系统权限”的临界点。只有自己真正完整走过一轮,再回头看网上那些漏洞情报或热搜关键词,才会明白每条漏洞公告背后到底在说什么。