标签:isa ssi upload 测试的 OLE decode rtu 老版本 with
之前遇到了一个JEECMS大概看了一下, 测试版本JEECMSV9.3
/src/main/java/com/jeecms/cms/action/member/UeditorAct.java
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
 | 
 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方法
| 
 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方法的检测,就直接发起请求, 并且把请求到的结果输出到文件当中。
| 
 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就可以绕过了。
不过本地测试时, 访问这个jpg文件的结果却是404.
首先来看看保存访问结果的文件的文件名生成方法, 是包含一个月份目录的。
| 
 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这个目录的。
并且在saveRemoteImage方法当中, 并没有”判断这个目录存不存在,如果不存在的话就创建该目录”这种逻辑。
在FileOutputStream时, 如果目录是不存在的话, 会出异常, 所以这里的文件并没有保存上。
要想保存上这个文件, 首先还是得创建这个目录。
在
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
 | 
 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方法,
| 
 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协议。
JEECMS中存在一些可以上传任意文件的点, 只举例一个
/src/main/java/com/jeecms/cms/action/member/SwfUploadAct.java
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
 | 
 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 
 | 
 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编码,


1.https://freemarker.apache.org/docs/versions_2_3_19.html
2.https://portswigger.net/blog/server-side-template-injection
Some vulnerabilities in JEECMSV9
标签:isa ssi upload 测试的 OLE decode rtu 老版本 with
原文地址:https://www.cnblogs.com/nul1/p/13953864.html