SQL注入攻防实战:从原理、盲注到WAF绕过与防御加固

发布时间:2026/10/22 12:03:39
SQL注入攻防实战:从原理、盲注到WAF绕过与防御加固 我把这些年做渗透测试和帮企业做加固的经验浓缩成了一条从“看懂”到“跑通”再到“防住”的完整链路SQL注入到底是怎么一回事、常规注入和盲注怎么判断、WAF来了之后常见的拆解思路以及更重要的——作为防守方你到底应该在代码和架构的哪个位置把洞堵上。这篇文章不会只贴Payload我会把每一步背后的判断依据也一起写出来这样你拿到测试环境里能复现换到真实业务里也能举一反三。先说一句最重要的话这篇文章里所有涉及攻击手法的内容都只能在授权后的靶场、测试环境或合规的众测/渗透测试项目里复现。未经授权去碰别人的网站那不是挖洞是违法而且现在日志和流量追溯能力都很强别拿前途开玩笑。下面进入正题。1. 从零认识SQL注入它到底是什么1.1 一条URL暴露的秘密很多首次接触SQL注入的人会把问题想得太神秘。其实它本质上就是一件事程序把用户输入的内容当成了SQL语句的一部分去拼接执行。假设某个站点有一个商品详情页URL长这样https://example.com/goods.php?id58后端代码大概是这个思路$id $_GET[id]; $sql SELECT * FROM goods WHERE id $id;输入58时语句是SELECT * FROM goods WHERE id 58很正常。如果我把58改成58 OR 11语句就变成SELECT * FROM goods WHERE id 58 OR 11因为11恒为真整条WHERE条件对全表都成立——如果权限没限制返回的就是整个表的数据。到这一步你可能还没感觉到危害但这其实就是SQL注入的核心模型你的输入穿越了“数据边界”进入了“命令边界”。1.2 注入点判断的“三步法”拿到一个参数怎么判断它背后是不是SQL拼接我常用的方法是三步过程很朴素但效率极高。第一步输入一个单引号观察响应是否出现异常。比如报错信息、页面空白、状态码500这些都是线索。如果原本能正常显示的商品列表直接崩了说明这个参数很可能被拼到了SQL中且没有过滤单引号。第二步输入58 AND 11和58 AND 12对比两个响应是否一致。前者恒真应该能正常返回数据后者恒假应该返回空。如果两者页面表现不同那基本可以确认这里存在字符型注入而且布尔状态会被页面直接反映出来。第三步用报错函数或注释符进一步确认数据库类型。比如MySQL下输入58 AND extractvalue(1,concat(0x7e,database()))--如果页面回显了版本信息或库名那注入点和数据库类型就一并确认了。整个流程听起来不复杂但实际测试里最大的变数是中途可能被WAF拦、被CDN缓存混淆、被开发者在代码里写了一个字符集转换函数。所以三步法不是背下来就能用重点是理解每一步的目的前两步判断“有没有拼SQL”第三步判断“拼进去之后能不能利用”。1.3 为什么有些注入一眼就能看出来做漏洞测试久了你会发现一个问题有些注入点位拿工具扫半天扫不出来但手工看几秒就能确认。原因是工具判断“是否存在注入”的时候依赖的是特定的特征模型比如和标准正则匹配。而真实业务里的SQL拼接千奇百怪有的参数外面套了括号有的字段被函数包裹比如WHERE id intval($id)看起来防住了但如果代码写的是WHERE id intval($id) $_GET[extra]那extra依然是拼接点。我之前在一次授权测试中遇到过一个搜索接口参数在代码里的拼接方式是SELECT * FROM articles WHERE title LIKE %$keyword%这种场景下工具很容易因为关键字被%包裹而判断为“过滤严格”但实际上只要把关键词构造成% UNION SELECT ... --就能完美逃逸。所以人工测试的核心价值是理解代码的真实上下文而不是机械地往URL里塞一堆特征字符串。2. 入门阶段经典注入类型与判断逻辑2.1 数字型、字符型、搜索型先分清参数性格SQL注入的利用方式很大程度取决于参数在SQL中处于什么位置。我在实战中习惯先把参数分三类数字型、字符型、搜索型。数字型参数典型特征是SQL中直接拼接数字比如WHERE id $id。测试时不需要考虑引号闭合问题直接id58 AND 11或id58 AND 12就能判断。字符型参数则不同它在SQL里带着引号比如WHERE username $name你必须先考虑如何闭合引号比如输入admin --这样把后面的引号注释掉。搜索型参数更特殊很多站点会用LIKE加通配符拼接比如WHERE title LIKE %$kw%这种位置的引号和百分号闭合都要精心设计通常的Payload是%kw% UNION SELECT ... --这样的组合。判断参数类型只靠猜不靠谱。最实用的一招提交一个输入看数据库报错信息或者用AND 11/AND 12做布尔对比。如果偶数和奇数页面的返回有明显差异说明这里对布尔条件敏感然后根据闭合方式判断用引号还是直接拼接。2.2 联合查询注入入门最快的一条路联合查询UNION SELECT是SQL注入里最直白的利用方式。它的逻辑很简单原本的查询结果会显示在页面上UNION SELECT可以将你自己的查询结果拼接到原始结果后面一起回显到页面。经典操作分四步。第一步确认原查询的字段数用ORDER BY n不断调整n值直到页面报错为止。比如ORDER BY 5正常ORDER BY 6报错说明查询返回5个字段。第二步用UNION SELECT 1,2,3,4,5替换出页面上哪些位置是回显位。第三步把回显位换成敏感查询比如版本、当前库名、表名。第四步从表名筛选到列名再到具体数据内容。这套流程跑起来非常快也是各类自动化工具的底层逻辑。但你可能不知道的是联合查询有个前题条件前后两个查询的字段数必须一致而且数据库得允许UNION操作。遇到MySQL 8某些情况下的严格模式或者订单表查询时关联了太多字段手工调整字段数反而更灵活。我见过不少人在这一步浪费大量时间原因不是不会写UNION而是没有搞清楚“页面到底回显了第几列”。有的页面会把所有字段循环输出有的只显示第一个非空值因此你必须先构造UNION SELECT 1,2,3,4,5看数字出现在什么位置而不是直接把敏感查询写进去。2.3 报错注入把数据库的抱怨变成输出不是所有页面都会回显数据列表。有些功能登录后只显示“操作成功”或“密码错误”这时候UNION注入基本没法用因为攻击者看不到结果集。但数据库很诚实遇到语法错误时会把详细信息抛出来——报错注入的思路就是故意制造一个错误把要查询的数据带到错误信息里。MySQL下最经典的两种报错函数是updatexml()和extractvalue()它们的SQL语句长这样AND updatexml(1,concat(0x7e,(SELECT database())),1) AND extractvalue(1,concat(0x7e,(SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1)))这里0x7e是波浪号~的十六进制作用是作为分隔符让报错信息更直观。SQL Server下常用convert(int,version)这类类型转换报错Oracle则用CTXSYS.DRITHSX.SN但那套语法比较绕新手可以先放一放。报错注入虽然好用但限制也明显单条报错回显的字符串长度通常有限MySQL下一般是32个字符左右。所以碰到抽取大量数据时需要配合substr()函数一段段截取非常耗时。我在测试中更愿意优先看有没有UNION或者布尔盲注的条件实在没路走了才用报错注入。3. 深入阶段盲注与必然要面对的执行面3.1 布尔盲注与时间盲注页面不显示数据也不报错这类注入最折磨人。但“不显示”不等于“无状态”我们用布尔盲注的思路构造一个恒真和恒假的SQL条件观察页面在两种条件下的反应是否不同。比如某个页面条件为真时正常显示数据条件为假时显示“暂无数据”哪怕只是一个字数差异也能作为判断依据。id1 AND SUBSTRING((SELECT database()),1,1)t如果页面正常说明库名的第一个字符是t然后逐步调整位置和字符就像拆盲盒一样把数据一位一位拆出来。时间盲注则是给布尔盲注的一种兜底方案——当页面无论真假都返回同样内容、甚至完全空白时用延时函数来制造时间差。id1 AND IF(SUBSTRING((SELECT database()),1,1)t,SLEEP(3),0)如果响应时间明显超过3秒说明猜测正确。这种方式很稳定但速度极慢而且在高并发业务上很容易触发WAF的速率检测。所以我的建议是时间盲注只用于确认“这里还有路”真正跑数据还得靠工具或者干脆换一个输出点。3.2 堆积查询与写文件执行面从哪里打通很多刚入门的测试者会在某个环节卡死明明确认了注入点但既没有回显也不能用布尔判断。这时候就要考虑当前这个数据库连接是否支持多语句执行。如果支持就可以使用堆积查询堆叠注入。在MySQL中PDO连接如果设置了对多语句的支持攻击者就能在一条语句后追加任意SQLid1; SELECT sleep(3)-- id1; INSERT INTO test_table(col) VALUES (payload)--堆叠注入的价值不在于查数据而在于操作数据甚至操作系统。比如在SQL Server里执行EXEC xp_cmdshell或者在MySQL里通过SELECT ... INTO OUTFILE向网站目录写入webshell。这些都属于执行面打通。但这类操作的限制同样明显MySQL的--注释符后面必须紧跟一个空格有些版本对注释要求很严格另外现在大部分线上数据库用户权限都不高写文件的前提是具备FILE权限且知道网站绝对路径。所以我在做测试时不会把“写文件”当作必现路径而是先确认数据库连接用户到底有没有这个资格。3.3 编码、大小写、空格基础静态绕过手法在还没遇到真正WAF的时候很多开发者也会做一些简单的关键词过滤比如把SELECT、union等字符串直接替换为空。这些过滤看起来很吓人但绕过其实不难。最基础的思路是大小写混写Union SeLeCt因为早期过滤只比对小写。接着是注释符拆分MySQL支持/*!*/这种内联注释例如/*!50000UNION*/把关键字中间切开开发者的正则就很难匹配。还有一种常见手法是用制表符、换行符代替空格因为很多过滤规则只关注空格和关键字的组合比如%0b、%09、%0a都能在一些场景中替代空格生效。这里我要提醒一句静态绕过只是“预赛”。真正复杂的是遇到WAF之后它会做语法分析、行为检测会和解码、规范化过程结合着判断。所以在了解静态绕过时与其背一堆Payload不如理解一个核心WAF和过滤规则本质都在“匹配”而匹配就有误报和漏报绕过就是在它的匹配盲区里找可被后端解析的机会。4. 进阶阶段常见WAF的拦与拆4.1 WAF到底在拦什么先说结论市面上绝大多数WAF拦截SQL注入的核心逻辑并不是“我看不懂SQL”而是通过规则匹配和少量行为分析找到高度疑似攻击的特征。它关注的点无非是这几类敏感关键字union、select、sleep、extractvalue、updatexml等特殊符号单引号、注释符、括号、分号异常URL编码%27、%2527这种双重编码请求参数的数量和大小头部其他字段里的注入特征理解了这一点你就明白为什么同一个Payload在不同时期、不同WAF下的结果会完全不同。因为WAF的规则集是动态更新的你今天能绕过的点可能三天后就被那条规则堵上了。所以与其追求“万能绕过包”不如掌握拆解思路。4.2 等价函数替换与谓词变换WAF会对关键字做正则匹配比如extractvalue命中了那就换一个功能相近的函数。MySQL里报错函数不止一个updatexml被拦可以试ST_XPointFromWKB这类小众函数sleep被拦可以用BENCHMARK(5000000,SHA1(test))顶上去虽然不是精确延时但计算量足够让响应慢一个量级。谓词变换的思路更常用。原始条件IF(11,SLEEP(3),0)里有IF和SLEEP两个特征可以改成天然等价但有不同写法的逻辑比如CASE WHEN (SELECT 1 FROM dual WHERE 11) THEN 1 ELSE 0 END或者用位运算、乘法运算来构造恒真、恒假id1 AND 11 id1 AND 2-11 id1 AND 1-10 id1 AND 10这些变换相当于用另一种“方言”表达同一个逻辑如果WAF的规则里没有覆盖这种语法变体就会直接放行。需要强调的是变换之后的Payload必须经过本地数据库验证不能想当然因为不同数据库对运算符的优先级和函数支持并不相同。4.3 大数据包、分块传输与协议层绕过关键词和逻辑变体层面的绕过属于“内容对抗”。还有一个方向是“传输对抗”核心思路是WAF的解析引擎感知到的内容和后端业务程序接收到的内容不完全一致。最典型的是分块传输编码Chunked Transfer Coding。HTTP协议里请求体可以被拆成多个分块每块自带长度标记。很多WAF默认只解析第一个分块或者因为分块组装超时直接放行。于是可以在第一个分块里放一个正常值第二个分块里放真正的攻击语句实现绕过。另一种思路是“参数污染”和“参数冗余”。同一个参数名出现多次不同中间件取值的逻辑不同。比如Nginx取第一个Tomcat取最后一个如果WAF检查的是第一个而业务取的是最后一个那攻击语句放在后面就能逃过检测。还有一种容易被忽略的点内容类型混淆。WAF一般会按Content-Type决定是否做SQL注入解析。如果业务本身需要接收JSONWAF可能只解析JSON格式但如果你把请求体改成multipart/form-data或application/x-www-form-urlencoded的混合体而业务后端同样能解析就可能出现检测规则被绕过的情况。这类协议层绕过对WAF的对抗要求很高但对测试者也提出了一个问题你能否完全理解目标中间件的解析差异如果不能建议先在本地搭建相同中间件做针对性验证而不是直接在真实环境里试。4.4 从绕过给防御带来的启示聊完绕过思路想回到防御端说一点真实的感受。很多企业上WAF是为了“合规”或“省心”但实际上WAF永远只是最后一道外部防线真正决定系统安全性的还是代码层是否使用了安全的SQL执行方式。我在一次真实测试中观察到某个系统已经上了一款主流云WAF但业务代码里是用字符串拼接SQL而且接口参数直接被传入存储过程。测试Payload被WAF拦了十几次后我把Payload做了分块传输处理结果WAF直接放行后端一次查询就把全库数据带出来了。这个案例充分说明一个残酷的现实攻击者只要花时间研究WAF的漏洞绕过的概率并不低。所以防御不能只靠WAF。合理的纵深防御应该是参数化查询兜底输入校验做辅助数据库账号按最小权限设计WAF负责拦截已知攻击模式日志审计负责发现绕过后的异常行为。这四层各司其职才是一个合格系统的样子。5. 防御视角从源头堵住注入5.1 参数化查询为什么能根治问题很多人问SQL注入到底能不能彻底防御答案是能前提是所有SQL语句都使用参数化查询或预编译机制。这里的原理我在入门时一直理解不透直到有一天用生活常识想明白了。你要是把SQL语句看成“填空题”把用户输入看成需要填入的答案。字符串拼接的做法是先把答案和题目混合成一句话再让数据库去执行——所以攻击者输入的“伪答案”能改变整句话的意思。而参数化查询的做法是先告诉数据库这句话的格式是这样这里、那里是参数位然后你再把参数值传过去。数据库在执行时只会把参数当作数据而不是命令的一部分。数据和命令从一出生就被分离了。以Java为例String sql SELECT * FROM users WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);Python版本类似cursor.execute(SELECT * FROM users WHERE username %s AND password %s, (username, password))无论用户输入什么都只会被当作字符串字面量处理不存在引号逃逸和关键字拼接的余地。这就是为什么我总说只要全项目严格使用参数化查询SQL注入这个漏洞类型基本可以从自家系统里“清零”。5.2 硬编码白名单与最小权限原则参数化查询解决了“怎么执行”的问题但还有两类场景需要额外注意一类是动态列名、排序字段名、表名有时无法参数化另一类是业务需要执行动态拼接的存储过程。这两个场景是防御中最容易被忽略的口子。我的建议是能白名单就白名单。列名、表名、排序字段这些结构性的标识不要在用户输入的字符串里直接拼。可以在代码里建立一个映射表比如sortable_columns {time: create_time, hot: view_count, price: price} sort_col sortable_columns.get(request_args.get(sort), create_time)用户只能从有限的键里选择值本身是代码里写死的SQL片段这样即使传入的内容再离谱也不会改变查询结构。另一个原则是数据库账号的权限最小化。业务连接数据库的账号不应该拥有DROP、ALTER、FILE这类高危权限。应用账号只需要CRUD加连接权限就够了。这样就算注入真的发生攻击者也只能在数据库连接账号的能力范围内操作后续影响会被压制到最小。5.3 三类典型的防注入代码剖面实战里见过很多号称“防住了”的代码但写法其实分三档效果差别很大。第一档黑名单过滤。典型做法是把select、union、sleep这些关键词替换成空或加上反斜杠。我见过的很多自研防护代码都停在这一档。问题也很明显过滤覆盖面有限正则写得稍微差点就会被编码绕过而且维护成本高规则跟不上攻击手法的变化。第二档转义函数。比如MySQL的mysql_real_escape_string()以及框架自带的转义工具。这种做法能让输入里的单引号、双引号、反斜杠得到转义因此单条语句拼接时攻击者很难再“逃出”引号。但它的局限在于如果输入点同时出现在其他上下文比如LIKE查询里、存储过程里或者数据流的某个解码环节里就可能被绕过。第三档参数化查询加白名单校验。这是我推荐所有团队最终落位的做法。SQL语句结构固定参数值输入通过预编译接口传递动态标识符通过白名单映射。再配合数据库账号的最小权限这套方案能应对绝大多数真实攻击场景。我理解很多老项目迁移到参数化要改不少代码但这事儿不能偷懒。如果项目短期改不完至少给存量高危接口做一次参数化改造排期新接口一律强制预编译这是最小成本、最大收益的路径。6. 常见问题与实战排查6.1 明明有注入点但工具扫不出来做授权测试时有时候手工已经确认了某个GET参数存在布尔盲注但流行的自动化工具却报告“未发现注入”。原因大概有三种。第一种工具自带Payload被WAF拦截。主流的工具检测时会发送大量特征明显的Payload遇到WAF直接就被打了所以工具识别不到。第二种目标站点对频率有限制工具并发过快触发风控导致返回结果异常。第三种工具对数据库类型的指纹识别失败比如默认用MySQL的注入语句去测SQL Server自然无效。这种情况我的建议是先手工确认注入类型和数据库类型然后在工具里写自定义Payload或者干脆自己写一版基于布尔盲注的脚本配合代理工具做流量调整。补充说明一下sqlmap这类工具其实也支持--prefix和--suffix参数你可以把手工试出的闭合方式传给它再让它在那个基础上跑效果会好很多。6.2 注入后的“隐形坑”即使确认了注入可以利用测试过程中还有几个很容易让人翻车的隐秘问题。第一个是编码问题。有些数据库连接层设置了GBK或UTF-8的转换可能导致你输入的中文Payload在数据库侧被解析成完全不一样的字符。著名的宽字节注入就是利用这一点在GBK编码下让转义符本身失效。应对方法是先确认连接的字符集再看过滤函数实际的作用位置。第二个是注释符和语句截断。MySQL的#注释符和--后面必须带空格各有适用场景。部分情况下末尾注释符没写对会导致SQL语法错误页面直接崩了你以为是注入不存在其实是语法不对。在遇到末尾有括号或子查询时注释符的选择和括号补齐格外重要。第三个是脱裤不是终点。很多人拿到数据库后就停下来了但在授权测试里你要评估的是危害链路。权限能不能拓展、能不能横向移动、数据库服务器是否能连外网这些都是后续影响分析的一部分也是完整报告里不可缺少的内容。6.3 一场授权测试的完整复盘最后分享一段最近做的授权测试复盘省略敏感的站点信息和具体数据只保留攻击路径和判断过程。某业务系统提供用户查询功能输入手机号可以查询订单。我刚开始按常规思路提交13800138000 AND 11结果响应一片空白连业务错误码都没返回。第一反应是有WAF拦截但没有返回拦截页可能是WAF直接静默丢弃请求。随后我调整思路尝试用JSON格式提交把内容放在一个嵌套数组里。第一次提交被拦第二次把请求头的Content-Type改成text/xml但内容仍是JSON响应居然正常了。我立刻用布尔条件测试确认存在注入点。进一步构造UNION查询时发现目标数据库是SQL Server单一引号已经闭合于是调整表名和列名语法成功读取到当前库的表结构最终在授权范围内验证了数据泄露风险。全程最耗时的环节不是构造Payload而是定位中间件的解析差异。这个案例也再次印证了我开头的观点注入本身是代码问题但能不能被发现往往取决于环境对输入流的处理方式。作为测试者你的任务就是把自己放在开发、运维、安全设备三方都能接受的夹角里找到被忽略的那条缝。根据个人经验我还想专门提醒一点报告里除了要写清楚漏洞位置和利用方式更要说明修复优先级和配合方案。很多企业不是不想修而是不知道自己哪个系统高危、哪个要先改、怎么改。你给出可落地的修复建议远比冷冰冰的POC截图更有价值。最后再分享一点检测侧的经验这个内容讲到这里差不多可以收尾了。但还有一件事想单独拎出来说因为它和“从入门到绕过WAF”这个主题紧密相关却经常被测试者忽略——日志侧的证据留存。做攻防对抗时所有Payload的最终归宿都是服务器和数据库日志。你写进去的UNION语句、报错函数、延时条件都会成为后续应急响应中定位攻击来源的关键线索。我见过不少测试人员只关心能不能绕过去忽略了自己留下的痕迹和业务侧日志记录的情况。其实在授权范围内做一次完整测试应该在开始前就确定好日志保存范围、测试时间窗口并在结束后将特定记录传给防守方进行复盘。这样才能让一次演练真正产生安全价值而不只是“攻击者爽了一把”。另外如果你正在学习这类技术我建议你准备一套本地靶场环境把常见的WAF容器镜像、不同数据库版本、不同中间件都搭进去反复练习。理解“数据是如何被解析的”比背一百个绕过Payload有用得多。等你哪天能在靶场里连续绕过三个不同厂商的防护策略你回去再看业务代码很多一眼就能发现的洞就不需要工具帮你确认了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询