Some vulnerabilities in JEECMSV9

轉載:https://blog.csdn.net/weixin_44063566/article/details/88897406html

 

以前遇到了一個JEECMS大概看了一下, 測試版本JEECMSV9.3java

SSRF
/src/main/java/com/jeecms/cms/action/member/UeditorAct.javaweb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping(value = "/ueditor/getRemoteImage.jspx")
public void getRemoteImage(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String url = request.getParameter("upfile");
CmsSite site=CmsUtils.getSite(request);
JSONObject json = new JSONObject();
String[] arr = url.split(UE_SEPARATE_UE);
String[] outSrc = new String[arr.length];
for (int i = 0; i < arr.length; i++) {
outSrc[i]=saveRemoteImage(arr[i], site.getContextPath(), site.getUploadPath());
}
String outstr = "";
for (int i = 0; i < outSrc.length; i++) {
outstr += outSrc[i] + UE_SEPARATE_UE;
}
outstr = outstr.substring(0, outstr.lastIndexOf(UE_SEPARATE_UE));
json.put(URL, outstr);
json.put(SRC_URL, url);
json.put(TIP, LocalizedMessages.getRemoteImageSuccessSpecified(request));
ResponseUtils.renderJson(response, json.toString());
}
在接受了用戶傳遞過來的url以後, 帶入saveRemoteImage方法spring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private String saveRemoteImage(String imgUrl,String contextPath,String uploadPath) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
CloseableHttpClient client = httpClientBuilder.build();
String outFileName="";
try{
if(endWithImg(imgUrl)){
HttpGet httpget = new HttpGet(new URI(imgUrl));
HttpResponse response = client.execute(httpget);
InputStream is = null;
OutputStream os = null;
HttpEntity entity = null;
entity = response.getEntity();
is = entity.getContent();
outFileName=UploadUtils.generateFilename(uploadPath, FileNameUtils.getFileSufix(imgUrl));
os = new FileOutputStream(realPathResolver.get(outFileName));
IOUtils.copy(is, os);
}
在saveRemoteImage方法當中, 若是經過了endWithImg方法的檢測,就直接發起請求, 而且把請求到的結果輸出到文件當中。apache

1
2
3
4
5
6
7
8
9
private boolean endWithImg(String imgUrl){
if(StringUtils.isNotBlank(imgUrl)&&(imgUrl.endsWith(".bmp")||imgUrl.endsWith(".gif")
||imgUrl.endsWith(".jpeg")||imgUrl.endsWith(".jpg")
||imgUrl.endsWith(".png"))){
return true;
}else{
return false;
}
}
endWithImg的檢測比較簡單, 繞過也比較簡單加個?.jpg就能夠繞過了。json

不過本地測試時, 訪問這個jpg文件的結果倒是404.
首先來看看保存訪問結果的文件的文件名生成方法, 是包含一個月份目錄的。cookie

1
2
3
4
public static String generateFilename(String path, String ext) {
return path + MONTH_FORMAT.format(new Date())
+ RandomStringUtils.random(4, Num62.N36_CHARS) + "." + ext;
}
結果相似爲 /u/cms/www/201902/15002619t400.jpg
而在jeecms的默認源碼當中, 是不存在201902這個目錄的。app


而且在saveRemoteImage方法當中, 並無」判斷這個目錄存不存在,若是不存在的話就建立該目錄」這種邏輯。
在FileOutputStream時, 若是目錄是不存在的話, 會出異常, 因此這裏的文件並無保存上。
要想保存上這個文件, 首先仍是得建立這個目錄。
dom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping(value = "/ueditor/upload.jspx",method = RequestMethod.POST)
public void upload(
@RequestParam(value = "Type", required = false) String typeStr,
Boolean mark,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
responseInit(response);
if (Utils.isEmpty(typeStr)) {
typeStr = "File";
}
if(mark==null){
mark=false;
}
JSONObject json = new JSONObject();
JSONObject ob = validateUpload(request, typeStr);
if (ob == null) {
json = doUpload(request, typeStr, mark);
} else {
json = ob;
}
ResponseUtils.renderJson(response, json.toString());
}
直接查看調用的doUpload方法,webapp

1
2
3
4
5
6
private JSONObject doUpload(HttpServletRequest request, String typeStr,Boolean mark) throws Exception {
.......
else {
fileUrl = fileRepository.storeByExt(site.getUploadPath(),
ext, uplFile);
}
繼續查看storeByExt方法

1
2
3
4
5
6
7
8
9
10
11
public String storeByExt(String path, String ext, MultipartFile file)
throws IOException {
//String filename = UploadUtils.generateFilename(path, ext);
//File dest = new File(getRealPath(filename));
String fileName=UploadUtils.generateRamdonFilename(ext);
String fileUrl =path+fileName;
File dest = new File(getRealPath(path),fileName);
dest = UploadUtils.getUniqueFile(dest);
store(file, dest);
return fileUrl;
}
文件名和目錄的生成方法和saveRemoteImage時使用的方法相同,而後調用了store方法。

1
2
3
4
5
6
7
8
9
private void store(MultipartFile file, File dest) throws IOException {
try {
UploadUtils.checkDirAndCreate(dest.getParentFile());
file.transferTo(dest);
} catch (IOException e) {
log.error("Transfer file error when upload file", e);
throw e;
}
}

1
2
3
4
public static void checkDirAndCreate(File dir) {
if (!dir.exists())
dir.mkdirs();
}
能夠看到雖然在下載遠程圖片的功能中, 沒有」若是不存在這個日期目錄就建立該目錄」這個邏輯, 可是在上傳的時候存在這個邏輯。 因此能夠先經過上傳, 建立了該目錄以後, 再繼續給SSRF利用。
上傳這個功能, 須要登陸以後才能正常使用。
由於在doupload方法以前,

1
2
3
4
5
6
JSONObject ob = validateUpload(request, typeStr);
if (ob == null) {
json = doUpload(request, typeStr, mark);
} else {
json = ob;
}
通過了validateUpload方法, 在該方法當中

1
2
3
4
5
6
7
CmsUser user = CmsUtils.getUser(request);
// 非容許的後綴
if (!user.isAllowSuffix(ext)) {
result.put(STATE, LocalizedMessages
.getInvalidFileSuffixSpecified(request));
return result;
}
若是是未登陸狀態, user爲null 接下來就會出現空指針異常。


上傳以後, 就成功建立了目錄。


再SSRF

 

不過發起請求的httpClientBuilder, 僅支持HTTP/HTTPS協議。


SSTI
JEECMS中存在一些能夠上傳任意文件的點, 只舉例一個
/src/main/java/com/jeecms/cms/action/member/SwfUploadAct.java

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/member/o_swfAttachsUpload.jspx", method = RequestMethod.POST)
public void swfAttachsUpload(
String root,
Integer uploadNum,
@RequestParam(value = "Filedata", required = false) MultipartFile file,
HttpServletRequest request, HttpServletResponse response,
ModelMap model) throws Exception{
super.swfAttachsUpload(root, uploadNum, file, request, response, model);
}
調用了父類的swfAttachsUpload方法,

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
54
55
protected void swfAttachsUpload(
String root,
Integer uploadNum,
@RequestParam(value = "Filedata", required = false) MultipartFile file,
HttpServletRequest request, HttpServletResponse response,
ModelMap model) throws Exception {
JSONObject data=new JSONObject();
WebCoreErrors errors = validateUpload( file, request);
if (errors.hasErrors()) {
data.put("error", errors.getErrors().get(0));
ResponseUtils.renderJson(response, data.toString());
}else{
CmsSite site = CmsUtils.getSite(request);
String ctx = request.getContextPath();
String origName = file.getOriginalFilename();
String ext = FilenameUtils.getExtension(origName).toLowerCase(
Locale.ENGLISH);
// TODO 檢查容許上傳的後綴
String fileUrl="";
try {
if (site.getConfig().getUploadToDb()) {
String dbFilePath = site.getConfig().getDbFileUri();
fileUrl = dbFileMng.storeByExt(site.getUploadPath(), ext, file
.getInputStream());
// 加上訪問地址
fileUrl = request.getContextPath() + dbFilePath + fileUrl;
} else if (site.getUploadFtp() != null) {
Ftp ftp = site.getUploadFtp();
String ftpUrl = ftp.getUrl();
fileUrl = ftp.storeByExt(site.getUploadPath(), ext, file
.getInputStream());
// 加上url前綴
fileUrl = ftpUrl + fileUrl;
}else if (site.getUploadOss() != null) {
CmsOss oss = site.getUploadOss();
fileUrl = oss.storeByExt(site.getUploadPath(), ext, file.getInputStream());
} else {
fileUrl = fileRepository.storeByExt(site.getUploadPath(), ext,
file);
// 加上部署路徑
fileUrl = ctx + fileUrl;
}
cmsUserMng.updateUploadSize(CmsUtils.getUserId(request), Integer.parseInt(String.valueOf(file.getSize()/1024)));
fileMng.saveFileByPath(fileUrl, origName, false);
model.addAttribute("attachmentPath", fileUrl);
} catch (IllegalStateException e) {
model.addAttribute("error", e.getMessage());
} catch (IOException e) {
model.addAttribute("error", e.getMessage());
}
data.put("attachUrl", fileUrl);
data.put("attachName", origName);
ResponseUtils.renderJson(response, data.toString());
}
}
在這個方法中, 上傳時沒有檢查文件的後綴,


從TODO註釋中也能看出來, 檢查容許上傳的後綴這個功能還未實現就直接上線了。

不過在jeecms中上傳的jsp,jspx文件並不能被訪問到。

1
2
3
4
5
6
7
8
<servlet-mapping>
<servlet-name>JeeCmsFront</servlet-name>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>JeeCmsFront</servlet-name>
<url-pattern>*.jsp</url-pattern>
</servlet-mapping>
jsp和jspx文件都通過了JeeCmsFront,

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<servlet-name>JeeCmsFront</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/jeecms-servlet-front.xml
/WEB-INF/config/plug/**/*-servlet-front-action.xml
</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
jsp和jspx文件都會通過org.springframework.web.servlet.DispatcherServlet, 上傳上去的jsp文件確定是沒有對應的映射的 就直接404了。
這裏得結合一些其餘的點進行利用,
/src/main/java/com/jeecms/cms/action/front/CsiCustomAct.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping(value = "/csi_custom*.jspx")
public String custom(String tpl, HttpServletRequest request,
HttpServletResponse response, ModelMap model) {
log.debug("visit csi custom template: {}", tpl);
CmsSite site = CmsUtils.getSite(request);
if(StringUtils.isNotBlank(tpl)){
// 將request中全部參數保存至model中。
model.putAll(RequestUtils.getQueryParams(request));
FrontUtils.frontData(request, model, site);
FrontUtils.frontPageData(request, model);
return FrontUtils.getTplPath(site.getSolutionPath(), TPLDIR_CSI_CUSTOM,
tpl);
}else{
return FrontUtils.pageNotFound(request, response, model);
}
}
能夠看到將用戶傳遞過來的tpl變量直接帶入了getTplPath方法,

1
2
3
public static String getTplPath(String solution, String dir, String name) {
return solution + "/" + dir + "/" + name + TPL_SUFFIX;
}
可控的tpl變量直接拼接進了模板路徑當中,

1
public static final String TPL_SUFFIX = ".html";
默認的模板後綴爲.html, 高版本jdk當中已經再也不可以截斷, 因此這裏先經過剛纔的任意文件上傳一個.html文件, 而後控制模板文件路徑爲本身上傳的模板文件進行SSTI.

由於jeecms的模板引擎使用的是freemarker, 一開始覺得直接用freemarker的SSTI就能rce了, 可是測試的時候失敗了。

1
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

 

在新版本freemarker中, 多了一個TemplateClassResolver.SAFER_RESOLVER配置。

TemplateClassResolver.SAFER_RESOLVER now disallows creating freemarker.template.utility.JythonRuntime and freemarker.template.utility.Execute. This change affects the behavior of the new built-in if FreeMarker was configured to use SAFER_RESOLVER, which is not the default until 2.4 and is hence improbable.

1
2
3
4
5
6
7
8
9
10
11
12
13
TemplateClassResolver SAFER_RESOLVER = new TemplateClassResolver() {
public Class resolve(String className, Environment env, Template template) throws TemplateException {
if (!className.equals(ObjectConstructor.class.getName()) && !className.equals(Execute.class.getName()) && !className.equals("freemarker.template.utility.JythonRuntime")) {
try {
return ClassUtil.forName(className);
} catch (ClassNotFoundException var5) {
throw new _MiscTemplateException(var5, env);
}
} else {
throw MessageUtil.newInstantiatingClassNotAllowedException(className, env);
}
}
}
若是使用了TemplateClassResolver.SAFER_RESOLVER, 就不容許再調用freemarker.template.utility.Execute, freemarker.template.utility.ObjectConstructor以及freemarker.template.utility.JythonRuntime。

1
2
3
4
5
6
public ConstructorFunction(String classname, Environment env, Template template) throws TemplateException {
this.env = env;
this.cl = env.getNewBuiltinClassResolver().resolve(classname, env, template);
if (!TemplateModel.class.isAssignableFrom(this.cl)) {
throw new _MiscTemplateException(NewBI.this, env, new Object[]{"Class ", this.cl.getName(), " does not implement freemarker.template.TemplateModel"});
}
而且容許調用的類只容許爲實現了freemarker.template.TemplateModel接口的類, 大概看了下實現了該接口的類, 除了不容許使用的三個類,沒有找到其餘能利用的類, 就只有放棄RCE了。

從文檔中能夠看出, freemarker從2.4版本之後才默認打開TemplateClassResolver.SAFER_RESOLVER, jeecms使用的版本爲

1
<freemarker.version>2.3.25-incubating</freemarker.version>
雖然沒有默認打開該配置, 可是JEECMS中的freemarker手動打開了TemplateClassResolver.SAFER_RESOLVER,因此SSTI沒辦法RCE了。

1
2
3
4
5
6
7
8
9
10
11
protected void initApplicationContext() throws BeansException {
super.initApplicationContext();
if (getConfiguration() == null) {
FreeMarkerConfig config = autodetectConfiguration();
Configuration configuration=config.getConfiguration();
configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
setConfiguration(configuration);
}
checkTemplate();
}
在TemplateClassResolver.SAFER_RESOLVER的限制下, SSTI也就只能讀讀文件了, 而且只能讀取WEB目錄下的文件。

 


反序列
JEECMS中使用了shiro, 版本爲

1
<shiro.version>1.4.0</shiro.version>
老版本shiro(1.2.4)曾爆過一個反序列,
看了一下maven下載的1.4.0的shiro包, 依然存在反序列的點


1
2
3
4
5
6
7
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}
return this.deserialize(bytes);
}
通過decrypt, aes解密以後就開始反序列了。

1
2
3
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
}

1
2
3
4
5
6
7
8
9
10
11
12
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
高版本shiro只是沒有在AbstractRememberMeManager中硬編碼了AES的key, 可是在JEECMS當中, 又再次硬編碼了AES的key
/src/main/webapp/WEB-INF/config/shiro-context.xml

1
2
3
4
5
<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean>
直接使用這個AES key就能打反序列了。
看了下JEECMS的jar包, 打反序列版本比較合適的爲C3P0的jar包。
JEECMS的C3P0包版本和ysoserial自帶的C3P0包版本相同。

1
<c3p0.version>0.9.5.2</c3p0.version>
一開始不知道C3P0這gadget究竟是咋用, 看了下代碼。
/com/mchange/c3p0/0.9.5.2/c3p0-0.9.5.2.jar!/com/mchange/v2/c3p0/impl/PoolBackedDataSourceBase.class

1
2
3
4
5
6
7
8
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch(version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}
繼續調用getObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object getObject() throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}
Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName);
}
return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
調用referenceToObject方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}
Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}
Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
經過URLClassLoader獲取遠程jar包中的類, 而後classforname後, newInstance實例化該類, 調用構造方法。

 

不過在打反序列的時候, 出現了suid錯誤


明明yso的C3P0版本和jeecms的同樣, 可是仍是提示suid錯誤。

由於jeecms中依賴了quartz-scheduler包, 這個包又依賴了0.9.1.1的c3p0. 反序列的時候調用的是老版本的C3P0的包。(這裏我也不太懂我本地爲何調用的是老版本的包, 按理maven解決依賴衝突時 優先最短路徑優先, 應該調用的是0.9.5.2包。而且高版本的C3P0依賴在前,有大哥懂爲啥調用老版本的jar包的麻煩教我一手。)

 

這時候ysoserial的C3P0版本和jeecms的版本就不相同了 suid就不一樣了, 這裏直接修改一下ysoserial的C3P0版本,

text變量的字符串爲ysoserial生成的C3P0 payload base64編碼,

 

 

References1.https://freemarker.apache.org/docs/versions_2_3_19.html2.https://portswigger.net/blog/server-side-template-injection--------------------- 做者:;console.log(document.cookie);// 來源:CSDN 原文:https://blog.csdn.net/weixin_44063566/article/details/88897406 版權聲明:本文爲博主原創文章,轉載請附上博文連接!

相關文章
相關標籤/搜索