升級過log4j,卻還沒搞懂log4j漏洞的本質?


摘要: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:}來獲取當前機器中對應的域名對象進行打印,來確認網絡請求失敗時,是否是dns獲取有問題。

這也就是log4j為啥要引入jndi的原因,可以更方便地獲取一些可打印的對象進行日志統計。

然而,jndi還支持通過RMI/LDAP+url字符串,來尋找並獲取一個遠程對象
這個尋找遠程對象的操作,就是此次漏洞的核心問題所在。

這里只講RMI。LDAP類似,就不再論述。

Q:RMI是什么?
A:RMI,RemoteMethodInvocation。

具體含義:

  • 遠程服務器實現具體的Java方法並提供接口
  • 客戶端本地僅需根據接口類的定義,提供相應的參數即可調用遠程方法

在RMI中,實際上就是返回了一個stub(樁)調用對象給客戶端,然后客戶都用這個stub對象去做遠程調用。

這樣客戶端就不用關心背后網絡怎么寫的
甚至不用知道對方服務是什么端口或者ip
因此也不需要寫sokect的一堆方法搞半天了,也避免了總是修改訪問的url啥的。

具體過程如下:

  1. Server端監聽一個端口,這個端口是JVM隨機選擇的;
  2. Client端並不知道Server遠程對象的通信地址和端口,但是Stub中包含了這些信息,並封裝了底層網絡操作;
  3. Client端可以調用Stub上的方法;
  4. Stub連接到Server端監聽的通信端口並提交參數;
  5. 遠程Server端上執行具體的方法,並返回結果給Stub;
  6. 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就是問題的根本原因。

整個利用流程如下:

  1. 目標代碼中調用了InitialContext.lookup(URI),且URI為用戶可控;
  2. 攻擊者控制URI參數為惡意的RMI服務地址,如:rmi://hacker_rmi_server//name;
  3. 攻擊者RMI服務器向目標返回一個Reference對象,Reference對象中指定某個精心構造的Factory類;
  4. 目標在進行lookup()操作時,會動態加載並實例化Factory類,接着調用factory.getObjectInstance()獲取外部遠程對象實例;
  5. 攻擊者可以在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的調用方式,未來應該會被更加謹慎地使用了。

 

點擊關注,第一時間了解華為雲新鮮技術~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM