SQL注入绕过注释符过滤实战:从CISP-PTE真题看攻防本质
1. 项目概述从一道真题看SQL注入攻防的本质最近在带几个朋友备考CISP-PTE发现很多人在“SQL注入”这个核心考点上尤其是面对一些基础的过滤机制时容易卡壳。比如题目里经常出现一个看似简单的限制过滤了常见的SQL注释符像--、#、/* */这些。新手一看就懵了觉得注入的“语法”被破坏了无从下手。这其实是一个非常好的切入点它逼着我们去理解SQL注入的本质而不仅仅是死记硬背payload。今天我就以一道典型的、涉及注释符过滤的CISP-PTE真题为蓝本手把手带你走一遍完整的实战流程。我们不仅要复现解题步骤更要深挖背后的原理让你明白为什么过滤会失效以及攻击者是如何“见招拆招”的。无论你是正在备考的学员还是想巩固Web安全基础的朋友这篇内容都会让你对SQL注入有更立体、更实战化的认识。2. 真题场景与核心思路拆解2.1 目标环境与题目描述我们模拟的真题场景是一个简单的用户登录查询接口。后端PHP代码的关键片段可能如下$username $_POST[username]; $password $_POST[password]; // 过滤掉常见的SQL注释符 $filtered_username str_replace(array(--, #, /*, */), , $username); $filtered_password str_replace(array(--, #, /*, */), , $password); $sql SELECT * FROM users WHERE username $filtered_username AND password $filtered_password; $result mysqli_query($conn, $sql);题目要求在已知存在SQL注入漏洞的前提下绕过对注释符--,#,/*,*/的过滤实现未授权登录并获取数据库信息如版本、当前数据库名。2.2 思路解析为什么注释符如此重要在标准的SQL注入流程中注释符扮演着“终结者”的角色。当我们进行联合查询Union Injection时通常的payload结构是 UNION SELECT 1,2,3 --这里的--后面有个空格用于注释掉原始查询中之后的所有内容特别是可能存在的AND password...部分使得我们注入的UNION SELECT语句能够正确执行而不引发语法错误。当--和#被过滤后这个经典的payload就会变成 UNION SELECT 1,2,3这会导致SQL语句变成SELECT * FROM users WHERE username UNION SELECT 1,2,3 AND password ...。多出来的那个单引号无法被配对从而引发语法错误注入失败。所以绕过过滤的核心思路就变成了在不使用注释符的情况下构造一个语法完全正确的SQL语句。我们需要让注入点之后的部分原查询的剩余部分要么被“消化”掉要么融入我们构造的合法语法中。2.3 绕过方案选型逻辑与闭合优先面对注释符过滤主要有两种高阶绕过思路利用逻辑运算符与括号闭合这是最优雅、最通用的方法。通过AND、OR和括号()来操纵查询逻辑并确保所有引号都被正确闭合使整个SQL语句的语法保持完整。利用行内注释的变体在某些数据库如MySQL中注释符--和#其实有“替代品”比如--或--注意--后必须跟一个空格或控制字符。但题目明确过滤了--字符串所以这种简单的变体可能失效除非过滤逻辑有缺陷如未考虑URL编码%20或。我们优先考虑更可靠的方案一。我们的攻击链将围绕“逻辑闭合”展开目标是最终执行一个联合查询泄露信息。3. 手工注入实战步步为营绕过过滤我们假设目标URL是http://target.com/login.php采用POST方式提交数据。我们将使用Burp Suite的Repeater模块进行手动测试这能让你清晰地看到每一个请求和响应。3.1 第一步确认注入点与过滤规则首先我们需要验证注入点是否存在以及过滤规则是否严格。发送一个正常登录请求观察响应。尝试基础注入在用户名处输入admin。如果返回了数据库错误信息如MySQL语法错误说明可能存在注入且单引号未被过滤。测试过滤规则输入admin--。观察--是否被移除。如果返回的错误信息中--消失了或者错误与输入admin时不同说明过滤生效。同样测试#和/*。注意在实际CTF或渗透测试中错误信息可能被屏蔽。此时需要通过观察页面回显的差异、响应时间盲注或使用条件语句如 AND 11与 AND 12来判断。本题为了方便讲解假设有详细错误回显。3.2 第二步构造不使用注释符的永真条件既然不能注释掉后面的AND password...我们就让它变成一个永远成立的条件从而“无害化”。 原始SQL... WHERE username $input1 AND password $input2我们可以在用户名字段注入admin OR 11那么拼接后的SQL变为SELECT * FROM users WHERE username admin OR 11 AND password $input2根据SQL运算符优先级AND优先级高于OR。所以这句等价于SELECT * FROM users WHERE (username admin) OR (11 AND password $input2)由于11恒真所以11 AND password $input2这个子句的结果完全取决于password $input2。这并没有完全绕过密码检查。我们需要用括号()来改变优先级构造一个独立的永真条件 注入用户名admin) OR (11拼接后的SQLSELECT * FROM users WHERE username admin) OR (11 AND password $input2)现在OR连接了两个条件条件A是username admin)这会导致语法错误因为括号不匹配。我们需要让第一个条件也闭合。正确的构造是让我们的注入部分完整闭合username的引号和括号然后开始新的逻辑 注入用户名 OR 11 AND username LIKE %这看起来复杂其实有更清晰的思路我们直接瞄准WHERE子句的整体结构。更优解我们同时控制用户名和密码字段。用户名admin OR 11 --(假设过滤这个会被破坏)改为admin OR 11密码任意值比如testSQL变为SELECT * FROM users WHERE username admin OR 11 AND password test还是优先级问题。我们需要在密码字段也做手脚来配合完成一个永真逻辑。 让我们换一种攻击方式联合查询注入。这才是我们泄露数据的关键。但联合查询需要处理后面的语句所以我们先解决“闭合”问题。3.3 第三步为联合查询构造完美闭合这是本次实战的核心技巧。我们不再试图注释而是通过精心构造让原查询的剩余部分成为我们注入语句的一部分或者将其转化为一个无影响的子查询。思路原始查询是WHERE username$user AND password$pass。 我们注入的目标是将其变为WHERE username [注入的联合查询] AND 11这样AND password$pass就变成了AND 11的一部分而11恒真相当于被“中和”了。具体构造在用户名字段输入 UNION SELECT 1,2,3 AND 11在密码字段输入(一个单引号)让我们看看拼接后的SQL语句SELECT * FROM users WHERE username UNION SELECT 1,2,3 AND 11 AND password 分析一下username 这部分闭合了。然后接上了我们的UNION SELECT 1,2,3。关键来了后面原本的AND password$pass变成了AND 11 AND password 。这里有个问题UNION SELECT 1,2,3 AND 11这个AND属于UNION SELECT的一部分吗语法上是错误的。UNION前后必须是完整的SELECT语句。所以我们需要确保UNION SELECT是一个完整的语句。那么原查询AND password...这部分必须被处理掉。既然不能注释我们就让它变成一个永远为真的条件并且属于整个WHERE子句而不是UNION的一部分。正确构造用户名 UNION SELECT 1,2,3 FROM DUAL WHERE 11密码(一个单引号)拼接后的SQLSELECT * FROM users WHERE username UNION SELECT 1,2,3 FROM DUAL WHERE 11 AND password DUAL是MySQL中的一个虚拟表。现在语法是主查询SELECT * FROM users WHERE username UNION注入的查询SELECT 1,2,3 FROM DUAL WHERE 11 AND password 注入的查询中WHERE 11恒真所以AND password 这个条件决定了该行是否被选中。由于我们密码输入了一个单引号password 意味着“密码等于一个单引号字符”这基本不可能成立所以整个注入的SELECT语句结果为空集。这不行我们需要它返回数据。我们需要让WHERE子句整体为真。那么让password 这个条件也恒真即可。怎么让一个单引号等于一个单引号用字符串连接或运算。最终有效Payload用户名 UNION SELECT 1,2,3 FROM DUAL WHERE 11密码||1拼接后的SQLSELECT * FROM users WHERE username UNION SELECT 1,2,3 FROM DUAL WHERE 11 AND password ||1在MySQL中||1的结果是字符串1||在某些SQL模式下是逻辑OR在ANSI模式下是字符串连接这里我们假设是字符串连接函数CONCAT的替代。但更通用、更可靠的方法是使用更通用的闭合技巧 我们回到最初的想法让AND password$pass整体变成一个真值表达式。 观察AND password$pass需要被闭合。如果我们让$pass的值是 OR 11会怎样用户名admin UNION SELECT 1,2,3密码 OR 11拼接SQLSELECT * FROM users WHERE username admin UNION SELECT 1,2,3 AND password OR 11这又出现了语法错误因为UNION SELECT 1,2,3这个单引号多余了。问题出在用户名注入点没有闭合。我们需要在用户名处就闭合掉username字段的引号并且“吞掉”后面AND开始的部分。经典手法是利用一个子查询或者将后面部分转化为一个恒真条件的一部分。经过反复测试一个在注释符过滤下非常稳定的Payload构造如下 我们放弃在密码字段做复杂操作专注于在用户名字段构造一个自闭合的注入Payload。核心Payload OR 11 UNION SELECT 1,2,3 FROM (SELECT 1)a WHERE 11用户名输入 OR 11 UNION SELECT 1,2,3 FROM (SELECT 1)a WHERE 11密码输入任意值比如test。拼接后的SQL语句假设密码为testSELECT * FROM users WHERE username OR 11 UNION SELECT 1,2,3 FROM (SELECT 1)a WHERE 11 AND password test现在我们来解析这个“魔法”般的语句username 是初始条件。OR 11使得前面部分恒真但由于后面有UNION这里其实返回了users表中所有满足11即所有的行。UNION SELECT 1,2,3 FROM (SELECT 1)a WHERE 11这是我们精心构造的注入查询。FROM (SELECT 1)a创建了一个别名a的临时派生表里面有一行一列数据1。这是一种常见的构造合法FROM子句的技巧。WHERE 11是一个恒真条件确保这个UNION SELECT返回数据(1,2,3)。最关键的部分来了原查询中AND password test这个条件现在被“附加”到了我们注入的UNION SELECT语句的WHERE子句之后。完整的WHERE子句变成了WHERE 11 AND password test。对于UNION SELECT的结果集password字段根本不存在我们SELECT的是1,2,3所以password test这个条件对UNION的结果集而言在逻辑上通常会被评估为TRUE因为涉及未知列行为取决于数据库但许多情况下会返回结果或者更稳妥地说我们通过WHERE 11已经保证了返回。而主查询部分OR 11因为返回了数据也会显示。在实际测试中这种构造能有效绕过简单的注释符过滤并执行联合查询。你可能需要根据实际情况调整SELECT的列数通过ORDER BY探测和FROM子句。3.4 第四步信息收集与数据获取一旦联合查询成功执行页面会回显UNION SELECT的结果通常是数字1,2,3。我们需要确定回显点。确定列数使用ORDER BY。由于注释符过滤我们需要闭合。Payload OR 11 ORDER BY 5 --这里--仅用于说明实际需用上述闭合技巧替代例如 OR 11 ORDER BY 5 AND 11。不断递增数字直到报错即可确定列数。假设确定为3列。确定回显点使用类似之前的闭合Payload将UNION SELECT后的数字替换为可识别的字符串。例如 OR 11 UNION SELECT a,b,c FROM (SELECT 1)a WHERE 11观察页面哪个位置出现了ab或c。获取数据库信息假设回显点是第2和第3列。获取数据库版本和当前数据库名 OR 11 UNION SELECT 1,version(),database() FROM (SELECT 1)a WHERE 11获取所有数据库名需要查询information_schema.schemata OR 11 UNION SELECT 1,2,group_concat(schema_name) FROM information_schema.schemata WHERE 11注意这里WHERE 11是我们注入的UNION SELECT自带的恒真条件它和后面原查询附加的AND password...共同组成了完整的WHERE条件。对于查询information_schema这个条件通常不影响结果。获取表名、列名、数据遵循标准的联合查询注入流程使用information_schema.tables和information_schema.columns。关键在于确保整个Payload的语法正确闭合。4. 自动化工具辅助与防御思考4.1 使用SQLMap进行辅助测试在手工理清绕过思路后我们可以用SQLMap进行验证和快速利用。但面对过滤需要给SQLMap一些提示。使用--tamper脚本SQLMap自带很多绕过脚本如space2comment、equaltolike等。但对于自定义的注释符过滤可能需要我们自己编写简单的tamper脚本或者使用charencode等。指定注入点与过滤我们可以通过Burp抓取登录请求保存为login.txt文件然后运行sqlmap -r login.txt --level 3 --risk 2 --tamperspace2comment,charencode --dbmsmysql--level和--risk调高以尝试更多测试。手动提供绕过Payload如果SQLMap无法自动识别我们可以通过--prefix和--suffix参数手动指定Payload的前缀和后缀来模拟我们手工构造的闭合。sqlmap -r login.txt --prefix OR 11 --suffix AND 11 -p username这告诉SQLMap在测试Payload时会在我们提供的username参数值前后加上这些字符串来构造完整语句。实操心得不要过度依赖自动化工具。在像CISP-PTE这样的考试或复杂WAF环境中手工构造和理解Payload的能力至关重要。SQLMap是一个强大的验证和利用工具但思路的突破往往来自于手动分析。4.2 从攻击看防御如何真正堵住漏洞我们绕过了注释符过滤这本身就说明了这种黑名单过滤方式的脆弱性。真正的防御应该基于以下原则使用参数化查询预编译语句这是根治SQL注入的银弹。将SQL语句与数据完全分离数据库引擎不会将输入的内容解析为SQL指令。在PHP中使用PDO或MySQLi的预处理功能。$stmt $conn-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-bind_param(ss, $username, $password); $stmt-execute();输入验证与白名单对于已知固定范围的数据如状态码、类型使用白名单验证。对于字符串定义严格的字符集如仅允许字母数字。最小权限原则数据库连接账户不应具有DROP、FILE等高级权限仅赋予其应用所需的最小权限。避免详细的错误信息在生产环境中禁止向用户显示数据库原始错误信息使用自定义的统一错误页面。Web应用防火墙WAF部署WAF可以帮助拦截已知的攻击模式但不能作为唯一防线。5. 常见问题与排查技巧实录在实战和教学过程中我总结了一些高频问题和解决技巧问题1Payload执行后页面无变化或返回空白/错误。排查思路检查闭合符号单引号是最常见的但有时可能是双引号或括号)。尝试admin\、admin)等。检查过滤规则是否还有其他字符被过滤如AND、OR、SELECT、UNION等关键字。尝试大小写混淆UnIoN、双写SELSELECTECT、或使用等价函数/符号如代替AND||代替ORLIKE代替。检查列数UNION查询前后列数必须一致。务必用ORDER BY精确确认。检查数据库类型Payload语法因数据库而异MySQL, PostgreSQL, SQL Server等。观察错误信息或通过简单函数判断如version()MySQL、versionSQL Server。问题2知道是盲注但无法确定注入是否成功。技巧使用条件响应差异。例如基于时间的盲注 OR IF(11,SLEEP(2),0) AND 11。观察页面响应是否延迟2秒。基于布尔值的盲注 OR LENGTH(database())1 AND 11通过页面内容的不同如图片显示/不显示文字存在/不存在来判断条件真伪。问题3在构造复杂Payload时总是出现语法错误。技巧本地模拟调试。在本地安装MySQL或使用在线SQL验证工具将你构造的完整SQL语句粘贴进去执行看报错信息。这是最快定位语法问题的方法。例如对于Payload OR 11 UNION SELECT 1,2,3 FROM (SELECT 1)a WHERE 11 AND password test‘你可以单独在数据库客户端测试SELECT 1,2,3 FROM (SELECT 1)a WHERE 11 AND password test这个子查询是否合法。问题4遇到更严格的过滤比如关键字被完全剥离。进阶绕过编码绕过尝试URL编码、十六进制编码、Unicode编码。例如SELECT可以写成SEL%45CT%45是E的URL编码或0x53454c454354十六进制。注释符内联利用/*!50000SELECT*/这种MySQL特有的内联注释其中的代码只有在MySQL版本大于等于5.00.00时才会执行可以用来绕过一些简单的关键字检测。等价替换AND-OR-||-LIKE-GREATEST()等。一个实用的速查表问题现象可能原因排查/尝试方案输入单引号后报错消失可能不是字符型注入或是数字型尝试id1 and 11与id1 and 12UNION查询不显示数据列数不对或回显位置不对用ORDER BY确定列数用NULL代替部分列测试空格被过滤导致关键字连在一起使用注释/**/代替空格或使用括号()、加号URL中单引号被转义输入\被转义尝试宽字节注入如GBK编码下%df%27或寻找数字型注入点所有Payload都返回相同页面可能被重定向到错误页或是盲注尝试时间盲注SLEEP()或观察细微的响应差异如响应头、响应时间绕过注释符过滤只是SQL注入攻防世界中的一个小关卡。它考验的是对SQL语句语法结构的深刻理解而非记忆几个Payload。真正的安全工程师需要具备这种“在限制中构造攻击”的思维能力。希望这篇从真题出发的深度解析能帮你打通任督二脉。下次再遇到类似的过滤不妨静下心来画一画SQL语句的结构图想一想如何用合法的语法“包裹”或“消化”掉那些不受你控制的部分。这种思维训练其价值远大于通过某一次考试。