之前遇到了一个JEECMS大概看了一下, 测试版本JEECMSV9.3
SSRF /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 @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方法
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 @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方法,
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.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); 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()); 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)){ 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 <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编码,
References 1.https://freemarker.apache.org/docs/versions_2_3_19.html 2.https://portswigger.net/blog/server-side-template-injection