摘要:log4j遠端程式碼漏洞問題被大範圍曝光後已經有一段時間了,今天完整講清JNDI和RMI以及該漏洞的深層原因。
本文分享自華為雲社群《升級過log4j,卻還沒搞懂log4j漏洞的本質?為你完整講清jndi、rmi以及該漏洞的深層原因!》,作者:breakDraw。
log4j遠端程式碼漏洞問題被大範圍曝光後已經有一段時間了。
很多人只能看到一個“彈出一個計算器”的演示,於是內心想著“哦,就是執行任意程式碼,啟動個計算器”,卻對這個漏洞的原理不甚瞭解。
而對於java開發應用不是非常深的同學來講,jndi、rmi更是很陌生的名詞。
這裡會以不斷提問的方式,逐步推進這個問題的解答,一步步揭開這個漏洞的本質,並給出對這個漏洞的思考。
Q:log4j裡的”${}“符號是什麼?有什麼用?
A:可以通過${}的方式,列印一些特殊的值到日誌中。
例如${hostName}就可以列印主機名
${java:vm}列印jvm資訊
${thread:threadName}就可以列印執行緒名
當你把這個值作為日誌的引數,就會列印出來這些值而非原引數名字。
可以理解為log4j的功能更強大了,不需要自己寫java程式碼來列印這些資訊,直接用一個字串就能搞定這些列印。
上面這些都是要實現對應的Lookup類才能做的,即要麼log4j內建,要麼我們自己新增。
Q:上面這個列印本機資訊的是漏洞的原因嗎?看起來好象可以在機器裡執行奇怪的命令?或者檢視檔案路徑?
A:不是的。
上面這些lookup,都是事先定義好的一些loopup字元,並不能做任意的事情!而且就算你發了這些${java.vm}啥的,也只能在服務端列印和收集,你作為攻擊者,是收集不到這些資訊的
真正的原因,是因為log4j支援的${jndi:xxxx},即支援jndi進行lookup來尋找物件並列印。
Q:什麼是JNDI?
A:JavaNamingandDirectoryInterface(JAVA命名和目錄介面)
簡單說就是可以通過JNDI,在java環境中用一個名字,去lookup尋找一個東西使用。
例如可以直接在自己的Java環境中配置一個資料庫連線,名字叫“java:MySqlDS”
然後別的java程式通過jndi去查詢”java:MysqlDs“,接著就會得到一個資料庫連線。
這樣如果1個機器有多個程式,都要用同一個連線,完全可以修改整個java環境的jndi資料庫物件,然後其他程式就能同時生效了。
Connectionconn=null; //Context就是jdni的類 Contextctx=newInitialContext(); //jndi關鍵方法,通過loopup找一個物件 ObjectdatasourceRef=ctx.lookup("java:MySqlDS"); //引用資料來源 DataSourceds=(Datasource)datasourceRef; conn=ds.getConnection(); ...... c.close();
除了資料庫連線,他還支援loopup找dns,可以弄一個dnsContext然後尋找”sun.com“對應的dns物件
使用JNDI進行高階DNS查詢
這樣log4j裡就可以通過${jndi:dns:http://huaweicloud.com}來獲取當前機器中http://huaweicloud.com對應的域名物件進行列印,來確認網路請求失敗時,是否是dns獲取有問題。
這也就是log4j為啥要引入jndi的原因,可以更方便地獲取一些可列印的物件進行日誌統計。
然而,jndi還支援通過RMI/LDAP+url字串,來尋找並獲取一個遠端物件。
這個尋找遠端物件的操作,就是此次漏洞的核心問題所在。
這裡只講RMI。LDAP類似,就不再論述。
Q:RMI是什麼?
A:RMI,RemoteMethodInvocation。
具體含義:
- 遠端伺服器實現具體的Java方法並提供介面
- 客戶端本地僅需根據介面類的定義,提供相應的引數即可呼叫遠端方法
在RMI中,實際上就是返回了一個stub(樁)呼叫物件給客戶端,然後客戶都用這個stub物件去做遠端呼叫。
這樣客戶端就不用關心背後網路怎麼寫的
甚至不用知道對方服務是什麼埠或者ip
因此也不需要寫sokect的一堆方法搞半天了,也避免了總是修改訪問的url啥的。
具體過程如下:
- Server端監聽一個埠,這個埠是JVM隨機選擇的;
- Client端並不知道Server遠端物件的通訊地址和埠,但是Stub中包含了這些資訊,並封裝了底層網路操作;
- Client端可以呼叫Stub上的方法;
- Stub連線到Server端監聽的通訊埠並提交引數;
- 遠端Server端上執行具體的方法,並返回結果給Stub;
- Stub返回執行結果給Client端,從Client看來就好像是Stub在本地執行了這個方法一樣;
Q:RMI客戶端不需要關心服務端的監聽埠?那客戶端從哪裡拿到stub物件呢?總不可能憑空生成吧
A:服務端那邊可以啟動一個RMI註冊中心服務RMIRegistry,埠設定為統一的1099,ip也是固定的。
然後當客戶端希望拿到某個服務例如訂單服務order的stub物件時,就用”order“這個名字到RMI註冊中心上去請求這個stub
這樣的話,客戶端只需要知道RMI註冊中心即可,不需要知道其他服務的ip、埠,非常節省管理成本。
服務端程式碼長這樣:
//建立一個訂單服務通訊樁 OrderServerStubstub=newOrderServerStub(); //啟動一個RMI註冊中心,埠為1099 LocateRegistry.createRegistry(1099); //把OrderServer這個樁,註冊到rmi://0.0.0.0:1099/order這個url上 Naming.bind("rmi://0.0.0.0:1099/order",stub);
客戶端的程式碼長這樣,可以看到一個loopup就把這個樁找過來了。
然後就能直接呼叫stub裡的queryOrder方法查詢訂單了!
Registryregistry=LocateRegistry.getRegistry("kingx_kali_host",1099); OrderServerStubstub=(OrderServerStub)registry.lookup("hello"); stub.queryOrder("aaa");
Q:那JNDI和RMI又是什麼關係?怎麼就聯絡到一起了
A:上面的程式碼裡,可以看到RMI需要自己寫一段Java程式碼執行。
如果以後你不用RMI來存這個通訊物件了,而是用LDAP之類的,咋辦?難道程式碼都要重新寫然後部署一份嗎?
而如果能用JNDI的方式,通過一個小小的字串,就能拿到,那就簡單了。
那麼當我需要切換通訊物件的獲取方式時,切換JDNI裡的設定即可。
而RMI正好實現了JNDI的spi介面,以至於能支援用JNDI+字串去獲取物件
這裡貼一下SPI的概念:
SPI,全稱為ServiceProviderInterface,是一種服務發現機制。它通過在ClassPath路徑下的META-INF/services資料夾查詢檔案,自動載入檔案裡所定義的類。
這一機制為很多框架擴充套件提供了可能,比如在Dubbo、JDBC中都使用到了SPI機制
- 說人話,spi就是框架方提供一個interface介面,然後只要有人在服務的class發現路徑下寫一個實現類,就能在程式碼裡直接用上。
而log4j裡,正好就支援用${jndi:rmi:x.x.x.x:1099/path}的方式進行RMI物件的獲取。
log4j開發者可能本意只是方便用jndi獲取各種java容器內建物件,沒想到忽略了rmi的獲取方式。
這就導致了我們的服務可能會訪問黑客部署的RMI服務,獲取到一個不可信的遠端呼叫物件。
Q:但是剛才提到,我們只會通過RMI去拿到一個stub,
stub裡的內容僅僅是通過特定的ip+port去做傳送,程式碼是固定的
再怎麼惡意的命令,也只會在RMI註冊中心即黑客的伺服器上執行,怎麼就在我這邊觸發了攻擊?
而且這個stub物件的class檔案在我們伺服器本地並沒有,難道不會報classNotFind異常嗎?
A:某個講RMI注入的文章裡這樣說道:
RMI服務端除了直接繫結遠端物件之外,還可以通過References引用類來繫結一個外部的遠端物件(當前名稱目錄系統之外的物件)。
繫結了Reference之後,服務端會先通過Referenceable.getReference()獲取繫結物件的引用,並且在目錄中儲存。當客戶端在lookup()查詢這個遠端物件時,客戶端會獲取相應的objectfactory,最終通過factory類將reference轉換為具體的物件例項。
- 說人話,就是RMI允許客戶端的java環境中沒有這個stub物件
- RMI服務端(那個1099埠的服務)他會返回給你一個factory(序列化傳過來),讓你呼叫這個factory做轉換。而這個可被序列化生成的factory就是問題的根本原因。
整個利用流程如下:
- 目的碼中呼叫了InitialContext.lookup(URI),且URI為使用者可控;
- 攻擊者控制URI引數為惡意的RMI服務地址,如:rmi://hacker_rmi_server//name;
- 攻擊者RMI伺服器向目標返回一個Reference物件,Reference物件中指定某個精心構造的Factory類;
- 目標在進行lookup()操作時,會動態載入並例項化Factory類,接著呼叫factory.getObjectInstance()獲取外部遠端物件例項;
- 攻擊者可以在Factory類檔案的構造方法、靜態程式碼塊、getObjectInstance()方法等處寫入惡意程式碼,達到RCE的效果;
Q:那麼log4j-core2.15版本又是怎麼改的呢?
A:限定jndi使用的協議,禁止在jndi中用ldap、rmi去呼叫一些遠端的服務。
思考
說實話,這個漏洞影響之所以這麼大,就是因為原理太過簡單,隨便發一段rmi註冊中心的demo和客戶端呼叫demo給別人,他就能復現,甚至用這個方式去攻擊。
為什麼log4j的設計者當時沒有考慮到呢?
很大概率可能是因為jndi的spi機制擴充套件性太強。
也許最初,jndi只支援dns、資料庫driver等物件的命名獲取
但是後來隨著版本更新,JNDP通過SPI機制,支援了RMI、LDAP等實現,而這個是log4j開發者當時沒考慮到的。
換句話說,這是java高可擴充套件性和安全性的一次衝突,因此JNDI的呼叫方式,未來應該會被更加謹慎地使用了。