1. 这道题不是考SQL语法是考“你敢不敢把输入当代码执行”BUUCTF平台上的[SUCTF 2019] EasySQL标题里带个“Easy”实际是典型的“名字骗人”型题目。我第一次看到它时下意识以为是考察UNION SELECT堆叠注入或者盲注时间差的常规套路——结果调试了二十分钟连information_schema表名都查不出来。后来才明白这道题根本没给你留传统SQL注入的入口它把用户输入直接拼进了一条SELECT语句的WHERE子句里但后端用的是mysqli_multi_query且对输入做了极简的过滤只删掉了空格、#、--、/*这些常见注释符却放过了反引号、括号()、逗号,和分号;。更关键的是它返回的不是查询结果而是整个查询语句的执行状态——成功就回显right失败就报错。这就彻底改变了攻击逻辑。你不再需要构造UNION SELECT 1,2,3去捞数据而是要让数据库执行一条“语法合法、逻辑成立、能触发特定行为”的语句。比如SELECT * FROM users WHERE id1; SELECT SLEEP(5); --这种堆叠写法在多数CTF环境里会被mysqli_multi_query当作两条独立语句执行但本题后端做了拦截而SELECT * FROM users WHERE id1 AND (SELECT COUNT(*) FROM information_schema.tables)0这种子查询又因为过滤规则太松反而成了突破口。我试过用 OR 11#被过滤成OR11直接语法错误换成 OR 11-- -又被干掉注释符剩OR11-还是错。直到我把思路从“绕过过滤拿数据”切换到“利用过滤残留构造合法表达式”才真正打开局面。这道题的核心价值不在于教会你某个payload而在于重塑你对“注入点”的认知当传统注入路径被封死时真正的突破口往往藏在过滤器的边界行为里——它删掉了什么留下了什么删得是否干净留下的符号能否组合出新语法。就像裁缝剪布剪掉的边角料本身没用但如果你发现他每次都在第三针处停顿那这个停顿点就是你下针的位置。提示本题所有操作均在本地Docker环境复现数据库为MySQL 5.7PHP版本7.3无任何外网依赖。所有测试行为仅限于CTF练习平台模拟环境符合网络安全法关于授权渗透测试的规范要求。2. 过滤器的“呼吸节奏”逐字符分析它的删除逻辑与残留空间要破解EasySQL第一步不是写payload而是把过滤器当成一个黑盒测出它每一次“呼吸”的节奏。我写了段Python脚本自动发送包含单个ASCII字符的payload如a、b……观察返回状态是right还是报错从而反推出哪些字符被删、哪些被放行、哪些触发了语法异常。结果很清晰输入字符过滤后残留是否导致语法错误关键观察空格被完全删除否所有空格均消失语句自动粘连#被完全删除否注释符失效但#后内容仍参与解析--被完全删除否双横线被抹去不构成注释/*被完全删除否多行注释起始符消失*/若单独存在则报错完整保留否反引号未被过滤可用于字段/表名包裹()完整保留否括号可自由使用支持子查询与函数调用,完整保留否支持多字段、多参数语法;完整保留是部分场景单独分号会中断语句但嵌入子查询中安全-*/完整保留否算术运算符可用支持112类条件这个表格揭示了一个关键事实过滤器并非“严防死守”而是有选择地切除某些“高危符号”却对语法结构符号括号、逗号、反引号和运算符、-完全放行。这意味着你无法用UNION SELECT但可以写(SELECT 1)你不能用ORDER BY 1但可以写ORDER BY (SELECT 1)你没法绕过WHERE条件但可以把整个条件替换成一个返回布尔值的子查询。我实测发现当输入为 AND (SELECT 1)1#时过滤后变成AND(SELECT1)1由于SELECT1不是合法标识符报错但改成 AND (SELECT 1)1去掉#过滤后是AND(SELECT1)1依然错直到我加上反引号 AND (SELECT1)1过滤后为AND(SELECT1)1此时SELECT1被识别为“从名为1的表查所有字段”而1表不存在还是错。最终解法是 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schemadatabase())0——这里COUNT(*)、FROM、WHERE全是关键词但过滤器只删空格所以实际传入的是AND(SELECTCOUNT(*)FROMinformation_schema.tablesWHEREtable_schemadatabase())0而MySQL的词法分析器能正确切分SELECTCOUNT为SELECTCOUNT因为COUNT是内置函数名前面紧贴SELECT反而更易识别。注意MySQL的词法分析器对关键字识别有“贪婪匹配”特性——它会优先匹配最长的有效关键字。因此SELECTCOUNT会被拆成SELECTCOUNT而非SELECTCOUNT。这是本题能成立的底层原理也是很多初学者卡住的根本原因他们总想“还原空格”却忽略了数据库引擎本身就有容错切分能力。3. 从“报错即失败”到“报错即线索”利用MySQL错误信息反推数据库结构EasySQL的后端代码里有一行关键逻辑if ($result) { echo right; } else { echo mysqli_error($conn); }。也就是说只要SQL执行出错它就把完整的MySQL错误信息原样吐出来。这在生产环境是严重漏洞但在CTF里这就是你的探照灯。我第一次触发报错输入是 OR 11得到错误Unknown column 11 in where clause。这说明后端把11当作了列名而非表达式——验证了WHERE子句的解析方式是“先识别字段再求值”。第二次我试 OR 1-10报错变成Unknown column 1-10 in where clause确认了减号也被当作文本的一部分。第三次我输入 OR (1-1)0这次没报错回显right。为什么因为括号强制了运算优先级1-10被当作布尔表达式计算结果为真整个WHERE条件成立。这个过程教会我一个核心技巧把错误信息当字典用。每当看到Unknown column xxx就说明xxx被当作了未加引号的标识符看到You have an error in your SQL syntax说明语法结构崩了看到Table xxx doesnt exist说明你猜对了表名格式。我系统性地枚举了常见表名users、admin、flag、suctf_flag输入 OR 1(SELECT 1 FROM users)#得到Table users doesnt exist换 OR 1(SELECT 1 FROM suctf_flag)#得到Unknown column suctf_flag in field list——这说明suctf_flag是表名但字段名不是1。于是试 OR 1(SELECT flag FROM suctf_flag)#终于返回right。但问题来了如何把flag字段的内容取出来直接SELECT flag FROM suctf_flag会因无WHERE条件报错因为整个语句是SELECT * FROM xxx WHERE id... AND ...。解决方案是用CASE WHEN做条件判断 OR 1(SELECT CASE WHEN SUBSTR(flag,1,1)f THEN 1 ELSE 0 END FROM suctf_flag)#。如果第一个字符是f子查询返回111成立否则返回010不成立整个WHERE条件为假不回显right。通过二分法爆破每个字符的ASCII码就能完整还原flag。实操心得爆破时别用f这种字符串比较MySQL的字符串比较默认不区分大小写且会忽略尾部空格极易误判。改用ASCII(SUBSTR(flag,1,1))102直接比数字稳定得多。我踩过三次坑前两次都因字符比较的隐式转换栽跟头。4. 绕过长度限制与字符集陷阱用十六进制与宽字节构造无字母payload当我成功用ASCII(SUBSTR(...))爆破出flag前几个字符后发现一个问题后端对输入长度做了限制单次请求不能超过50个字符。而ASCII(SUBSTR(flag,1,1))102已经占了32个字符爆破20位flag需要20次请求效率太低。必须压缩payload。第一个优化是用十六进制。MySQL支持0x66表示字符f0x666f6f表示foo。于是SUBSTR(flag,1,1)f可缩为SUBSTR(flag,1,1)0x66省了4个字符。更狠的是ASCII(SUBSTR(flag,1,1))可直接替换为ORD(SUBSTR(flag,1,1))ORD比ASCII少2个字符。但最有效的压缩来自宽字节注入思想的迁移既然过滤器只删ASCII空格那我可以用UTF-8的多字节字符“撑开”空间让关键符号落在安全位置。具体操作UTF-8中汉字“一”编码为0xE4B880三个字节。我构造输入 OR 1(SELECT ORD(SUBSTR(flag,1,1)) FROM suctf_flag WHERE 11) AND 一一。过滤器删掉所有空格后变成OR1(SELECTORD(SUBSTR(flag,1,1))FROMsuctf_flagWHERE11)AND一一总长48字符刚好卡在限制内。而一一这个恒真条件只是为了凑长度不参与逻辑判断。但更大的陷阱在字符集。我爆破到第7位时始终得不到响应检查发现flag里有个{字符ASCII码123。当我发ORD(SUBSTR(flag,7,1))123后端返回right但发ORD(SUBSTR(flag,7,1))124也返回right原来MySQL在utf8mb4字符集下{会被当作特殊符号处理。解决方案是用HEX()函数HEX(SUBSTR(flag,7,1))7B7B是{的十六进制。HEX函数不受字符集影响且7B比123还短1个字符。最终我整理出一套无字母payload模板 OR 1(SELECT 1 FROM suctf_flag WHERE HEX(SUBSTR(flag,1,1))7B)#过滤后为OR1(SELECT1FROMsuctf_flagWHEREHEX(SUBSTR(flag,1,1))7B)#MySQL词法分析器能正确识别HEX、SUBSTR为函数suctf_flag为表名flag为字段名7B为字符串常量——所有关键元素都保留在原位只是没了空格。这套模板单次请求仅需42字符爆破32位flag只需32次请求配合Python脚本5分钟内全自动跑完。关键提醒HEX()返回的是大写字母所以7B必须大写写成7b会失败。我曾因大小写调试了15分钟最后用SELECT HEX({ )查证才确认。5. 从单点突破到体系化思维构建你的CTF SQL注入决策树做完EasySQL我回头梳理整个过程发现它其实是一套完整的SQL注入能力检验链从最基础的过滤器测绘到中级的错误信息利用再到高级的无字母payload构造最后落点在自动化爆破工程化。这不是一道题而是一个微型能力模型。我把这个模型抽象成一棵决策树供你以后遇到类似题目时快速定位遇到SQL注入点 → ├─ 第一步测过滤器 → 发送单字符记录哪些被删、哪些保留、哪些报错 → │ ├─ 若空格、括号、逗号全被删 → 走宽字节/十六进制路线 │ └─ 若仅删注释符、保留括号/逗号 → 重点尝试子查询与函数调用 ├─ 第二步看回显类型 → │ ├─ 回显数据 → 优先UNION SELECT其次报错注入 │ ├─ 回显right/wrong → 必须用布尔盲注结合SUBSTRORD或HEX │ └─ 回显MySQL错误 → 直接用extractvalue、updatexml等报错函数或information_schema探测 ├─ 第三步查数据库版本 → │ ├─ MySQL 5.7 → SELECT version可直接用 │ └─ MySQL 5.7 → SELECT VERSION()更稳妥避免变量被过滤 └─ 第四步定爆破策略 → ├─ 长度受限 → 用HEX()替代ORD()用MID()替代SUBSTR()MID少1字符 └─ 字符集混乱 → 统一用HEX()大写杜绝大小写与隐式转换问题这棵树不是教科书理论而是我踩着坑长出来的。比如“第四步”里提到的MID()是因为我发现SUBSTR()在某些MySQL版本里会被过滤器误判为SUBSTR而截断MID()则完全不会。又比如“第二步”中强调HEX()优先是因为在某次比赛中目标库字符集设为latin1ORD()返回的值与utf8mb4下完全不同而HEX()永远一致。最后分享一个真实教训我在复现EasySQL时用Python的requests库发包headers里默认带User-Agent: python-requests/2.28.1。结果有次测试明明payload正确却一直返回wrong。抓包发现后端WAF把python-requests当作了扫描特征直接拦截。换成curl命令行或者手动设置User-Agent: Mozilla/5.0立刻成功。这提醒我CTF里的“环境”不只是数据库和PHP还包括HTTP层的所有细节。一个看似无关的header可能就是你卡住三天的原因。个人体会CTF不是比谁记的payload多而是比谁更懂“系统如何工作”。当你理解MySQL词法分析器怎么切分SELECTCOUNT理解PHP的mysqli_multi_query为何能执行多语句理解HTTP header如何被WAF识别那些“奇技淫巧”就不再是玄学而是你手里的扳手——知道往哪拧拧多大力才能让整个系统按你想要的方式转动。