zoukankan      html  css  js  c++  java
  • Some vulnerabilities in JEECMSV9

    之前遇到了一个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就可以绕过了。
    -w976
    不过本地测试时, 访问这个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这个目录的。
    -w539

    并且在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.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 接下来就会出现空指针异常。

    -w1412
    上传之后, 就成功创建了目录。
    -w587

    再SSRF
    -w1029
    -w744

    不过发起请求的httpClientBuilder, 仅支持HTTP/HTTPS协议。
    -w770

    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());
    }
    }

    在这个方法中, 上传时没有检查文件的后缀,
    -w396

    从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") }

    -w1344

    -w770

    在新版本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目录下的文件。
    -w1222

    -w834

    反序列

    JEECMS中使用了shiro, 版本为

    1
    <shiro.version>1.4.0</shiro.version>

    老版本shiro(1.2.4)曾爆过一个反序列,
    看了一下maven下载的1.4.0的shiro包, 依然存在反序列的点
    -w899

    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实例化该类, 调用构造方法。

    -w867

    不过在打反序列的时候, 出现了suid错误
    -w966

    明明yso的C3P0版本和jeecms的一样, 但是还是提示suid错误。

    因为jeecms中依赖了quartz-scheduler包, 这个包又依赖了0.9.1.1的c3p0. 反序列的时候调用的是老版本的C3P0的包。(这里我也不太懂我本地为什么调用的是老版本的包, 按理maven解决依赖冲突时 优先最短路径优先, 应该调用的是0.9.5.2包。并且高版本的C3P0依赖在前,有大哥懂为啥调用老版本的jar包的麻烦教我一手。)

    -w476

    这时候ysoserial的C3P0版本和jeecms的版本就不相同了 suid就不同了, 这里直接修改一下ysoserial的C3P0版本,
    -w488
    text变量的字符串为ysoserial生成的C3P0 payload base64编码,
    -w715

    -w1399

    -w1136

    References

    1.https://freemarker.apache.org/docs/versions_2_3_19.html
    2.https://portswigger.net/blog/server-side-template-injection

  • 相关阅读:
    一致性hash算法
    运算符的重载
    HTTP协议详解
    SOA 新业务语言 新系统架构——什么是SOA
    js中几种实用的跨域方法原理详解
    Linq基于两个属性的分组
    BackBone结合ASP.NET MVC实现页面路由操作
    CSS学习总结
    单页应用 WebApp SPA 骨架 框架 路由 页面切换 转场
    2016年最值得学习的五大开源项目
  • 原文地址:https://www.cnblogs.com/nul1/p/13953864.html
Copyright © 2011-2022 走看看