1. 当常规SQL注入失效时遇到一个过滤了union、select、where等关键词的注入点就像拿着万能钥匙却发现锁芯被焊死。去年打CTF时就碰到这么一道题——CISCN2019华北赛区的Web1 Hack World。题目页面只有一个简单的输入框提示flag藏在数据库里但用传统注入手法测试时不是报错就是返回空白页。尝试了最常见的单引号测试id1--页面返回了数据库错误说明存在注入点。但当我把这个注入点丢给sqlmap时工具跑了几分钟就卡住了——因为题目用正则表达式过滤了几乎所有SQL关键字。这时候就需要跳出常规思维从计算机底层逻辑里找突破口。2. 异或运算的魔法在二进制世界里异或运算^有个神奇特性两个相同数字异或结果为0不同则为1。比如1 ^ 1 0 # 相同为0 1 ^ 0 1 # 不同为1这个特性可以被我们用来构造布尔判断。当我在Hack World题目中输入id1^1服务器返回了Hello, glzjin wants a girlfriend.这个固定提示而输入id1^0则返回了正常查询结果。这说明服务器在执行我们的异或运算且运算结果决定了返回内容3. 构建布尔盲注条件利用这个特性我们可以构造这样的探测语句id0^(length(database())10)这里发生了三个关键操作先执行length(database())获取数据库名长度将结果与10比较得到True/False用0与这个布尔值进行异或运算由于True在MySQL中被视为1False视为0所以当条件成立时0^11返回id1的结果条件不成立时0^00返回id0的结果通过观察页面返回内容的变化我们就能判断条件是否成立。这就是基于异或的布尔盲注的核心原理。4. 折半查找实战确定了原理后我用了经典的折半查找算法来高效推测数据。以获取数据库名长度为例先测大范围id0^(length(database())16) # 返回False id0^(length(database())8) # 返回True逐步缩小范围id0^(length(database())12) # 返回False id0^(length(database())10) # 返回True id0^(length(database())11) # 返回False最终确定id0^(length(database())11) # 返回True整个过程就像玩猜数字游戏每次都能排除一半的可能性。用同样的方法我们可以逐字符爆破数据库名id0^(ascii(substr(database(),1,1))100) # 测试第一个字符的ASCII码 id0^(ascii(substr(database(),1,1))99) # 确认第一个字符是c5. 直接获取flag的捷径在实战中我发现可以直接爆破flag表省去了猜表名的步骤。构造语句id0^(ascii(substr((select(flag)from(flag)),1,1))50)这里有几个绕过技巧用括号代替空格select(flag)等效于select flag避免使用where子句直接from(flag)用substr逐字符测试flag值最终通过几百次请求我们就能像拼图一样把完整的flag拼接出来flag{bcc3af9d-a5ee-4395-a540-ffb516eaba15}6. 防御与思考这种注入手法的关键在于利用数据库的布尔运算特性完全避开被过滤的关键词通过数学运算实现条件判断作为防御方应该过滤所有特殊符号包括^、()等使用预编译语句限制错误信息返回这道题给我最大的启示是真正的安全研究需要理解计算机底层逻辑而不仅仅是工具使用。当你掌握了原理任何防御规则都可能找到突破口。