本文主要来讲解fastjson反序列化内容。
依赖包如下:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
在学习该漏洞之前,先整点前置知识。贴出一个User类。主要来发现fastjson的几个小特性
public class User {
public String name;
public User() {
System.out.println("构造函数...");
}
public String getName() {
System.out.println("调用了get方法");
return name;
}
public void setName(String name) {
System.out.println("调用了set方法");
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
'}';
}
}
在进行toJSONString()的时候,可以发现会调用到get方法。(切记 构造函数和set方法是初始化和赋值时调用)
Object obj = JSON.parseObject(res,User.class);//返回JSONObject 如果加上User.class则是
User类型
Object obj2 = JSON.parse(res);//返回类 User
System.out.println(obj.getClass().getName());
System.out.println(obj2.getClass().getName()
);
可以看到在使用parseObject进行反序列化之后,会调用到set方法。而parse没有调用
在了解完基础用法之后,我从网上找了一段解释的比较专业的一段话:
为了让开发者更加方便的使用Fastjson的一系列功能,同时也为了方便自省,Fastjson设计了一个叫autoType的功能,也就是网上常⻅的 @type 。
只要JSON字符串中包含 @type ,那么 @type 后的属性,就会被当做是 @type 所指定的类的属性,从而在不传递第二个参数的情况下让Fastjson明确要还原的类。
这是什么意思呢?相信很多小伙伴们复现过fastjson这个漏洞,而下方这条代码,是不是非常像你用到的
“Payload”。用通俗的话来理解,这是一个用来被反序列化的东西,@type后面指定的是类名。
cmd指的是该类的属性,calc为属性的值。
{"@type":"com.example.fastjson.Evil","cmd":"calc"}";
既然有了前面的知识铺垫,接下来准备一个恶意类,该类的set方法会传递参数进去,并通过
Runtime执行
public class Evil {
String cmd;
String a;
public Evil(){}
public String getCmd() {
System.out.println("get方法");
return cmd;
}
public void setCmd(String cmd) throws IOException {
System.out.println("set方法");
this.cmd = cmd;
Runtime.getRuntime().exec(cmd);
}
public void setA(String a) throws IOException {
Runtime.getRuntime().exec(a);
}
}
前面知道了parseObject会调用到set方法。那么就给该方法传个值吧
{"@type":"com.example.fastjson.Evil","cmd":"calc"}
在反序列化之后,可以看到成功执行了命令
基础的玩法是远远不够的,接下来更深一层的看是如何调用set方法的?
跟进parseObject方法中会调用到parse方法中,在跟进之后会调用到DefaultJSONParser类中
在DefaultJSONParser类中发现设置token=12,这个后面会用到,先记住
当再几次F8就会调用到DefaultJSONParser类中的parse方法。
这里的token就是刚才赋值的那个,为12
跟进parseObject方法,前面代码比较多,直接跟到162行的while处单步跟进162行,是对一些r n t f 空格等进行不解析
继续分析,ch此时为单引号,剧透一下,其实就是"@type" 最前面的这个单引号。
之后会判断是否开启了AllowArbitraryCommas,如果开启了,就会进入到后面的while
而while则是对"逗号,"进行忽略。所以此处是可以对某些waf进行绕过的。
假设某waf只会校验前几个字符,如果为@type就凉拌掉,所以此时将payload写为
{,,,,,,"@type":"com.example.fastjson.Evil","cmd":"calc"}
在下方会获取到@type,scanSymbol会将获取目前的字符以及到第二个参数为止的字符中间的内容获取"@type" 也就是@type
在274行中,会判断key是否为@type以及DisableSpecialKeyDetect是否开启
稍后就会生成一个Class类。ref则是Evil这个恶意类
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
最终在318行跟进
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
跟进后发现,这里进行了一层校验,如果为Thread则会异常
继续跑到411行,执行了createJavaBeanDeserializer
跟进createJavaBeanDeserializer发现了480行的build方法
而该方法会获取属性和方法。
在继续往后跑,后面会有个重要点,这里贴出一部分
可以看到if处:
-
方法名长度不能小于4
-
不能是静态方法
-
返回的类型必须是void 或者是自己本身
-
传入参数个数必须为1
-
方法开头必须是se
for(i = 0; i < var29; ++i) {
method = var30[i];
ordinal = 0;
int serialzeFeatures = 0;
parserFeatures = 0;
String methodName = method.getName();
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) &&
(method.getReturnType().equals(Void.TYPE) ||
method.getReturnType().equals(method.getDeclaringClass()))) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 1) {
annotation = (JSONField)method.getAnnotation(JSONField.class);
if (annotation == null) {
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
}
if (annotation != null) {
if (!annotation.deserialize()) {
continue;
}
当执行完成build之后,beanInfo则会存储该类的一些信息。
最后跑完这个方法之后,会执行deserializer.deserialze方法。而该类无法断点跟进,前辈们师傅说是ASM机制生成的临时代码,所以跟不了断点
直接放行即可,就会自行调用到set处
关注公众号
公众号长期更新安全类文章,关注公众号,以便下次轻松查阅
原文始发于微信公众号(moonsec):java安全 fastjson反序列化基础分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论