Fastjson漏洞分析
本篇文章以1.2.24版本出现的反序列化漏洞为基础学习Fastjson个版本漏洞,及POC。
环境准备
- java version “1.8.0_152”
- idea 2023.1
- FastJson 1.2.24
漏洞原理1.2.24
我们先学习一下1.2.24的漏洞原理
演示demo
在学习之前我们先写一个测试基类。
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| package org.fvuln;
import java.util.HashMap; import java.util.Map;
public class User { public boolean isB() { System.out.println("调用isB b"); return b; }
public void setB(boolean b) { System.out.println("调用setB b"); this.b = b; }
public int getAge() { System.out.println("调用getName age"); return age; }
public void setAge(int age) { System.out.println("调用setName age"); this.age = age; }
public Flag getFlag() { System.out.println("调用getName flag"); return flag; }
public void setFlag(Flag flag) { System.out.println("调用setName flag"); this.flag = flag; }
public String getName() { System.out.println("调用getName name"); return name; }
public void setName(String name) { System.out.println("调用setName name"); this.name = name; }
private String name; private int age; private Flag flag; private boolean b; private Map<String, String> map; public int i;
public Map getMap() { System.out.println("调用getMap map"); return map; }
@Override public String toString() { System.out.println("调用User toString"); return "User{" + "name='" + name + '\'' + ", age=" + age + ", flag=" + flag + ", map=" + map + ", i=" + i + '}'; }
public User() { System.out.println("调用User无参构造"); this.name = "f19t"; this.age = 8; this.i = 2; this.map = new HashMap<String, String>(); map.put("1", "2"); this.flag = new Flag(); }
}
class Flag { private String flag;
public String getFlag() { System.out.println("调用getName flag类的flag"); return flag; }
public void setFlag(String flag) { System.out.println("调用setName flag类的flag"); this.flag = flag; }
public Flag() { System.out.println("调用flag class无参构造函数"); this.flag = "flag{123123}"; } }
|
下面是Mian程序,可以根据自己的需要设置断点和设置注释
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
| package org.fvuln;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
public class Main { public static void main(String[] args) throws ClassNotFoundException { User user = new User(); System.out.println("================"); String serJson = JSON.toJSONString(user); System.out.println(serJson); System.out.println("================"); System.out.println(JSON.parse(serJson).getClass()); System.out.println("================"); System.out.println(JSON.parseObject(serJson).getClass()); System.out.println("================"); System.out.println(JSON.parseObject(serJson, User.class).getClass()); System.out.println("================"); String sertest = "{\"@type\":\"org.fvuln.User\",\"age\":8,\"b\":false,\"flag\":{\"flag\":\"flag{123123}\"},\"i\":2,\"map\":{\"1\":\"2\"},\"name\":\"f19t\"}"; System.out.println(JSON.parse(sertest).getClass()); System.out.println("================"); System.out.println(JSON.parseObject(sertest).getClass()); System.out.println("================"); System.out.println(JSON.parseObject(sertest, User.class).getClass()); System.out.println("================"); } }
|
Fastjson反序列化效果
这里我们先运行这三行
运行结果如下:
我们可以发现parseObject会调用全部get与set方法、parse只会调用全部set方法与部分get方法。
下图是parseObject方法我们可以看到他是调用了parse,那么接下来我们重点分析一下parse这条调用路径。
把其他代码都注释掉只留下下面两句关键代码,打断点进行分析。
Json字符串处理
第一步、首先parse会调用自己的重载函数。进入重载函数后给关键代码下断点。
首先进入到
1
| DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
|
这里需要注意:
- this.symbolTable的值为[null, null, null, null, $ref, 其他 @type,091]
- 大括号 “{}”: 在Fastjson中,大括号表示一个JSON对象的开始和结束。
- 方括号 “[]”: 在Fastjson中,方括号表示一个JSON数组的开始和结束。
然后进入到
1
| Object value = parser.parse();
|
进入到case12
走到关键代码
skipWhitespace()是去除回车换行等字符串的函数。
接着走到关键代码
1
| key = lexer.scanSymbol(this.symbolTable, '"');
|
接着就是通过双引号去分割取值
这里取出第一个双引号包裹的字符串”@type”
接着会走到key是”@type”的if分支
这里得到ref为org.fvuln.User
这里可以看到存在一个mappings一个数组,用来判断是不是白名单
接着判断是不是[开头的、还判断是不是L开头;结尾的,如果是则缺[1,-1]区间的值进行类加载。
因为我们这里都不满足,所以会走到当前线程的的类加载器。并且将我们的class加入到mappings里面。
反序列化处理
接着会走到生成ObjectDeserializer对象这个位置
先是从derializers获取类加载,如果找不到的话会继续向下走。
最后会通过clazz.getName();获取类名,并将’$’转换成.
接着进行类黑名单校验。
接着会走到createJavaBeanDeserializer
JavaBeanInfo.build
接着会走到build
build是是获取目标类变量名field与函数setter、getter的地方,有些不同点记录在下方:
- Methods获取方式为Method[] methods = clazz.getMethods(); 然后通过if判断是否添加到JavaBeanInfo
- Field是通过methodName里面的字符串拼接得到的
添加getter与setter的主要三个for循环
添加setter
这里首先获取所有Methods,然后遍历循环这些Methods,然后经过一些if判断,取出了set开头的函数。
这里因为boolean类型的Field如果找不到的话,会通过拼接is+XXX方式。最后将确定的setter与变量名称添加到JavaBeanInfo里面。
添加public的变量
Field[] var31 = clazz.getFields();也就是说public的变量可以添加到JavaBeanInfo里面。
这个地方没调试,估计是直接反射赋值。
添加getter
getter函数的添加是这里的第三个for循环。
这里也是获取所有Methods,然后遍历循环这些Methods,然后经过一些if判断,取出了get开头的函数。
不过这里的if判断需要注意:
1 2
| methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())
|
这里简要分析一下:
- methodName.length() >= 4 应该是说geta()多一个字符串就大于4了
- !Modifier.isStatic(method.getModifiers()) 不是是静态函数
- Character.isUpperCase(methodName.charAt(3)) 第四个字母要大写
- method.getParameterTypes().length == 0 函数没有参数
- getReturnType()是判断是否集成这些某个类
我们最开始写的map例子就是为了验证这个地方。
进入到getter for循环有个坑,就是这个地方。
我们进入getField可以看到。
这个地方的逻辑顺序是先添加setter和public的一些变量,如果fieldList里面存在了与getter一样名称的变量,则不会添加getter,也就是说如果fieldList里面之前被setter和public变量添加过Field,那么就不会添加getter。
可以将我们最开始的基类,取消注释setter然后再运行,就会发现没有getter了。
最后可以获得包含JavaBeanInfo的deserializer了。
反序列化
接着跟进deserializer.deserialze()。
接着会创建对象
然后就进行fieldDeser.setValue(object, fieldValue);设置值了。
value的值通过lexer.scanxxx获取。
设置值的时候就会进行函数调用了。
写在最后
本篇文章讲了Fastjson1.2.24产生的漏洞原理,下一篇文件讲解各个版本漏洞产生的原因,以及POC和利用Fastjson漏洞的一些技巧。