news 2026/10/23 9:14:08

SQL注入之ORDER BY注入:Less-48无回显场景的报错与盲注实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL注入之ORDER BY注入:Less-48无回显场景的报错与盲注实战

打sqli-labs打到第48关的人,多半已经对报错注入、布尔盲注这些基础操作很熟了。但Less-48有个特点,它不像前面那些关卡给你一个明显的“注入点”,而是一个藏在sort参数里的数字型ORDER BY注入,而且页面照常渲染数据,可你就是没法用union控制回显位置。这一关卡了不少人,原因不是难,而是“思路没切换过来”——排序子句里的注入,跟WHERE后面的注入玩法不一样。

如果你正在通sqli-labs,或者想搞清楚“为什么ORDER BY后面也能注入”“无回显情况下数据怎么拿出来”,这篇就把Less-48从原理到完整拿数据的过程拆开讲一遍,包括我打的时候踩过的坑和最后沉淀下来的判断顺序。

1. 关卡整体设计与思路拆解

1.1 从46到53:排序注入家族的排位

sqli-labs从Less-46开始进入了一整块“排序注入”专题,一直延伸到Less-53左右。这一块的核心参数都是sort,也就是页面里的排序功能。Less-46是数字型无过滤,页面有回显结果;Less-47是字符型报错注入;Less-48是数字型、结果不回显但有报错信息;Less-49是字符型完全盲注;再往后的50到53又加入了堆叠注入的变化。

Less-48在这个序列里的位置特别有意思。它跟46是同一类数字型排序注入,但因为它“查询结果不回显”,很多习惯用UNION一把梭的人,到了这里会突然发现自己的通用payload全部失效,然后就开始瞎试。实际上这关的设计用意很简单:逼你从“回显型注入”切换到“报错型注入”或“盲注型”思路。

这种设计在真实业务里非常有代表性,因为很多后台的排序功能就是直接把用户传的字段名拼进ORDER BY,而排序结果本身只是“先后顺序变了”,并不会把SQL查询出来的其他列暴露给你。所以你在页面上只能看到数据顺序在变,至于你注入的表达式干了什么,页面一句话都不说。

1.2 48关的设计核心:没有回显才是重点

Less-48的页面其实照常显示了一张用户表,有id、username、email之类的列,你传?sort=1和?sort=2,表格顺序会跟着变化。看到这个现象,第一反应是“order by注入”,但如果直接拖出UNION的payload:

?sort=1 UNION SELECT 1,2,3

你会发现页面没有任何变化,也不报错,好像什么都没发生。

这背后的原因就是这关的核心设计:虽然sort参数拼进了SQL语句,但查询字段是固定的SELECT * FROM users ORDER BY $id,你无法通过UNION控制查询结果集里显示哪些列。数据库真正执行的语句里,你的UNION SELECT虽然在语法层面被拼进去了,但由于整个关卡后端对结果的处理方式限制(通常是只取第一段查询输出并渲染固定表格),你注入的“自定义输出列”根本不会出现在页面上。

这是很多人在Less-48卡住的第一道坎。这时候要明确一点:注入不一定非要有回显,能用报错信息带出数据,或者通过页面“排序是否正常”作为布尔条件判断,都一样可以拿数据。

1.3 思路选型:为什么优先报错,其次盲注

在Less-48里,我个人建议的优先级是:报错注入 > 布尔盲注 > 时间盲注。

报错注入排第一,是因为它最快,一条请求就能爆出库名、表名、数据内容。而且这关后端并没有屏蔽updatexml这类报错函数,报错信息会直接渲染在页面上。

布尔盲注排第二,是当报错函数被过滤掉时,你还能靠?sort=1 AND 1=1和?sort=1 AND 1=2的页面顺序差异来判断条件真假,逐字符猜数据。

时间盲注排最后,因为ORDER BY子句里加sleep()很容易造成整条SQL执行时间被放大,而且如果你对靶场环境本身不熟,很容易误判是网络问题还是真触发了延时。

2. 核心细节解析:注入点定位与ORDER BY语法

2.1 先确认参数与排序逻辑

拿到Less-48,第一步不是直接上payload,而是先观察一下这个排序功能正常长什么样。你访问:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 http://127.0.0.1/sqli-labs/Less-48/?sort=2

页面表格的排序会跟着变,这是一种正常现象。接着再试试:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 desc http://127.0.0.1/sqli-labs/Less-48/?sort=1 asc

如果页面排序结果确实发生了变化,说明这个值被直接拼进了ORDER BY后面,而且没有加引号。这就是数字型排序注入的典型特征:后端SQL大概是这样的:

SELECT * FROM users ORDER BY $id

注意,这里不需要你像字符型注入那样去“闭合引号”,因为数字型直接就是裸值,字符串型则需要先闭合前面的引号,再处理后面的引号,比如Less-47的sort=1'报错。

2.2 数字型ORDER BY注入的特殊之处

很多人习惯的注入场景是WHERE id=1,那块区域是条件判断,你注入的表达式会直接影响“哪些行被查出来”。但ORDER BY不一样,它是“排序依据”,你注入的表达式会被数据库当成一个排序键来求值,最终不直接决定返回哪些行,只决定行的顺序。

这个差异是理解Less-48的关键。比如你构造:

SELECT * FROM users ORDER BY 1 AND updatexml(1, concat(0x7e, database(), 0x7e), 1)

MySQL在执行排序时,会去计算1 AND updatexml(...)这个表达式的值,而计算过程中updatexml的第二个参数里包含了数据库名,并且因为特殊字符0x7e和正常字符串拼接后不符合XPath格式,MySQL就会抛出错误,这个错误信息会带着database()的结果直接显示在页面上。

这就像是你在排队叫号,本来只是决定谁排前面谁排后面,但你硬塞给它一个“喊话器”,它在算排序依据的时候把你的数据内容喊了出来。

用生活化一点的类比:ORDER BY本来是让你按“号码牌”排队,但你把自己的号码牌设计成了一段“播报文字”,系统在读取号码牌内容的时候,就把不该公开的信息念出来了。

2.3 为什么UNION在这里失效

UNION注入的前提是你能控制第二个SELECT的列数和列内容,并且页面会把第二个SELECT的结果回显出来。Less-48里面,查询语句是固定的SELECT * FROM users ORDER BY ...,你传入的UNION部分即使语法正确,页面真正渲染的仍然是SELECT * FROM users产生的结果集。

更具体地说,当SQL里出现ORDER BY时,典型的UNION结构通常是UNION SELECT ... ORDER BY ...,你无法在ORDER BY后面再插入一个完整SELECT来改变主查询的列。就算你在sort里构造1 UNION SELECT 1,2,3,最终语句变成:

SELECT * FROM users ORDER BY 1 UNION SELECT 1,2,3

这在MySQL语法里通常是报错的,或者即使不报错,页面也不会输出UNION部分的内容。这就是为什么很多人在Less-48用UNION测试时“什么都看不到”。

所以到了这一关,必须果断放弃UNION,转头用报错函数。

3. 实操过程:从零到完整数据的全步骤

3.1 第一步:构造报错注入探针

Less-48最稳定的报错函数是updatexml,其次是extractvalue。我先用updatexml确认环境是否支持:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND updatexml(1,concat(0x7e,database(),0x7e),1)

如果页面返回类似这样的报错:

XPATH syntax error: '~security~'

说明报错注入成立,~security~里的security就是当前数据库名。这就算拿到了第一管数据。

要注意URL里的空格和特殊字符,浏览器地址栏里最好用HackBar这类工具,或者用Burp Suite重发,避免URL编码问题导致payload被拆散。我自己习惯用HackBar,直接粘贴整条URL,它会自动处理编码。

3.2 第二步:报库名、表名、列名、数据

拿到了数据库名之后,后面就是流水线操作。爆表名:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND updatexml(1,concat(0x7e,(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()),0x7e),1)

爆users表的列名:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND updatexml(1,concat(0x7e,(SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users'),0x7e),1)

爆账号密码数据:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND updatexml(1,concat(0x7e,(SELECT group_concat(username,0x3a,password) FROM users),0x7e),1)

这里有一点必须提醒:updatexml报错内容最长只能显示32个字符,超过的部分会直接被截断,而且末尾的~标记也会失效。所以当数据超过31个字符时,要用substr()分段取。比如:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND updatexml(1,concat(0x7e,substr((SELECT group_concat(username,0x3a,password) FROM users),1,31),0x7e),1)

这个只取第1到31个字符。下一段就把偏移改成32:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND updatexml(1,concat(0x7e,substr((SELECT group_concat(username,0x3a,password) FROM users),32,31),0x7e),1)

依此类推,直到把所有数据拼完整。

3.3 第三步:布尔盲注兜底方案

如果靶场环境某些版本把updatexml过滤了,或者你用的是别的关卡变体,布尔盲注也能打。原理是用页面“排序是否正常”作为真假判断的开关。

构造:

http://127.0.0.1/sqli-labs/Less-48/?sort=1 AND (SELECT ascii(substr(database(),1,1))>115)

如果database()第一个字符的ASCII大于115(也就是s,因为s的ASCII是115),整体表达式为真,排序正常执行,页面跟?sort=1时一样正常显示。如果为假,排序表达式结果变成0,页面大部分情况下会以另一种顺序显示表格,或者在首行出现明显变化。

实际操作中,我习惯一组一组对比:

?sort=1 AND (SELECT ascii(substr(database(),1,1))>100) ?sort=1 AND (SELECT ascii(substr(database(),1,1))>110) ?sort=1 AND (SELECT ascii(substr(database(),1,1))>115)

这样二分下去,没几次就能确定一个字符。虽然慢,但能用。时间盲注在这种场景下我不推荐,因为ORDER BY后面的sleep会让排序子句反复计算,很可能一次网络请求卡好几秒,反而不好判断。

3.4 第四步:验证是否拿到结果

Less-48走到这里,其实并没有一个传统意义上的“通关页面”,它的目标就是把数据库里存储的数据通过报错或盲注完整提取出来。当你看到类似:

XPATH syntax error: '~Dumb:Dumb~'

以及后续一条条用户数据都出来了,这关就算打通了。如果页面用的是extractvalue,报错格式会略有不同,但原理一样。

4. 常见问题与排查技巧实录

4.1 典型问题速查表

我打这关时遇到过几个问题,整理成表方便你对照:

现象可能原因解决方案
?sort=1 AND updatexml(...)没有任何反应空格被URL编码丢失,或HackBar没生效手动把空格换成%20,用Burp Suite直接发包
页面报了一大段普通SQL错误,但没有数据内容拼接位置没闭合好,表达式没被当成排序键确认参数是sort,不是id,确认前面没有多余引号
updatexml报错只显示前32个字符,数据不完整函数本身限制用substr()分段提取偏移
UNION SELECT看不到任何回显ORDER BY场景不支持这种方式换报错注入或布尔盲注
布尔盲注时页面顺序变化不明显排序字段本身区分度低对比1 AND 1=1和1 AND 1=2的极限状态,确认判断基准
数据里有单引号或特殊字符导致SQL语法崩报错函数拼接出了问题用0x3a代替冒号,用0x7e包裹内容

4.2 几个容易踩的细节坑

第一个坑是参数名。Less-48的注入参数是sort,不是id。有些朋友看到页面是显示用户表,就习惯性去测?id=1,折腾半天发现没反应,其实方向就错了。先看URL里的参数名,这比任何技巧都重要。

第二个坑是报错函数的长度限制。updatexml最多爆32个字符,extractvalue也类似,官方文档里写的是最多32。很多人在这一步栽跟头,以为数据就那么多,实际上是被截断了。你只要看到报错信息末尾没有预期的~符号,基本就是截断。

第三个坑是子查询的括号。你在concat()里面写的SELECT语句必须用括号包起来,否则group_concat会被当成独立的语句,导致语法错误。我早期写payload经常漏掉那个括号,报错信息一出来直接就是SQL syntax error,根本没机会看到数据。

第四个坑是浏览器地址栏直接输入长URL时,单引号很容易被浏览器转义或截断。请优先用HackBar或者Burp Suite的Repeater,这两个工具能保证payload原样发送。

4.3 耗时点与效率优化

打这关如果只走报错注入,其实非常快,正常情况下几分钟就能拿完数据。但如果走到布尔盲注,效率就会骤降。我的建议是:能报错就不盲注,能布尔就不时间。

另外,信息收集的时候,可以先跑一遍数据库版本和当前用户,确认环境权限。比如:

?sort=1 AND updatexml(1,concat(0x7e,version(),0x7e),1) ?sort=1 AND updatexml(1,concat(0x7e,current_user(),0x7e),1)

拿到版本信息后,心里对函数支持情况有个底,后面就不会因为用了太高版本的函数而莫名报错。

5. 延伸思考:这类注入在真实系统里的样子

5.1 真实业务中的ORDER BY注入场景

Less-48看起来就是个靶场题目,但ORDER BY注入在真实系统里相当常见,只是大家平时不太注意。典型场景包括:

  • 电商商品列表的“按价格排序”“按销量排序”,前端传sort=price或order=sales
  • 后台管理系统的列表排序,传field=create_time
  • 报表系统的“按列排序”,传column_name
  • 排行榜、日志展示、订单管理这些带表格页面的地方

这些功能里,开发为了省事,往往会直接把请求参数的值拼进SQL语句。如果服务端只校验了“参数不能为空”,没有做字段名白名单,那可注入面就出来了。Less-48里的sort参数本质上就是这个东西。

5.2 为什么ORDER BY难以参数化

这是很多开发同学的一个盲区。SQL预处理对WHERE条件里的值很友好,可以用?占位符绑定参数,但ORDER BY后面跟的是“字段名”,不能直接绑定。你没法写:

SELECT * FROM users ORDER BY ?

因为MySQL会把问号当成一个字面量值,而不是字段名。这就导致很多团队偷懒,直接把用户输入拼进SQL。我见过不少系统,WHERE部分做得很严谨,ORDER BY部分裸奔,这就是Less-48这类漏洞大面积存在的原因。

5.3 防御与白名单方案

要防住Less-48这种注入,最有效的方法是“白名单映射”,而不是过滤特殊字符。服务端可以维护一个允许排序的字段集合:

$sortWhitelist = [ 'id' => 'id', 'name' => 'name', 'email' => 'email', 'price' => 'price' ]; if (isset($sortWhitelist[$_GET['sort']])) { $orderField = $sortWhitelist[$_GET['sort']]; } else { $orderField = 'id'; }

这样用户只能从预设字段里选,即使传入1 AND updatexml(...),也只会落到默认的id,没有任何注入机会。同时排序方向也做白名单:

$orderDir = strtolower($_GET['dir'] ?? 'asc'); $orderDir = ($orderDir === 'desc') ? 'DESC' : 'ASC';

这比任何过滤函数都可靠。过滤黑名单迟早有绕过的办法,白名单是直接从根上掐断。

5.4 合法边界提醒

还要唠叨一句:Less-48的所有操作,都应该在本地sqli-labs靶场环境里完成。sqli-labs就是一个跑在自己电脑上的PHP+MySQL靶场,随便造。但同样的手法绝对不能用在没有授权的系统上,法律边界清晰得不能再清晰。安全测试的核心素养不是“会不会注入”,而是“知道什么能碰、什么不能碰”。

我个人在实际操作里的体会是:Less-48这一关,训练的不是某个技巧,而是“面对无回显注入时能不能冷静切换思路”。很多新手到这一关之所以卡住,就是因为大脑里只有一条“UNION注入”的路,一旦回显消失就不知道怎么办了。而真正打多了之后,你会条件反射地先判断注入点环境,再看回显条件,再决定用报错、布尔还是时间盲注。这个决策链路,就是Less-48留给你的最大价值。

最后再分享一个小技巧:打排序注入相关关卡时,建议先把sort参数的所有正常状态摸清楚,比如按1排序是什么样的,按2排序是什么样的,加不加desc又是什么样。只有把正常基线定准了,后续无论是布尔盲注还是判断payload是否生效,你才有参照物。Less-48的干扰项其实不多,输就输在“没建立基线就乱打”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 9:13:24

SSM博物馆售票系统开发实战:从数据库设计到并发防超卖全解析

做毕业设计那会儿,我抽到的题目是一个JavaWeb方向的经典项目——博物馆售票管理系统。拿到题目的第一反应是“这有什么难的”,真正动手才发现,光是一个在线选票和库存扣减的逻辑,就能让新手纠结一整天。如果你也正在做类似的SSM项…

作者头像 李华
网站建设 2026/10/23 9:14:01

Transformer单轮对话机器人实战:从源码拆解到训练推理全流程

简介:基于Transformer模型训练的中文单轮对话聊天机器人项目,面向计算机、人工智能等专业学生及NLP初学者,可作为课程设计或毕业设计参考。压缩包共十三个文件,以六个Python源码文件为核心,覆盖数据预处理、词表生成、…

作者头像 李华
网站建设 2026/10/20 10:00:06

Spring Boot师生互动桥毕业设计:从版本选型到定时任务与部署实践

1. 项目核心架构与模块拆解1.1 师生互动桥到底要解决什么问题先说个现象:很多学校里的师生交流其实非常碎片化。老师建一个微信群,发通知、收作业、答疑全在里面,消息一多就刷屏,作业文件过期打不开,问题答案转眼就淹没…

作者头像 李华
网站建设 2026/10/20 10:00:07

Sentinel限流实战:QPS与线程数模式选型及Nacos动态配置

1. 先从一次线上事故说起:QPS 限流为什么会误伤慢请求大概去年年中,我们有个核心接口因为下游依赖抖动,单次响应时间从 50ms 暴涨到 3s 左右。当时这套系统用的是 Sentinel QPS 限流,阈值设的 1000。表面上看,这个数字…

作者头像 李华
网站建设 2026/10/20 10:00:06

Golang四方支付系统源码实战:从能跑到敢收单的架构与避坑指南

简介:这是一套用 Golang 编写的四方支付系统完整源码,面向具备一定后端基础、希望深入理解支付业务链路的开发者与二次开发团队。四方支付平台连接商户、消费者、银行及第三方支付机构,源码覆盖订单创建、支付接口调用、状态同步、退款处理等…

作者头像 李华