资讯详情

sqli-labs Less-48实战:ORDER BY子句盲注与排序侧信道利用

📅 2026/10/9 6:02:16 | 华诺云谱 👁 阅读
sqli-labs Less-48实战:ORDER BY子句盲注与排序侧信道利用
sqli-labs这套靶场玩到Less-48这一关很多人的感受是终于要把“联合注入”和“报错注入”的惯性思维放下开始直面ORDER BY子句的注入问题了。Less-48在关卡列表里的定位是“GET - Blind based on Order By Clause - Numeric - Sort Display”翻译过来就是基于排序子句的数值型盲注并且页面会把排序后的结果展示出来。也就是说这一关在考你一件事在没有报错回显、没有联合查询余地的情况下怎么靠排序顺序的变化把数据库里的“家底”一点点翻出来。对于刚刷完前面几十关的初学者来说Less-48最别扭的地方在于前面学的UNION SELECT、extractvalue报错等招数在这里都不太好使。这一关也是从Less-46开始的ORDER BY注入系列十连关中的一环和后面的Less-49、Less-50、Less-51、Less-52、Less-53紧密相关。把这个系列吃透你对MySQL中ORDER BY子句能插进去什么、不能插进去什么会有一个非常清醒的认识。我也建议大家把Less-46到Less-53放在一起刷因为它们的核心矛盾其实是同一个只是每次换了闭合方式和过滤条件而已。1. 关卡背景与核心思路拆解1.1 Less-48在sqli-labs关卡链条中的定位sqli-labs从Less-1到Less-65划分了好几个主题段。前面1到10关是基础联合查询11到22关是POST注入23关开始专门折腾被注释符和过滤绕过的场景而Less-46到53这一整段主题是ORDER BY子句注入。为什么单列一段因为ORDER BY在SQL语句里的位置太特殊——它在一个查询的最后UNION只能出现在它的前面这就把前面练得最熟的“联合查询注入”直接废掉了必须换一套打法。Less-48在系列里的具体设定是参数名为sort通过GET传递数值型注入闭合方式为无引号闭合页面不会输出SQL报错信息但会把排序后的用户列表展示出来。和Less-46相比Less-46同样是数值型ORDER BY注入但没有排序结果展示判断条件真伪只能靠页面是否显示数据而Less-48加上了“排序显示”这个特性相当于给了你一个侧信道——通过观察记录排列顺序来判断条件真假。和Less-47、Less-49这类单引号闭合的关卡相比Less-48又省掉了闭合判断的步骤可以直接拼数字。我整理了一张这个系列的小对照表方便你理解Less-48所处的位置关卡闭合方式是否报错回显排序结果是否展示Less-46数值型否否Less-47单引号否否Less-48数值型否是Less-49单引号否是Less-50数值型是否Less-51单引号是否Less-52数值型否否Less-53单引号否否从这个表能看出来Less-48其实是“排序可见的盲注”这个分支里最简单的一个数字型、无过滤、有排序展示。它比Less-46多了一个观测窗口又比Less-49少了单引号的干扰。所以我的建议是Less-46和Less-48作为ORDER BY注入的入门关先把这里的盲注逻辑跑通再去折腾带引号的变体。1.2 为什么ORDER BY注入值得单独研究很多人刚开始学SQL注入的时候注意力都集中在SELECT、WHERE、JOIN这些位置ORDER BY往往被当成“排序规则”一笔带过。但实际业务里排序几乎无处不在商品列表按价格排序、排行榜按得分排序、后台数据表格按时间排序。只要排序字段来自用户输入且没校验就存在注入可能。ORDER BY注入的麻烦点在于注入点在一个查询的最后UNION的语法决定了你没法在ORDER BY后面拼一个SELECT。举个例子假设后台查询是SELECT * FROM users ORDER BY id如果我在id后面直接拼UNION SELECT 1,2,3最终变成SELECT * FROM users ORDER BY id UNION SELECT 1,2,3这在MySQL里是语法错误因为UNION必须出现在ORDER BY之前。这就是为什么很多人前面刷得顺手到这个系列突然卡住——过去的经验在这条语法规则面前完全失效。那ORDER BY后面究竟能做什么深入看MySQL的语法ORDER BY后面其实可以接列名、列别名、列位置序号比如ORDER BY 1表示按第一列排序、任意表达式比如ORDER BY IF(condition, 1, 2)、函数调用比如ORDER BY SLEEP(5)。这给了注入者两个方向一是用表达式改变排序键实现布尔盲注二是用延迟函数实现时间盲注。这也正是Less-48这关要练的核心能力。2. 核心原理与判断方法2.1 排序键是怎么被用户输入控制的在Less-48的源码里关键查询大致长这样$sql SELECT * FROM users ORDER BY $id;其中$id就是接收自GET参数sort的值。由于没有加引号、没有做过滤我的输入会直接拼接到ORDER BY之后的SQL语句中。要理解后续的盲注方式得先明白ORDER BY后参数的三种身份。第一种列位置序号。ORDER BY 1按查询结果的第一列排序ORDER BY 2按第二列排序以此类推。这个特性常用来探测“当前查询有几列”不过在这个关卡里意义不大。第二种列名。ORDER BY username会按username列排序。如果列名不存在MySQL会报错但在Less-48中报错不回显所以你看到的只是页面数据消失或不变。第三种表达式。ORDER BY IF(条件, 1, 2)会先计算IF函数结果为1就按第一列排序为2就按第二列排序。这就把“SQL里的逻辑真假”翻译成了“页面里数据的排列顺序”。这是Less-48破解的核心。实操测试的时候第一步永远是给sort传1、2、3三个值观察页面数据排列有没有变化。如果sort1和sort2结果不同说明参数确实作用于排序如果sort1和sort1没有任何区别再尝试sort1%27单引号看是否变成字符型闭合。Less-48是数值型所以sort直接传数字即可。2.2 布尔盲注靠排序差异说话布尔盲注的核心是把一个条件表达式塞进IF里面让条件真假体现为不同的排序键。常见的payload模板是sortIF(条件, 1, 2)当条件为真时排序键为1数据会按照第一列的字母序或数字序排列当条件为假时排序键为2数据会换成第二列的排列顺序。你只需要记录两三次请求后页面首条数据的差异就能反推出条件真假。举个最简单的验证访问sortIF(11,1,2)因为11恒真等价于按第一列排序再访问sortIF(12,1,2)12恒假等价于按第二列排序。如果两次页面返回的数据顺序不同说明这个payload结构有效。此后把“条件”替换成任何你想判断的SQL子查询即可。比较条件有很多种写法判断长度sortIF(length(database())N,1,2)判断字符范围sortIF(ascii(substr(database(),1,1))100,1,2)判断表内容sortIF(ascii(substr((select table_name from information_schema.tables where table_schemadatabase() limit 0,1),1,1))100,1,2)这些都是可以一笔一划把数据库信息“描”出来的布尔盲注标准姿势。2.3 时间盲注当排序差异判断不了时理论上布尔盲注在Less-48中已经够了。但在某些场景下比如排序结果展示关闭、或者排序键变化不明显你还可以走时间盲注这条路。ORDER BY后面直接接SLEEP函数是完全合法的。典型的payload是sortIF(条件, SLEEP(3), 1)条件为真时MySQL会停顿3秒再返回页面条件为假时立即返回。用页面响应时间差来判断条件真假。如果数据库用户对sleep函数没有权限限制这会非常稳定。我做测试的时候一般会把延迟设成3到5秒太短可能被网络波动干扰太长则测试效率低。Less-48这一关我实测过布尔盲注和时间盲注都能打通。布尔盲注效率更高因为排序是比较出来的时间盲注适合写自动化脚本时统一处理因为脚本只需要记录响应时间不需要解析页面内容的语义。2.4 报错注入在这个关卡能不能用先给结论Less-48的报错注入大多数情况下不可用原因在源码里对SQL错误的处理是静默的也就是mysqli查询失败后不会把错误信息打印到页面。extractvalue和updatexml这类报错注入依赖SQL错误信息回显到客户端既然页面不吃报错那这条路自然走不通。不过有个细节值得注意Less-48的排序结果是正常的查询输出。也就是说只要查询不报错哪怕你用了extractvalue(1,concat(0x7e,database()))这种写法数据库返回的还是排序后的记录报错本身不会让你看到任何额外信息。所以在这个关卡里遇到报错注入“失灵”别怀疑自己路子错了这是关卡设计给的限制老老实实用盲注才是正解。3. 实操过程与关键payload详解3.1 环境准备与初始探测实操之前先把环境准备好。sqli-labs是基于PHPMySQL的靶场常用搭建方式就是本地跑一个Apache或Nginx配合MySQL 5.x。我这边用的小皮面板phpstudy带的Apache和MySQL 5.7PHP版本用的7.x跑sqli-labs没有任何问题。把源码放进网站根目录导入sql-lab.sql建好库表访问sqli-labs目录就能看到关卡列表。进入Less-48后先看URL结构。默认页面大概是http://localhost/sqli-labs/Less-48/?sort1sort就是注入点参数。第一步我先做三件事给sort分别传1、2、3确认数据排序顺序会变传一个非常大的数字比如sort999看是否报错或返回空传sort1%27看是否出现字符型闭合的痕迹。实测下来sort1、2、3分别对应按users表第一列、第二列、第三列排序说明这里的排序键确实由参数控制。sort999会返回空数据集因为第999列不存在但错误被静默处理了。sort1%27不会触发任何报错也不影响排序说明闭合方式就是数值型不需要考虑引号。3.2 利用排序差异做布尔盲注确认了基础情况后我直接进入核心环节构造IF表达式验证排序差异。先访问http://localhost/sqli-labs/Less-48/?sortIF(11,1,2)页面按第一列排序再访问http://localhost/sqli-labs/Less-48/?sortIF(12,1,2)页面按第二列排序。两次返回的第一条用户名明显不同证明布尔盲注的通道已经建立。接下来爆破数据库名长度。假设当前数据库名长度为8访问sortIF(length(database())5,1,2)如果排序键为1说明length(database())确实大于5继续用二分法调整阈值最终能确定长度。我当时测试时用的命令是sortIF(length(database())8,1,2)页面和sort1一致说明长度确实是8。然后逐字符爆破数据库名。以第一个字符为例sortIF(ascii(substr(database(),1,1))113,1,2)如果返回排序键1对应的顺序说明第一个字符的ASCII大于113也就是在字母q之后再通过二分收敛最终确定第一个字符。然后用substr(database(),2,1)、substr(database(),3,1)这些把整个库名拼出来。表名的获取同样思路。以“当前数据库下的第一张表”为例sortIF(ascii(substr((select table_name from information_schema.tables where table_schemadatabase() limit 0,1),1,1))100,1,2)这里information_schema.tables是所有数据库的元数据表table_schemadatabase()过滤出当前库的表limit 0,1取第一张再逐字符爆破表名。sqli-labs的当前库里通常有users表爆破出来之后进一步获取它的列名和记录值。3.3 利用时间盲注做备选验证如果不想靠肉眼分辨排序差异可以用时间盲注交叉验证。访问sortIF((select ascii(substr(database(),1,1)))115,SLEEP(3),1)当子查询结果为115也就是字母s的ASCII值时页面延迟3秒返回否则立即返回。通过调节ASCII阈值同样能拼出库名。我个人更喜欢用时间盲注来验证几次布尔盲注的结论因为排序差异有时受数据库默认排序规则影响可能首条记录不明显时间盲注的结果则非常确定——延迟就是延迟不延迟就是不延迟。Burp Suite的响应时间标签页可以直接看到毫秒级差异比肉眼翻页面快得多。3.4 获取关键数据的完整payload参考这里把我通关过程中实际使用的几组payload整理成一个速查表供你参考。前提是当前库为默认的安全测试库里有张users表目标构造条件payload片段只写IF里的条件部分判断库名长度长度为8length(database())8判断库名第1个字符ASCII115ascii(substr(database(),1,1))115判断库名第2个字符ASCII101ascii(substr(database(),2,1))101取第一张表名第1个字符ASCII117ascii(substr((select table_name from information_schema.tables where table_schemadatabase() limit 0,1),1,1))117取users表第一列名第1个字符ASCII105ascii(substr((select column_name from information_schema.columns where table_schemadatabase() and table_nameusers limit 0,1),1,1))105查询users表第一行username第1个字符ASCII68ascii(substr((select username from users limit 0,1),1,1))68使用时把条件部分拼到sortIF(...)的括号里就行。如果你用的是二分法可以先把条件写成大于某个阈值再逐步逼近速度会快不少。4. 常见问题与排查技巧4.1 排序结果始终不变怎么办如果两次请求sortIF(11,1,2)和sortIF(12,1,2)返回的页面数据顺序完全一样最可能的原因是你把sort的值写进了数据库但真正控制排序的字段不是sort。可以去看一下页面源码确认表单参数名或者检查URL是不是真的把sort传进去了。另一种情况是数据库查询结果集只有一行或几行排序键再怎么变视觉上感受不到明显变化。sqli-labs的users表一般有13行以上不太会撞上这个问题但如果你在别的页面用这套思路测试就要注意样本量。这里有个经验即便数据顺序变化不明显也可以关注第二个、第三个字段的值变化而不要只盯着第一条记录。4.2 报错注入失败是因为没开报错显示我在前面提过Less-48对SQL错误处理是静默的。如果你试了sortextractvalue(1,concat(0x7e,database()))发现啥也没有这不是你的payload写错了而是关卡设计本身就不让报错回显。把这个现象记下来判断一个注入点是否支持报错注入最快的方法就是故意构造一个语法错误比如sort1%27看页面有没有任何变化页面毫无波动就说明报错被吞了赶紧切到盲注路径。这里还有一个延伸经验在实际代码审计里如果一个查询语句拼接了用户输入同时PHP端又用了mysqli_query或mysqli_error不输出那你的注入思路就得自动切换到盲注或时间盲注不要把宝押在报错注入上。4.3 盲注效率太低怎么办手工逐字符爆破确实枯燥。Less-48作为通关练习我建议你至少要手工完成一次完整的布尔盲注把逻辑跑通。但如果你已经明白原理想快速把数据捞出来可以用脚本。我这里分享一个思路不一定需要很长的代码用Python的requests库定义两个函数一个是“发送请求并返回排序后的第一条记录”另一个是“根据排序差异判断条件真假”。然后在条件判断函数里写一个二分搜索从32到126循环获取每个字符的ASCII值。脚本逻辑很直接几十行就能写完。这样你就能自动跑出库名、表名、字段名整个过程也加深了对布尔盲注的理解。我当时写自动化脚本时踩过一个坑requests每次请求之间默认不保持连接多线程并发时容易被WAF拦截或触发数据库压力所以建议用requests.Session()复用连接并且每次请求之间加一个0.1秒的小延时。如果在真实的授权测试环境中更要严格控制频率避免影响业务。4.4 不要把ORDER BY注入和WHERE注入混为一谈最后半个提醒Less-48的注入点在ORDER BY后面不是在WHERE后面。所以别想着什么 OR 11 --这种套路那套在WHERE注入里好使在ORDER BY注入里只会让查询变成一个奇怪的表达式。ORDER BY注入的正确姿势永远是围绕“排序键表达式”做文章。把这一层想通了Less-49到Less-53的系列关卡你就能以不变应万变。我自己在打通Less-48之后的最大体会是盲注不是只能靠“页面有无数据”这种二元判断排序顺序、响应时间、甚至返回数据的具体排列都是可以被利用的“侧信道”。Less-48虽然没有直接给你一个报错窗口却给了你一个观察排序的大屏幕这个设计比纯粹的布尔盲注友好得多也更容易让人建立起“信息总会有地方投射出来”的思维。后续刷Less-49的时候你只需要把数值型闭合改成单引号闭合其他payload几乎可以平移过去。反过来如果你在实际项目里遇到ORDER BY可控但其他注入位置都堵死的情况Less-48这套“条件排序 二分爆破”的组合就是稳妥的兜底方案。最后再分享一个小技巧通关之后别急着走把Less-48的源码打开看看重点观察它处理SQL错误的方式和输出排序结果的方式你会发现很多实战场景里的“黑盒判断”在源码里都是有迹可循的。理解靶场的代码比单纯背payload重要得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑