Java安全-log4j漏洞
Java安全- log4j漏洞
Log4j2 基础
理解
先看一下log4j是什么,是用来做什么的
Log4j是一个Java日志组件,通过日志记录器接口,为程序提供了灵活的配置选项,可以将不同级别的消息输出到不同的目的地,如控制台,文件,数据库等。Log4j可以帮助开发人员更好地调试应用程序,同时也方便了运维人员对应用程序进行监控和故障排查
环境
- jdk8u65
网上有很多说 jdk8u191 之后就不行了,其实不是的;高版本 jdk 是有绕过手段的
- Log4j2 2.14.1
- CC 3.2.1 (最好是)
Demo 实现
- 开发的话,其实也不难,因为作为组件的话,如果要实现组件功能的话实现配置即可。
log4j 和 log4j2 都是日志管理工具,相比于 log4j,log4j2 一步步变得越来越主流,现在市场很很多的项目都是 slf4j + log4j2
- 首先要实现 Log4j2 的组件应用,先是 Pom.xml
1 | |
这里,我们简单用 xml 的方式来实现,文件如下
这个文件名要设置成log4j2.xml,Log4j2默认会查找log4j2.xml配置文件
1 | |
然后写一个 demo
1 | |
实际开发场景
刚刚在开头时看到log4j作为日志组件,是可以从数据库中获取信息并输出的
在实际利用时,Log4j一般可以结合数据库进行使用
比如从数据库获取到了一个 username ,要把它登录进来的信息打印到日志里面,这个路径一般有一个 /logs 的文件夹的
1 | |

Log4j2 漏洞成因
影响版本
- 2.x <= log4j <= 2.15.0-rc1
漏洞原理
可以看到在 logger.info("User {} login in!", username); 的这个地方,实际上 – username 这个参数是可控的。我输入 username 确实是用户可控的。
那么这里,我们尝试输入一下其他的呢?
我们将 username 修改为 String username = "${java:os}";,再跑一下看看

发现这里实际上是可以实现rce的
原因是Log4j提供了Lookups的能力,简单来说就是变量替换的能力
查看官方文件,可以看到log4j组件提供了lookup的功能:Log4j – Log4j 2 查找 - Apache 日志服务
- 问题出在基于jndi的lookup,我们早在之前说过直接调用 lookup() 是会存在漏洞的

在Log4j将要输出的日志拼接成字符串之后,它会去判断字符串中是否包含${和},如果包含了,就会当作变量交给org.apache.logging.log4j.core.lookup.StrSubstitutor这个类去处理,根据前缀调用不同的解析器,其中JNDI解析器就为本次漏洞源头
Log4j2 漏洞复现与分析
漏洞复现
导入依赖
1 | |
依旧利用yakit起一个ldap服务器,这个log4j的漏洞主要是利用jndi注入去打的

执行以下代码,生成该类的logger类,调用logger的error方法,payload放入,即可实现JNDI
1 | |
运行一下顺利弹出计算器

调试分析
下个断点在 PatternLayout 这个类下的 toSerializable() 方法

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

跟进 format() 方法,这个 format() 方法可以把它当作是处理字符串的一个方法,具体如何处理是根据具体情况重写的
因为这是一个循环来遍历 formatters 的,中间会做很多数据处理的工作,这都不重要,但是有一个地方特别重要,在 i = 7 的时候进入到了另外的一个 format 处理方法,如图
可以看到进入的方法类名是MessagePatternConverter.class,format方法会调用.getMessage方法,这里就会处理我们传入的东西

对msg处理之后会判断是否是 Log4j2 的 lookups 功能,这里我们是 lookups 功能,所以可以继续往下走

会遍历 workingBuilder 来进行判断;如果 workingBuilder 中存在 ${ ,那么就会取出从 $ 开始知道最后的字符串,上图的 value 就是我们输入的 payload

感觉遍历这部分不是很重要,我们直接跟进replace方法,replace() 方法里面调用了 substitute() 方法
继续跟进 substitute() 方法
进入到这个方法的while循环里,这个循环中的一系列操作就是会把${}里的东西取出来,赋给varName

之后将 varName 作为变量传入了 resolveVariable 函数

跟进到 resolveVariable 函数中,发现这个方法就是用来寻找对应解析器的

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

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

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

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

利用分隔符和多个 ${} 绕过
- 例如这个 payload
1 | |
通过 lower 和 upper 绕过
这一点,因为我们之前说允许的字段是这一些date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,其中就有 lower 和 upper
同时也可以利用 lower 和 upper 来进行 bypass 关键字
1 | |
同时也可以利用一些特殊字符的大小写转化的问题
ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)
1 | |
由于这玩意儿测试过程中随便插都行,现在数据传输很多都是 json 形式,所以在 json 中我们也可以进行尝试
像 Jackson 和 fastjson 又有 unicode 和 hex 的编码特性,所以就可以尝试编码绕过
1 | |
总结一些 payload
- 原始payload
1 | |
对应的绕过手段
1 | |