接着第一章继续说:
除了之前的注入方式,我们还有其他的方法嘛?
第一个想法就是利用报错信息来传递我们想要的信息。
因为之前传入id=1'时,页面返回给了我们错误的信息,那么我们能不能让页面返回的错误信息中包含我们需要的信息呢?
构造如下url:
http://127.0.0.1/sql/Less-1/?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))) --
逐步来解释:
0x7e是~符号;
我们发现其中有两个拼接函数,concat和group_concat
concat和group_concat都是用在sql语句中做拼接使用的,但是两者使用的方式不尽相同,concat是针对以行数据做的拼接,而group_concat是针对列做的数据拼接,且group_concat自动生成逗号。
先分析最内层的语句:
select group_concat(table_name) from information_schema.tables where table_schema=database();
如果不使用group_concat查询会是如下效果:

我们想把这一列 的数据进行拼接,所以需要group_concat,拼接后结果如下:

这时候再使用concat函数拼接:

这个查询结果是没有问题的,将~与四个表名拼接一起,那为什么会报错呢?
原因就在于extractvalue()函数:
extractvalue():从目标XML中返回包含所查询值的字符串。
原型:extravalue (XML_document, XPath_string);
第一个参数:XML_document是String格式,为XML文档对象的名称
第二个参数:XPath_string (Xpath格式的字符串)
第二个参数 xml中的位置是可操作的地方,xml文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,也就是使用路径去定义一个元素。如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。
在我们构造的url中,XPath_string的值是~emails,referers,uagents,users
而以~开头的内容不是xml格式的语法,报错,但是会显示无法识别的内容是什么,这样就达到了目的。
最终报错结果如下:

同样的方法,获取users表的字段名:
http://127.0.0.1/sql/Less-1/?id=1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'))) --
结果如下:

有一点需要注意,extractvalue()能查询字符串的最大长度为32,就是说如果我们想要的结果超过32,就需要用substring()函数截取
比如我们只查看前五位:
http://127.0.0.1/sql/Less-1/?id=1' and extractvalue(1,concat(0x7e,substring((select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),1,5))) --
结果如下:

除了extractvalue()函数,还有updatexml()函数有同样的用途
构造如下xml:
http://127.0.0.1/sql/Less-1/?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),1) --
updatexml()函数与extractvalue()类似,是更新xml文档的函数。
语法updatexml(目标xml文档,xml路径,更新的内容)
我们需要注入的地方就是xml路径处,同样也只能查询32位。
结果如下:

还有一种利用报错进行注入的函数:floor()函数
构造如下url:
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,2,count(*) from users group by concat(database(),floor(rand(0)*2)) --
返回结果如下:

能看到成功爆出了数据库名。
一步一步来解析:
rand()用于返回一个[0,1]之间的随机数:

每一次结果是不一样的,但是如果rand(0)呢?

我们发现每次结果是一样的了。
也就是说如果我们指定一个整数参数N,这个整型参数称为种子值,rand(N)会根据这个种子值产生重复序列,也就是rand(0)的值重复计算是固定的。
那如果吧rand(0)*2,结果就是[0,2]之间的随机值,准确说应该是伪随机的,我们会发现两次计算结果的序列是一样的:

floor()返回不大于x的最大整数值,把这个函数作用于rand(0)*2后的结果如下:
会产生固定的0,1序列:

concat()是字符串拼接函数,拼接多个字符串,如果字符串中含有NULL,则返回结果为NULL。
所以concat(database(),floor(rand(0)*2))的结果为'security0'或'security1',且是按照固定顺序出现的。
count(*)是一个聚合函数,就是用来计数的,用于返回所计数的数目,它与count()的区别是它不排除NULL,常与group by一起用。
我们来看一个例子:
在原始的users表上我们添加了一行数据:id=15,username=admin,password=admin

这时,users表中username=admin的数据项就有两条了:

这时候我们按照username的值对users表的表项进行分类并计数:

在原始数据中有两则数据项的username字段值为admin,其他username都各不相同,所以计数结果是合理的。
group by在执行时,会依次取出查询表中的记录并创建一个临时表,group by的对象便是该临时表的主键。如果临时表中已经存在该主键,则将值加1,如果不存在,则将该主键插入到临时表中。
先是取到第一条记录,username=Dumb,但是临时表位空,并没有主键位Dumb,故将Dumb插入位主键,并设置count(*)=1:
| key | count(*) |
| Dumb | 1 |
..........
| key | count(*) |
| Dumb | 1 |
| ... | ... |
| admin | 1 |
当取到最后一条记录时,发现username=admin,而临时表中已有主键位admin的记录,那么就让count(*)加1:
| key | count(*) |
| Dumb | 1 |
| ... | ... |
| admin | 2 |
目前还看不出什么问题,因为group by的对象是concat(database(),floor(rand(0)*2)),也就是security0和security1的序列,
但是,还有一个最重要的特性要考虑,就是group by与rand()使用时,如果临时表中没有该主键,则在插入前rand()会再计算一次。就是这个特性导致了报错。
还记得之前floor(rand(0)*2)的序列嘛:

group by的对象是:concat(database(),floor(rand(0)*2))
当取第一条记录时,group by的是security0,但是临时表中并没有这个主键,这时rand(0)会再计算一次,然后插入的就是security1了:
| key | count(*) |
| security1 | 1 |
取第二条记录时,group by的是security1,临时表中有这个主键,直接将count(*)加1
| key | count(*) |
| security1 | 2 |
取第三条记录时,group by的是security0,但是临时表中并没有这个主键,这时rand(0)会再计算一次,得到security1并尝试插入,
但是插入的时候发现这个临时表里面已经有主键security1了,所以会报错:主键security1重复。报错的同时将我们想要知道的database()信息暴露了出来。
除了通过会显得错误信息进行注入,还有什么其他方法呢?
还有一种基于时间的注入,这个比较好理解,构造如下url:
http://127.0.0.1/sql/Less-1/?id=1' and if(length(database())=8,sleep(5),1) --
这句的意思是如果数据库名称长度为8就延迟五秒再继续执行。
发出这个请求后过了一段时间(大于5秒)才收到回送消息,说明数据库长度的确等于8。
类似的,更改payload可以测试其他想要知道的信息。
sqlmap使用
从之前的注入过程能明显感觉到,如果想获得一个想要的信息,可能需要很多次操作,这很费时间和精力。
而sqlmap则是一个很好的检测和利用sql注入的工具,使用方便。
先简单介绍一下sqlmap:
sqlmap是一个开放源码的渗透测试工具,配备了一个功能强大的检测引擎。如果url存在注入漏洞,他就可以从数据库中提取数据;如果权限较大,甚至可以在操作系统上执行命令、读写文件。
sqlmap基于python编写,是跨站台的,任意一台安装了python的操作系统都可以使用他。
对于刚才的例子,我们使用sqlmap进行注入演示:
第一步,判断是否存在注入点:
python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1"
使用-u参数指定url,如果url存在注入点,将会显示出web容器以及数据库版本信息;
结果如下,红框中即为web容器以及数据库版本信息:

第二步:获取数据库:
python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --dbs
使用-dbs参数读取数据库,结果如下:

同时sqlmap把获取的数据信息存放在了指定文件夹中;
第三步:查看当前应用程序所用数据库:
python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --current-db
使用--current-db参数列出当前应用程序所使用的数据库,结果如下:

第四步:列出指定数据库的所有表:
python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --tables -D "security"
使用--tables参数获取数据库表,-D参数指定数据库,结果如下:

第五步:读取指定表中的字段名称:
python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --columns -T "users" -D "security"
使用--columns参数列取字段名,结果如下:

第六步:读取指定字段内容:
python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --dump -C "id,paddword,username" -T "users" -D "security"
--dump参数意为转存数据,-C参数指定字段名称,-T指定表名(如果名称为数据库关键字,建议加上[]),-D指定数据库名称,结果如下:

sqlmap告诉我们读取的数据转存为了一个本地的csv文件。
上述步骤就是使用sqlmap进行一个最基本的sql注入的流程。
除此以外sqlmap还有很多其他选项与功能。
笔者刚开始阅读sqlmap源码,之后会不定时上传源码阅读心得以及更深入的sqlmap使用方法。
上述所说的都是基于url上更改参数进行注入,可以归类为GET注入;
但是有些情况下注入的位置并不反映在url中,比如说POST请求时表单位置的注入
这种情况下建议抓包再修改数据,比较方便。常用的工具有burpsuite
简单介绍下burpsuite:
使用burpsuite首先要设置代理,以firefox为例:
选项-->网络设置-->代理

手动配置代理如下:

同时在burpsuite中proxy模块的options中添加对应的参数,两个端口要保证一致:

然后就可以开心地使用burpsuite啦!
burpsuite的功能很多,这里简单介绍下几个常用的模块:
target模块:
当我们每次访问网站时,target模块会记录下所有请求,并归于一个网站文件夹中,
左边为网站目录,右边上方为每次请求的记录,点开每则请求都可以看到对应的request和response

蓝色框框点开是过滤器,点开可以设置对应的过滤选项:

对每则请求,我们可以右键添加comment或者highlight,效果如下:

里面有一个比较重要的参数是状态码字段:
下面讲讲proxy模块:
当intercept is on点击后成为灰色,就表示已经开启拦截功能,就可以开始抓包啦

比如访问如下url时:
http://127.0.0.1/sql/Less-1/?id=1
回车,并没有直接访问成功,浏览器一直在加载
因为发出的请求报文被burpsuite拦截下来了:

上面的forward表示放包,drop表示丢包,就是把这个包废弃掉,action表示执行其他动作;
按下forward后访问,浏览器就能正常接收到response了:

对于抓到的包,除了以raw格式查看,还可以以params查看请求参数,headers查看http报文头部,hex查看16进制报文
回到之前提到的action,点开后可以调用其他模块的功能,比如repeater和intruder模块:
比如我们点击action-->send to repeater,就可以在抓到包的基础上调用repeater模块的功能了。
repeater模块:

在这个模块中可以对抓到的包进行修改后发送,并且可以多次修改包内容,比较每次修改后的不同,且不需要多次抓包。
比如,我们先不进行修改,之间点击send把包发出去:

然后可以把id=1改为id=1',再点击send:
相比于repeater模块,之前的proxy模块中每次对包修改都需要重新抓包。
这个repeater模块就与我们谈到的sql注入有关了:
访问如下url:
http://127.0.0.1/sql/Less-11/
页面如下:

随便输入一些内容,比如username=aaa,password=aaa,进行抓包,结果如下:

这是一个POST请求,之前直接修改url来进行sql注入的方法已经不行了,我们把这个报文发送到repeater中进行操作:
再repeater中先点击send,把包发出去看看response是什么样的:

登陆失败了,用户名与密码不争取;
接下来再username=aaa后面加上单引号再send试一试:

报错了,说明这儿是存在sql注入的。
这时候我们可以尝试无密码登录(前提是我们知道有一个用户名时admin了),就是使用username=admin' -- ,通过注释符把后面的密码字段世界给注释掉,结果如下:

在此基础上我们来尝试基于报错的注入:
将username字段的值设置为:
admin' and extractvalue(1,concat(0x7e,(select database()))) --
成功爆出了数据库名字:

我们还可以尝试在没有用户名的情况下登录,username设置为:
aaa' or 1=1 --
结果如下:

当然,union注入也是可以的,username设置为:
aaa' union select 1,2 --
结果如下:

因为在1和2的位置上都有回显,那可以在此基础上查询其他信息,比如数据库名字:
设置username为:
aaa' union select 1,database() --
结果如下:

repeater模块就介绍到这儿。
intruder模块主要是用于暴力破解,将抓到的数据包send to intruder后intruder的界面如下:

target底下是设置攻击目标的地方;
position底下是设置具体要暴力破解什么参数的地方:
红框中能看到,有三个参数的值被$符号闭合住,并有绿色底色,这就表明这三个参数是被选中要破解的目标:

右边的add和clear等按钮可以添加或者清楚这些被选中的参数,比如我们只选中破解passwd字段:
就像这样(偷偷把username改为了admin,因为我们知道有一个用户叫做admin):

然后进入payloads功能:
因为我们只准备设置一个payload文件,所以payload set为1,payload type设定为simple list,就是一个列表,当然下拉下来可以有很多选项,还可以自己导入字典文件;在蓝框中输入我们觉得有可能成为密码的数据,并add到列表中:

这样我们最基础的payload就设置完成了,点击右上角start attack开始攻击:

可以看到,intruder模块将我们选中的参数遍历了payload中的数据并发送报文,并将每次请求后的response记录了下来;
其中需要注意的是状态码和长度字段,我们发现最后一席payload为admin时,response报文的长度不一样,点开渲染一下,发现登陆成功:

这样就暴力破解成功了。当然这个例子中我们提前是知道用户名与密码的,所以构造的payload列表比较简单。现实中的攻击可能不仅要攻击一个参数,还需要配置复杂的字典文件。
可以介绍下四种攻击模式(留个坑)。
至此,这一章内容结束了。
浙公网安备 33010602011771号