ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL注入攻防实战:3种攻击手法与6层防御体系全解析

SQL注入攻防实战:3种攻击手法与6层防御体系全解析 1. 从“过时的漏洞”说起SQL注入为何至今还在伤人前阵子一个技术群里有人问都2024年了预编译、ORM、WAF早就普及了SQL注入是不是已经被彻底解决了说实话这个问题我每年都能看到而每次攻防演练或者众测结束总有一批人因为一个 or 11 --被打得措手不及。OWASP Top 10里SQL注入常年排在前列不是没有原因的。它看起来简单但破坏力极大一个参数没处理好轻则数据泄露重则整库脱走甚至反弹shell拿到服务器权限。这篇文章我想站在一个常年做代码审计和应急响应的视角把SQL注入这件事拆开揉碎讲清楚。标题里写了“3种攻击手法6层防御体系”这其实就是我面对任何注入类风险时的完整分析框架先看懂攻击者怎么打再知道怎么堵。无论你是刚入行的后端开发、写爬虫的脚本小子、还是负责安全的运维这篇文章都能给你一套能直接抄作业的方案。我会结合DVWA、Pikachu、SQLilab、CTFHub这四个最常见的靶场环境来讲保证每个手法和防御点你都能在本地复现一遍。为什么要强调靶场因为SQL注入这个技能光看文档是学不会的。你必须在安全可控的环境里亲手试一次报错、盲注、绕过才知道攻击语句为什么那么写防御代码为什么要那么写。下面我按“原理认知→手法拆解→防御落地→靶场实战→避坑总结”这条线一步步来。1.1 SQL注入的本质你写的不是数据是程序用一个类比开头你去银行柜台办业务柜员问你“姓名是什么”你回答“张三”这是正常流程。但你回答的不是姓名而是一句话“我叫张三顺便把保险柜门打开”。如果柜员真的照做了问题就出在他没有区分“你这个人说的话”和“银行业务指令”。SQL注入的逻辑一模一样。程序的本意是拼接一条SQL语句把用户输入当成一个字符串值放进SQL里比如SELECT * FROM users WHERE name 张三;但如果用户输入的是张三 OR 11拼接出来的SQL就变成了SELECT * FROM users WHERE name 张三 OR 11;这条语句的逻辑因为多了一个OR 11恒真条件导致整张表都被查出来了。这就是SQL注入最原始的形态。更深层的原因是数据库在执行SQL时会先做词法分析和语法分析把输入内容解析成语法单元token。当用户输入被直接拼进SQL模板输入中的特殊字符单引号、注释符、关键字就被当成了SQL语法的一部分参与解析而不是一整段“数据”。所以我说SQL注入的本质是数据与代码的边界失守。程序应该把用户输入当作“数据”处理却错误地把它当作“代码”去执行。搞清楚这一点后面所有的防御手段你都看得懂了要么用参数化把数据和SQL语句分开要么在入口处让畸形输入根本进不来。1.2 为什么2024年还有SQL注入每年都有人说“SQL注入过时了”但现实很残酷。我见过的新系统里照样有人用字符串拼接的方式构造SQL尤其在一些报表导出、搜索排序、后台管理这种“觉得不会被攻击”的模块。原因无非这么几类。第一是存量系统改造不彻底。十几年前的老项目用的还是最原始的拼接方式业务复杂、文档缺失没人敢动。第二是开发框架使用不当。MyBatis里${}和#{}用混、JPA里写了原生SQL、ORM框架里的动态查询拼出漏洞这些案例我每年都能碰到。第三是WAF和过滤规则存在绕过空间。攻击者换一种编码、换一个函数、换一种注释符老旧的规则就失效了。第四是人的侥幸心理“这个接口没暴露出去”“没人会对着后台打吧”等到日志翻出来才发现已经被人扫了很久。所以不要觉得“现在还存在SQL注入漏洞吗”是个傻问题。答案是存量多、增量没断、绕过技术也在升级。这篇博文写的就是怎么在这环境下最大限度把风险压缩到零。1.3 四个靶场一条从入门到精通的路线新手最常问我的一句话是靶场这么多我先玩哪个我的建议是分四步走对应四个靶场。靶场特点适合水平典型训练内容DVWA难度分级清晰Low到Impossible四档零基础入门数字型/字符型注入、UNION注入、SQL盲注Pikachu闯关式设计场景贴近Web漏洞本质入门到进阶宽字节注入、搜索型注入、二次注入SQLilabLess 1到65梯度化覆盖全手法系统化训练联合查询、报错、布尔盲注、时间盲注、堆叠注入CTFHub技能树技能树式模块场景丰富查漏补缺不同注入位置的变体、绕WAF技巧推荐顺序是先用DVWA把最经典的注入流程跑通再用SQLilab按Less编号系统刷一遍然后去Pikachu补宽字节、搜索型这类特殊场景最后用CTFHub零散时间验证自己是否真的掌握了每个细节点。注意这些靶场一定要装在本地虚拟机或Docker里玩绝不要拿去公网扫真实站点那是违法行为。2. 三种主流攻击手法拆解看懂攻击者的每一步标题里说“3种攻击手法”大家可能觉得是不是太少了。其实SQL注入的变体虽然多但从“回显利用”的角度来看万变不离其宗。我选了最有代表性的三种联合查询注入、盲注、报错注入含绕过技巧。这三种恰好覆盖了“有回显”“无回显”“有报错回显”三类真实场景吃透它们你在靶场里至少能应对九成以上的关卡。在开始之前先记住一条判断原则拿到一个注入点时永远先回答两个问题——这个参数是什么类型页面有没有回显数字型参数不需要闭合引号字符型参数需要小心引号闭合页面有回显优先考虑联合注入没回显就转盲注报错可用就上报错函数。很多新手卡在某个靶场过不去往往就是没先判断这两个问题拿着payload乱试。2.1 手法一联合查询注入让页面替你把数据吐出来联合查询注入是最好理解、也最直观的手法前提是页面上有查询结果的回显位置。攻击思路很简单让原有的SQL查询结果为空再用UNION把自己的SQL语句结果拼接进页面。以DVWA的Low级别为例这个关卡在URL上直接传ID参数原SQL类似SELECT first_name, last_name FROM users WHERE user_id $id;先把参数改成1 and 11看看页面是否正常再改成1 and 12看看是否空白或报错这一步用来确认参数是字符型且能被注入。接下来用order by判断字段数1 order by 1 -- 1 order by 2 -- 1 order by 3 --当order by 3报错时说明查询只有两个字段。然后构造联合查询1 union select 1,2 --页面把1和2作为结果回显出来说明这两个位置可用。接下来就把payload丢进去爆库名、表名、列名1 union select database(), user() -- 1 union select table_name, table_schema from information_schema.tables -- 1 union select column_name, table_name from information_schema.columns where table_nameusers -- 1 union select user, password from users --这里有个细节--后面要跟一个空格在URL里又通常写成--因为加号会被解析为空格。MySQL中#注释符也可以但某些环境下传参可能被编码干扰两个都试试。防御视角看这个方法页面回显是攻击者最大的帮手。生产环境里把错误信息隐藏、把查询结果与页面输出做分离联合查询的利用门槛会高不少。但从防御者角度必须明白联合查询只是三板斧之一它只是“有回显”场景下的最优解遇到无回显场景就要立刻转盲注。2.2 手法二盲注没有回显也有办法“猜”出数据很多程序员觉得“我的页面不报错查询结果也不显示”就安全了。这是大错特错。盲注就是专门对付这种“无回显”场景的。盲注的核心思想是页面虽然不告诉你查询结果但它会告诉你“这个条件是真还是假”。攻击者写一个带判断条件的SQL根据页面状态差异一位一位地猜出数据。布尔盲注和时间盲注是两种最常见的实现。布尔盲注的典型payload长这样1 and ascii(substr((select database()),1,1))64 --这条语句的逻辑是如果数据库名的第一个字符的ASCII码大于64条件为真页面正常显示否则条件为假页面显示异常。攻击者通过二分法不断调整阈值最终确定字符。比如ascii()大于100就继续试大于110一步步逼近具体ASCII值。时间盲注更进一步当页面连“真假差异”都没有时就用时间延迟制造差异1 and if(ascii(substr(database(),1,1))64, sleep(3), 0) --这里用if()三目运算条件成立就让数据库延迟3秒否则立刻返回。攻击者一看响应时间长短就知道猜得对不对。手工做盲注非常痛苦所以实际攻击中一定会写脚本。一个最简单的Python盲注脚本思路是用requests库循环遍历每个位置每次只判断一个字符。先猜长度再逐字符猜ASCII码。我给大家写了个参考模板import requests url http://127.0.0.1/sqli-labs/Less-8/?id1 and {} -- result # 先猜长度 for i in range(1, 20): payload flength(database()){i} if You are in in requests.get(url.format(payload)).text: print(database length:, i) break # 逐字符猜ASCII for pos in range(1, 20): low, high 32, 126 while low high: mid (low high) // 2 payload fascii(substr(database(),{pos},1)){mid} if You are in in requests.get(url.format(payload)).text: low mid 1 else: high mid result chr(low) print(result)You are in是SQLilab Less-8页面里判断条件真假的标志字符串换成你自己的目标页面特征即可。这套脚本放本地靶场跑能很直观地感受到盲注的本质大量的请求、规律的判断、字节级的信息泄露。防御上盲注最大的克星是网络白名单和参数化查询因为盲注再慢只要有网络连接就能慢慢磨出来。2.3 手法三报错注入与绕过技巧当数据库自己泄露秘密有一种场景比盲注舒服多了页面虽然没有直接的查询结果回显但会打印数据库报错信息。攻击者可以构造一个必定报错的SQL表达式让数据库在报错文本里把查询结果带出来。这就是报错注入。以MySQL为例最经典的报错注入函数是extractvalue和updatexml1 and extractvalue(1, concat(0x7e, (select user()), 0x7e)) --这条语句的含义是extractvalue函数解析XPATH时遇到非法格式0x7e是波浪号~的十六进制故意让XPATH格式非法于是报错信息里会包含我们的子查询结果。页面就会显示类似XPATH syntax error: ~rootlocalhost~的文字。SQL Server系列的报错函数又不太一样。比如SQL Server 2008里常见写法是1 and convert(int, db_name()) --利用convert把数据库名强制转换为int类型必然失败报错信息里就会带上db_name()的执行结果。这也是热搜词里“sql server 2008注入”常见于老系统的原因——大量金融、政务项目还在用SQL Server 2008语法体系和MySQL有差异很多人拿着MySQL的payload去试当然打不通。再说绕过技巧。实战中你的输入往往会被过滤常见过滤包括单引号被转义、and/or被替换、空格被过滤、注释符被拦截。对应的绕过思路要成体系注释符绕过--不行就换#、/* */或者利用MySQL的--。大小写绕过SeLeCt、UnIoN适用于大小写不敏感的数据库和过滤规则不严的WAF。等价替代and换成or换成||空格换成/*注释*/或%0a换行符。宽字节注入编码转换时%df%27里的%df会和转义符\即%5c合并成一个宽字符导致后面的单引号逃逸。Pikachu靶场专门有这个关卡用GBK编码的老系统里还是很常见的。等价函数替换sleep()被禁用就换benchmark(10000000, md5(1))。防御视角看报错注入不管SQL怎么写攻击者依赖的是“报错信息泄露了查询结果”。所以在应用层关闭详细报错、在数据库层不向客户端返回原始错误内容这两点做扎实报错注入基本废掉。绕过技巧再花哨绕不过“参数化白名单”的底层逻辑。3. 六层防御体系从代码到运维的一次性加固方案讲完攻击手法下面进入重头戏六层防御体系。我把它定义为“从代码到运维的一次性加固方案”因为这里每一层都不是可选项而是相互补充的纵深防御。很多团队有一种误解上了预编译就万事大吉或者加了过滤函数就够用了。但真实世界的脆弱系统往往是层层都缺一点代码里偶尔用字符串拼接SQL数据库账号权限过大报错信息全裸奔日志没有开出事了才发现数据已经被拖了很久。六层防御就是把这些洞一块一块堵上让攻击者打进来时每一步都浑身难受。3.1 第一层参数化查询把数据与代码彻底隔离这是所有防御里最重要的一层也是唯一能从底层根除SQL注入的方案。参数化查询的本质是SQL语句的结构在预编译阶段就已经被数据库确定用户输入只能作为参数值传入不再参与语法解析。拿Java的PreparedStatement举例安全写法是String sql SELECT * FROM users WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();不安全的写法则是String sql SELECT * FROM users WHERE username username AND password password ; Statement st conn.createStatement(); ResultSet rs st.executeQuery(sql);Python里用MySQLdb或PyMySQL同样如此占位符%s把参数与语句分开cursor.execute(SELECT * FROM users WHERE username %s AND password %s, (username, password))为什么参数化能防注入因为用户输入传进数据库后是被当作一个完整的数据值进行存储和比较而不是SQL语法片段。就算输入里带着单引号、OR、注释符也只是变成类似张三 OR 11这样的一个字符串值不会影响SQL语句结构。你可以从生活角度理解银行柜员终于学会区分“你填写的姓名”和“你发出的指令”提交给数据库的流程被固化了。在实际项目里哪怕你用了ORM框架也一定要检查框架是否能参数化地处理所有查询。很多ORM的where条件、order by排序还是可以传入原生字符串片段这同样是注入的温床。我见过不少团队以为“用了MyBatis就安全了”结果${}一用照样被打穿。3.2 第二层输入验证与规范化能接受什么先写清楚参数化是兜底输入验证则是把脏数据挡在系统外面。这一层的核心不是“过滤危险字符”而是“明确这个参数应该长什么样”。白名单思维要大于黑名单思维。比如数字型ID参数就强制转成整数。PHP里可以intval()Java里可以用类型转换多数情况下根本不需要关心SQL注入因为非数字内容在入口就被拦截了$id intval($_GET[id]);再比如状态字段你只允许取值为0、1、2中的某个值就直接用枚举校验不在枚举范围内的数据全部拒绝。邮箱、手机号、日期也都用正则或格式校验这样彻底杜绝畸形字符串进入后续处理。长度限制也很重要。一个用户名最多20字符你设一个50字节的上限攻击者塞一段几百字节的payload时直接被拦下。还有个容易忽略的点是统一编码尽量在系统入口做字符集统一校验避免出现宽字节注入依赖的GBK/UTF-8混用场景。但要强调一个关键认知输入验证和过滤只能作为辅助层不能作为主防御。因为黑名单永远不可能穷举所有攻击向量。攻击者今天用union select明天就可能用%75nion%20sel%65ct这种编码变形绕过。一旦你把过滤当作主要防线就会陷入和攻击者无休止的攻防拉锯战。所以正确的姿势是参数化打底输入验证过滤做纵深。3.3 第三层数据库账号权限收敛注入成功也拿不到东西这一层很多人忽略但实战中意义巨大。哪怕代码里出现了一个注入点如果数据库账号权限足够小攻击者充其量只能查到当前业务库的少量数据根本拖不了整个数据库更谈不上读information_schema以外的系统库。设计原则很简单Web应用连接数据库的账号绝对不能用root等高权限账号。应该按最小权限原则只授予该应用实际需要的库表权限。MySQL里可以这么分CREATE USER blog_web10.0.0.% IDENTIFIED BY StrongPass123; GRANT SELECT, INSERT, UPDATE, DELETE ON blog.* TO blog_web10.0.0.%; FLUSH PRIVILEGES;这样就限制了blog_web账号只能操作blog库别的库一概碰不了也没有FILE、CREATE、ALTER等高危权限。攻击者即使通过注入读取到了账号密码也无法利用这个权限去写文件或执行LOAD_FILE。如果业务确实需要管理端做数据导出也建议单独用一个受限的管理账号而不是复用Web连接账号。存储过程的权限问题也要留意。如果存储过程的DEFINER是root但调用者是低权限账号调用过程中对表的访问权限依然以定义者权限执行这在某些场景下等于提权。所以存储过程创建时就要明确权限边界不要让存储过程跑在比调用者高一级的权限上。3.4 第四层框架使用规范别让MyBatis和ORM背锅现在几乎没有团队不接ORM框架但框架不是免死金牌。以MyBatis为例#{}和${}的区别是每个Java开发都必须刻进脑子里的知识。#{}走的是预编译安全${}是直接字符串拼接危险。不安全的写法常见于动态排序、动态表名、模糊查询!-- 危险${}直接拼接 -- select idgetUser resultTypeUser SELECT * FROM users WHERE name LIKE %${keyword}% /select !-- 安全用 concat 结合 #{} -- select idgetUser resultTypeUser SELECT * FROM users WHERE name LIKE CONCAT(%, #{keyword}, %) /select排序字段又是另一个重灾区。因为ORDER BY后面的列名和方向不能被参数化很多人干脆用${orderBy}拼接。正确做法是做一个白名单映射前端传一个枚举值后端根据枚举值映射到固定的列名和排序方向严禁把前端字符串直接拼进去。String safeOrderBy switch (orderBy) { case createTime - create_time; case id - id; default - id; };还有IN列表的场景参数占位符不好处理很多开发就手贱写了拼接。其实MyBatis的foreach标签就是为了解决这个问题的。真要是框架能力不够也可以把列表拆成多个参数逐一绑定。总之凡是用框架的地方核查标准就是一句话这条SQL的结构部分是否允许用户输入参与。3.5 第五层数据库与基础设施加固纵深防御不靠运气这一层是在应用代码之外做文章。哪怕前面的代码有漏洞也要让攻击者的利用链路断在半路。首要任务是关闭错误回显。PHP环境设置display_errors Off并把error_reporting级别打到只写日志不输出页面Java系框架要自定义错误页不要让Spring Boot的Whitelabel Error Page把堆栈信息打在页面上。做安全测试时你会发现攻击者判断注入类型和注入点位置时最依赖的就是报错信息把回显一关攻击成本立刻上升。数据库本身也要加固。老版本数据库往往是重灾区热搜词里有“sql server 2008注入”就是因为SQL Server 2008早已停止主流支持很多老系统补丁跟不上报错函数、提权存储过程都存在历史漏洞。这类系统如果短期无法升级至少要做三件事打例外补丁、加上WAF前置、限制数据库端口只允许应用服务器网段访问。WAF和IPS属于基础设施工事。虽然攻击者有各种绕过WAF的技巧但WAF依然能拦下大量自动化扫描和初级攻击相当于给系统多加了一道门禁。自建规则时要注意不要只针对精确字符串做匹配要带上正则和语法分析维度比如检测经典的UNION SELECT组合、SLEEP()、BENCHMARK()、extractvalue、updatexml等。还要特别留意接口来源和频率一个正常用户不可能在3秒内请求100次同一个带参数的接口。3.6 第六层监控、审计与应急响应最后一道保险丝防御做得再好也要假设自己会被打穿。监控和审计的价值在于把“被攻击了半年才发现”压缩到“攻击发生后的几分钟内发现”。数据库层面可以开启审计日志。MySQL的general_log会记录所有语句生产环境通常不建议全开因为性能开销大。更可行的做法是开启binlog并配合应用日志记录关键操作或者用专门的数据库审计插件。SQL Server有扩展事件、审计功能可以记录指定账号的SELECT、DELETE等行为。日志的用途不只是事后的责任认定更重要的是溯源攻击路径。应用层也不要闲着。给关键接口的入参、执行时间、执行结果打上日志特别是搜索、排序、报表导出这类高发注入位置。时间盲注有一个明显特征是请求响应时间呈规律性波动比如每个请求都恰好比正常请求慢3秒监控系统如果能抓到这种指标异常基本就锁定了攻击行为。应急响应预案要提前写好。发现SQL注入后的标准流程应该是先通过WAF或网关临时切断该接口的外网访问再拉取最近一段时间该接口的请求日志用自动化工具分析可利用的payload评估数据泄露范围最后回滚代码、做修复、全量复查。平时可以做一次短暂的演练否则真出事时大家只会手忙脚乱。企业如果还没建设完整的SDL流程也可以先从“安全需求评审上线前代码扫描”做起SonarQube、Checkmarx这类工具能自动抓出不少语句拼接问题。4. 靶场通关实录与踩坑指南前面理论讲得再多不动手永远记不牢。这一章我把DVWA、Pikachu、SQLilab、CTFHub四个靶场的通关要点和踩坑经验写出来。这些经验的共同特点是光看答案没用你得自己把每个payload敲一遍然后观察页面的变化。4.1 DVWA四个难度怎么打DVWADamn Vulnerable Web Application是我最推荐的第一站因为它把同一漏洞分成Low、Medium、High、Impossible四个等级你能直观看到同一个注入点在防御逐级增强下的攻击变化。Low级别就是完全裸奔在User ID输入框或URL的id参数里输入1页面会报SQL语法错误这相当于已经把注入点写在脸上了。接着用order by判断字段数构造1 union select 1,2 --回显两个字段继续爆库名表名列名即可。Low级别通关的意义是让你把联合查询的流程练熟。Medium级别换成了POST提交请求体里是id1SubmitSubmit代码用了mysqli_real_escape_string做转义但对数字型参数没做类型约束。这个关卡的坑点在于--注释符在POST里放不住得用#或者把注释符编码成URL格式我试过--在某些版本的DVWA里无效老老实实用#才通。High级别是典型的二次注入场景。代码里用了参数化查询防止直接注入但它会把输入存入数据库后再用另一个查询把存储的数据取出来拼接SQL。你在输入框提交一个带的字段比如admin #它存进数据库时是安全的等到后台SQL把这条记录重新拼接使用时注入就触发了。这个关卡的本质提醒了很多人一次处理安全不等于全链路安全数据库里的脏数据值可能变成二次攻击的跳板。Impossible级别用的是PreparedStatement加白名单属于本文六层防御中第一层和第四层的综合体现。这关你无论怎么注入查询都只是把参数值绑定到预编译好的SQL里页面只返回正常用户数据。通关DVWA四档之后回来看六层防御体系你会觉得那些防御手段每个都有场景的对应关系。4.2 Pikachu的宽字节与搜索型注入Pikachu是我见过最适合练特殊场景的靶场它把Web安全知识做成了闯关游戏。其中两个关卡值得单独拿出来讲。第一个是宽字节注入。这个关卡模拟的是GBK编码环境下程序对用户输入的单引号做了转义把变成\看起来安全了但攻击者输入%df%27时%df与转义生成的%5c即\会被MySQL当作一个GBK宽字符運后面的单引号就从转义中“逃逸”出来了。payload大致是%df%27 or 11 --宽字节注入在现代框架里已经不容易出现因为字符集统一为UTF-8但这关作为“理解编码带来的安全边界问题”很有价值。你要能解释清楚为什么%df和%5c会合并成一个字节对才知道如何在系统里从入口统一编码来根治这个问题。第二个是搜索型注入。很多网站的搜索框是这样拼SQL的SELECT * FROM article WHERE title LIKE %{$keyword}%;攻击者输入的构造点是双引号或单引号外面的%。经典payload是任意值% or 11 -- 让原本的SQL变成WHERE title LIKE %任意值% or 11 -- %等于把LIKE条件解除了整表数据全被查出来。真实业务中也经常能见到搜索框注入因为搜索功能最常用也最容易拼接错误。4.3 SQLilab与CTFHub的系统化训练SQLilabSQLi-Labs靶场是“SQL注入训练界的题库”从Less-1到Less-65每一关都是特定注入类型。刚开始刷的时候不要贪快每一关都手工打一遍再用sqlmap自动打一遍做对照。Less-1是字符型注入Less-2是数字型注入Less-3是带括号的字符型注入Less-8是布尔盲注Less-9是时间盲注Less-15是POST注入Less-23是注释符过滤Less-32是宽字节Less-38是堆叠注入。这个顺序天然就是一个完整的学习大纲。CTFHub的技能树则是按知识点切片来组织的整数型注入、字符型注入、报错注入、布尔盲注、时间盲注、联合注入每个大类下都有独立的题目比如同一个盲注手法在GET参数、POST参数、User-Agent、Referer等不同位置的变形。这些场景对真实业务非常有参考价值因为真实系统的注入参数不会只出现在URL里你可能会在Cookie、请求头、JSON请求体里找到注入点。刷CTFHub时我建议给自己定个目标每个模块至少手写一次payload不要只做最后一步的拿到flag。比如布尔盲注模块你要能解释database()、substr()、ascii()这三个函数的配合逻辑时间盲注模块你要能解释if()和sleep()的执行流程。只有理解了每一步为什么这么写才算真正掌握。4.4 手动盲注脚本怎么写很多人在靶场里遇到盲注就会不耐烦觉得手工猜太慢。我的建议是第一次盲注一定要手工猜完一个字符串的前几个字符理解整个过程然后立刻写脚本自动化。这种“先懂原理再用工具”的顺序会让你以后用sqlmap时心里有数不会拿到结果也不知道它怎么来的。我之前写过的一个手动盲注脚本思路在2.2节已经贴过代码模板了。补充几个关键细节第一判断成功的标志字符串一定要在响应里足够独特不能用“页面正常”这种模糊判断否则容易误判第二二分法猜ASCII码时初始范围设32到126就够覆盖所有可打印字符第三脚本要设置合理的timeout时间盲注里响应会慢判断逻辑反而更简单——响应时间大于设定的阈值就认为条件为真。高级一点的盲注脚本还可以把编码、UA、Cookie等参数做成可配置项因为真实场景中WAF可能只审计UA或者Referer头。把这个脚本跑通了你再回去看SQLilab的Less-8、CTFHub的盲注题都会觉得思路清晰很多。5. 常见问题与防御误区速查表5.1 面试与实践中总被问到的几个问题问题一现在还存在SQL注入漏洞吗存在而且远比想象中多。一方面大量存量老系统长期带病运行代码里全是拼接SQL另一方面新技术栈里也有新的踩坑姿势MyBatis的${}、JPA的原生SQL、GraphQL的参数拼接都是新泄露点。每年的大规模攻防演练SQL注入都是告警排行榜里的“钉子户”。问题二万能密码为什么万能所谓万能密码本质是利用OR构造恒真逻辑比如admin or 11。写成SQL就是SELECT * FROM users WHERE usernameadmin OR 11 AND password任意值;由于OR的优先级让这个条件永远为真登录逻辑就被绕过了。但注意这只是最简单的攻击逻辑真实渗透中攻击者很少只满足于登录绕过更多会用联合查询直接拿数据、用报错函数探数据库信息、用盲注慢慢拖数据。问题三SQL Server 2008和其他数据库注入有什么差异SQL Server的系统视图是sysobjects、syscolumns和MySQL的information_schema不一样注释符上--后面必须换行或结束语句/* */嵌套规则也略有差异报错注入常用convert、cast函数构造类型转换错误而不是MySQL里的extractvalue。还有xp_cmdshell、sp_oacreate这类高危存储过程是老版本SQL Server提权绕不开的话题但这部分属于利用层面我只建议在授权测试环境里研究生产环境一定要禁止和监控这类存储过程。问题四SQL注入只能通过URL参数触发吗不是。GET参数、POST正文、Cookie、Referer、User-Agent、X-Forwarded-For、JSON字段任何进入后端后被拼进SQL的数据都可能成为注入点。CTFHub里就有专门练UA注入和Referer注入的题目建议都做一遍。5.2 防御侧的三个高频误区误区一只过滤不参数化。addslashes、mysql_real_escape_string、正则过滤这类手段看着简单但编码绕过、宽字节注入、等价替换都能绕过把它们当主线防御等于把安全寄托在攻击者“不会变通”上。误区二用了ORM就绝对安全。ORM只对你调用它的方式负责如果你在ORM里写原生SQL、用createNativeQuery、在查询方法里拼字符串照样能注入。框架的责任边界是每个开发都要清楚的。误区三没有报错信息就安全。盲注从头到尾不依赖报错它只依赖“真和假”的页面/时间差异。日志里看不到SQL报错不代表攻击者没在慢慢拖你的数据。5.3 代码审计快速定位注入点的经验最后分享一点代码审计层面的经验。拿到一个项目的源码想快速找SQL注入不要从文件头开始读那是低效的。我的做法是全局搜索关键词优先盯字符串拼接。Java里搜select * from 、String.format(sql, ...)Python搜fselect ... {、select ... %s %PHP搜select ... $。凡是搜索结果里出现“用户可控输入参与了拼接”的地方全部标红。然后重点关注几个天然高危的位置登录查询、搜索功能、报表导出、排序字段、分页参数、批量删除的ID列表。这些地方要么接收用户输入直接拼接要么开发觉得“只是内部功能”就省了防护。还有一个容易被忽视的检查点是老接口和“内部接口”。很多系统的老版本接口在迭代中被新的接口替代但路由没有下线参数校验也被历史遗留问题改来改去搞没了。攻击者往往就盯着这种接口打。我个人这些年做代码审计最大的体会是SQL注入拼的不是技术难度而是工程纪律攻击手法就那几类防御方案也早有标准答案真正掉链子的永远是“这里不会有人注入吧”“这个接口不对外吧”“先上线再补过滤”这些侥幸心理。有一年我们团队做完一次内部扫描发现最严重的一个注入点居然在报表导出的日期参数上——所有人都知道报表模块的SQL是拼接出来的但都觉得“参数就传两个日期能有什么问题”。结果攻击者把日期字段换成了子查询几秒钟就把另一个库的数据带了出来。好在发现得早还是在测试环境。从那以后我们团队定了两条硬规矩第一任何SQL语句禁止字符串拼接不管是业务逻辑还是报表导出全部参数化第二每次发布前自动跑一遍SAST扫描新代码里出现疑似拼接直接阻断合并请求。听起来很土但确实管用。最后再分享一个日常小技巧如果你在排查一个老系统的注入风险但拿不准某个接口是否安全最快的验证方式不是翻源码而是直接往参数里传单引号、order by、sleep(3)这三个测试点。传单引号看是否报错传order by看是否出现字段数异常传sleep(3)看响应时间是否有规律波动。三者都不中基本可以判断这个接口大概率是参数化处理了的。记住测试一定要在自己搭建的靶场或授权环境里做千万别拿生产系统开玩笑——安全测试的第一条永远是有授权第二条才是会挖洞。
返回列表