摘要:本文以 SQL 注入为主题,先回顾数字型注入与字符型注入的区别,再重点讲解两个关键探测手段:order by用于判断查询结果的列数,union用于把自定义查询结果拼接到原查询后面。文章结合靶场实例,详细演示了先用 order by 逐次递增探测列数、再用 union select 对齐列数找出回显点、最后把回显点替换成目标数据的完整配合流程,并强调所有操作都应在合规靶场中进行。
一:上一篇文章我们了解了关于SQL注入的基本流程,这次我们来深度剖析一下数字型注入和字符型注入
(1):注:下面操作均在合规靶场进行!!!
注:下面操作均在合规靶场进行!!!
注:下面操作均在合规靶场进行!!!
(2):我们知道,数字型注入和字符型注入根本的区别就是后端执行SQL代码的时候,有没有对用户输入这块进行校验,例如下面这段SQL代码:
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; $result=mysql_query($sql); $row = mysql_fetch_array($result);因为在
id='$id' LIMIT 0,1";这里后端没有对'$id'进行字符处理,所以导致攻击者输入{1'},后,后端的SQL代码就为这样:
$sql="SELECT * FROM users WHERE id='1'' LIMIT 0,1";因为原始SQL已经用单引号包裹了 $id,攻击者输入的 1' 带来了一个闭合单引号,导致剩下的 ' 变成了多余的语法错误。这就叫“打破原有的闭合规则
这样SQL代码不会被执行,而是抛出一个异常:{You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1'' LIMIT 0,1' at line 1}
意思就是在:{'1'' LIMIT 0,1' at line 1}这里有一个SQL语句错误,这就导致攻击者可以通过这个报错信息来反推这段SQL代码的闭合类型,这也就是攻击者最喜欢看到的信息,比起盲注,这个反而简单了,因为就等于告诉攻击者,注入的结果是直接回显在前端HTML页面里的。
二:在SQL注入中,order by 和 union 是两个非常关键的探测手段,它们能帮助攻击者快速判断SQL语句的结构,为后续的注入利用打下基础。
(1):order by 的用法
order by 在SQL中原本用于对查询结果进行排序,但在注入探测中,它有一个更重要的用途:判断查询结果的列数。
例如,对于前面的SQL代码:
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";我们打开靶场SQLI-LABS/Less-1,过一遍,感受一下。
当在URL后面加上?id=1'参数后,页面就把SQL代码的异常回显在页面上(因为我是直接在原生的HTTP请求里加的参数,浏览器自动进行了URL编码,在URL编码里,{ %20 +} 代表空格,%26代表{#号},%27则代表单引号{ ' }),你用的如果是BP(Burp Suite),则空格用{ + },注释掉后面的SQL代码,可以用{ -- 和 %26 }
回到正题,order by可以帮助我们更快的知道页面最多可以回显多少的字段,同样,当输入{ 1' order by 1 -- } 时,后端的SQL代码就为:
$sql="SELECT * FROM users WHERE id='1' order by 1 -- ' LIMIT 0,1";{ order by "这里的数字可以任意输入" -- },输入{ 3 }时页面没有报错,但输入{ 4 }时,页面如果爆破了,那么,前端回显的最大列数为{ 3 }列,以此类推,例如:
这就可以确定页面回显的最大字段为{ 3 }列。
为什么先探测列数?因为后面的 union 注入要求前后两个查询的列数必须一致,否则会报错。所以 order by 是 union 注入的前置步骤。
二、union 的用法
union 的作用是把两个查询的结果合并在一起。在注入中,我们利用它来把自定义的查询结果拼接到原查询后面,从而获取我们想要的数据。
union 的使用前提有两个:
- 前后两个查询的列数必须相同(这就是为什么要先用 order by 探测列数)。
- 前后两个查询的列类型要兼容。
例如,假设我们探测出原查询有 3 列,那么可以这样构造:
$sql="SELECT * FROM users WHERE id='-1' union select 1,2,3";这里的{ -1} 不是 SQL 的特殊关键字,它只是传给原查询参数的一个值,作用是:让 union前面那一段查询查不到任何数据,从而让页面显示 union 后面的 1,2,3 结果,如果页面回显了2和3,那就说明第 2 列和第 3 列是回显点,例如:
我们可以把占位符替换成想要查询的内容,比如:
$sql="SELECT * FROM users WHERE id='1' union select 1,2,username from users LIMIT 0,1";页面是这样回显的:
这里有一个要说的,这段SQL代码后面的{ LIMIT 0,1 }是这样的:
{ LIMIT 0,1 },LIMIT后面的0,1是LIMIT的子参数,意思是:{ 0 }从一整个结果集里面,从第0条记录(也就是第一条开始),{ 1 }表示是只显示第一条数据,这也是为什么我们使用{ -1' } 这个非法ID,因为原查询查不到数据,页面留出空位,正好让我们的 union select 数据补位回显。
像pikachu靶场就没有对数据限制,这里做一个对比,方便好理解:
三、order by 和 union 的配合流程
在实际注入中,两者的配合流程通常是这样的:
- 先用
order by N逐次递增,探测出查询的列数。 - 再用
union select 1,2,3...对齐列数,找出回显点。 - 最后把回显点替换成目标数据(如库名、表名、字段名、数据)。
下面我们把这个流程拆开,结合前面的例子一步步走一遍,看看每一步到底在做什么、为什么要这么做。
第一步:用 order by 探测列数。这一步是整个注入的“地基”。我们并不知道后端查询到底返回几列,所以从order by 1开始,逐次把数字加 1,直到页面报错为止。比如在 Less-1 中,order by 3页面正常,order by 4页面报错,就说明原查询只有 3 列。为什么必须知道列数?因为 union 合并结果时,前后两个查询的列数必须完全一致,多一列或少一列都会直接报错,导致注入失败。
第二步:用 union select 对齐列数,找出回显点。知道了列数是 3 之后,我们把查询参数改成一个查不到数据的值(比如-1),再拼接union select 1,2,3。这样前面那段查询返回空结果,页面就会显示 union 后面的内容。如果页面上出现了数字 2 和 3,就说明第 2 列和第 3 列是回显点——也就是我们可以把想要的数据“塞”进去的位置。这一步相当于在目标系统里找到了两个可以输出信息的“窗口”。
第三步:把回显点替换成目标数据。回显点确定之后,剩下的就是“换内容”了。比如把2换成database()就能看到当前数据库名,把3换成username就能看到用户名字段。以此类推,我们可以一步步把库名、表名、字段名、具体数据都查出来。整个过程就像在填一张表格,先确定表格有几列,再找到能显示内容的格子,最后往格子里填我们想要的信息。
这个流程是手工注入的核心思路,理解之后,后续的报错注入、盲注等技巧都是在这个基础上的延伸。比如报错注入是利用数据库报错信息来带出数据,盲注则是通过页面是否正常来逐字符猜测数据,它们的本质都是在“探测结构”和“获取数据”这两个环节上做文章。把 order by 和 union 的配合流程吃透,后面再学其他注入方式就会轻松很多。
四、总结
本文围绕 SQL 注入中的两个核心探测手段展开:order by用于判断查询结果的列数,union用于把自定义查询结果拼接到原查询后面。两者的配合流程可以概括为三步:先用 order by 逐次递增探测列数,再用 union select 对齐列数找出回显点,最后把回显点替换成目标数据(如库名、表名、字段名、具体数据)。
理解这个流程的关键在于把握两个前提:前后两个查询的列数必须相同、列类型要兼容。这也是为什么 order by 是 union 注入的前置步骤。掌握了这套手工注入的核心思路,后续的报错注入、盲注等技巧都是在这个基础上的延伸——它们的本质都是在“探测结构”和“获取数据”这两个环节上做文章。
最后提醒一句:以上所有操作都应在合规靶场中进行,切勿用于未经授权的真实系统。