|
本帖最后由 wangqiang 于 2022-4-7 10:54 编辑
java安全 fastjson反序列化基础分析
原创 Met32 moonsec
2022-04-05 10:00
转载自https://mp.weixin.qq.com/s?__biz=MzAwMjc0NTEzMw==&mid=2653578464&idx=1&sn=d51a2c7134c962df03f26e4b44c70671&chksm=811b72a2b66cfbb42551ae60f1466d3fa61147606b7b1d0b679f97a68c9e18534a836fc197ac&mpshare=1&scene=23&srcid=0405lThrYE7jLFnFYCLZR0vk&sharer_sharetime=1649124437825&sharer_shareid=ee83a55e0b955b99e8343acbb61916b7#rd
fastjson反序列化基础分析
本文主要来讲解fastjson反序列化内容。
依赖包如下:
- <dependency><groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.24</version>
- </dependency>
复制代码
在学习该漏洞之前,先整点前置知识。贴出一个User类。主要来发现fastjson的几个小特性
- <font color="#000000"><font style="background-color:white">public class User {</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">public String name;</font></font>
- <font color="#000000"><font style="background-color:white">public User() {</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">System.out.println("构造函数...");</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">public String getName() {</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">System.out.println("调用了get方法");</font></font>
- <font color="#000000"><font style="background-color:white">return name;</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">public void setName(String name) {</font></font>
- <font color="#000000"><font style="background-color:white">System.out.println("调用了set方法");</font></font>
- <font color="#000000"><font style="background-color:white">this.name = name;</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">@Override</font></font>
- <font color="#000000"><font style="background-color:white">public String toString() {</font></font>
- <font color="#000000"><font style="background-color:white">return "User{" +</font></font>
- <font color="#000000"><font style="background-color:white">"name='" + name + '\'' +</font></font>
- <font color="#000000"><font style="background-color:white">'}';</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
复制代码
在进行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"}";<img width="15" _height="15" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" border="0" alt="">
复制代码
既然有了前面的知识铺垫,接下来准备一个恶意类,该类的set方法会传递参数进去,并通过
Runtime执行
- <font color="#000000"><font style="background-color:white">public class Evil {</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">String cmd;</font></font>
- <font color="#000000"><font style="background-color:white">String a;</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">public Evil(){}</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">public String getCmd() {</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">System.out.println("get方法");</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">return cmd;</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">public void setCmd(String cmd) throws IOException {</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">System.out.println("set方法");</font></font>
- <font color="#000000"><font style="background-color:white">
- </font></font><font color="#000000"><font style="background-color:white">this.cmd = cmd;</font></font>
- <font color="#000000"><font style="background-color:white">Runtime.getRuntime().exec(cmd);</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">public void setA(String a) throws IOException {</font></font>
- <font color="#000000"><font style="background-color:white">Runtime.getRuntime().exec(a);</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
- <font color="#000000"><font style="background-color:white">}</font></font>
复制代码
前面知道了parseObject会调用到set方法。那么就给该方法传个值吧
- {"@type":"com.example.fastjson.Evil","cmd":"calc"}<img width="15" _height="15" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" border="0" alt="">
复制代码
在反序列化之后,可以看到成功执行了命令
基础的玩法是远远不够的,接下来更深一层的看是如何调用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"}<img width="15" _height="15" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" border="0" alt="">
复制代码
在下方会获取到@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
- <font style="background-color:white"><font color="#000000">for(i = 0; i < var29; ++i) {</font></font>
- <font style="background-color:white"><font color="#000000">
- </font></font><font style="background-color:white"><font color="#000000">method = var30;</font></font>
- <font style="background-color:white"><font color="#000000">ordinal = 0;</font></font>
- <font style="background-color:white"><font color="#000000">int serialzeFeatures = 0;</font></font>
- <font style="background-color:white"><font color="#000000">parserFeatures = 0;</font></font>
- <font style="background-color:white"><font color="#000000">String methodName = method.getName();</font></font>
- <font style="background-color:white"><font color="#000000">if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) &&</font></font>
- <font style="background-color:white"><font color="#000000">(method.getReturnType().equals(Void.TYPE) ||</font></font>
- <font style="background-color:white"><font color="#000000">method.getReturnType().equals(method.getDeclaringClass()))) {</font></font>
- <font style="background-color:white"><font color="#000000">Class<?>[] types = method.getParameterTypes();</font></font>
- <font style="background-color:white"><font color="#000000">if (types.length == 1) {</font></font>
- <font style="background-color:white"><font color="#000000">
- </font></font><font style="background-color:white"><font color="#000000">annotation = (JSONField)method.getAnnotation(JSONField.class);</font></font>
- <font style="background-color:white"><font color="#000000">if (annotation == null) {</font></font>
- <font style="background-color:white"><font color="#000000">
- </font></font><font style="background-color:white"><font color="#000000">annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);</font></font>
- <font style="background-color:white"><font color="#000000">}</font></font>
- <font style="background-color:white"><font color="#000000">if (annotation != null) {</font></font>
- <font style="background-color:white"><font color="#000000">
- </font></font><font style="background-color:white"><font color="#000000">if (!annotation.deserialize()) {</font></font>
- <font style="background-color:white"><font color="#000000">continue;</font></font>
- <font style="background-color:white"><font color="#000000">}</font></font>
复制代码
当执行完成build之后,beanInfo则会存储该类的一些信息。
最后跑完这个方法之后,会执行deserializer.deserialze方法。而该类无法断点跟进,前辈们师傅说是ASM机制生成的临时代码,所以跟不了断点
直接放行即可,就会自行调用到set处
|
|