内容简介:Apache Log4j 反序列化分析—【CVE-2017-5645】
apache Log4j 组件漏洞描述:
CVE-2017-5645: Apache Log4j socket receiver deserialization vulnerability Severity: High CVSS Base Score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P) Vendor: The Apache Software Foundation Versions Affected: all versions from 2.0-alpha1 to 2.8.1 Description: When using the TCP socket server or UDP socket server to receive serialized log events from another application, a specially crafted binary payload can be sent that, when deserialized, can execute arbitrary code. Mitigation: Java 7+ users should migrate to version 2.8.2 or avoid using the socket server classes. Java 6 users should avoid using the TCP or UDP socket server classes, or they can manually backport the security fix from 2.8.2: <https://git-wip-us.apache.org/repos/asf?p=logging-log4j2. git;h=5dcc192>
Log4j简介
在应用程序中添加日志记录最普通的做法就是在代码中嵌入许多的打印语句,这些打印语句可以输出到控制台或文件中,比较好的做法就是构造一个日志操作类来封装此类操作,而不是让一系列的打印语句充斥了代码的主体。 Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。 Log4j在工程中可以易用,方便等代替了 System.out 等打印语句,它是 Java 下最流行的日志输入工具,一些著名的开源项目,像spring、hibernate、struts都使用该 工具 作为日志输入工具,可以帮助调试(有时候debug是发挥不了作用的)和分析。
流程图
TCP
UDP
漏洞分析
其实对java的远程服务这一块没有了解过,但是可以在漏洞描述中看见这么一句:
TCP or UDP socket server classes
,那么就去全文搜一下tcp udp socket server 相关关键字
其实是有对应的 TcpSocketServer 和 UdpSocketServer 的类
TCP
那么我们直接去查看TcpSocketServer
先看一下类结构
CommandLineArguments类是自定义处理命令行参数用的
SocketHandler类用于处理客户端连接
然后就是三个构造函数,接着 create*SocketServer
函数用于创建各类服务端,main用于直接运行的,extract用于创建一个 ServerSocket
并返回
run函数存在是因为 TcpSocketServer
继承于 AbstractSocketServer
,而这个抽象类继承了Runable接口。在 TcpSocketServer
类中,run函数用于接受客户端连接并且交于SocketHandler处理连接
shutdown函数作用是清理并关闭线程
现在,我们从创建一个Tcp的远程日志服务开始跟起,在源码中,有TcpSerializedSocketServerTest类向我们展示了这一操作,虽然它的功能仅仅是用于测试..
在TcpSerializedSocketServerTest中,只有一个构造函数和createLayout函数,剩下的两个如上图所示,其中createLayout函数并没有具体的代码,所以和构造函数一样我们选择忽略..
@BeforeClass注解表示该函数用于测试类实例化前执行的函数,并且针对所有测试函数,它只会执行一次,就像static块一样
@AfterClass注解表示测试类实例化后只会执行一次
我们直接看setupClass函数,它先是获取了一下Log的上下文然后就调用了TcpSocketServer的 createSerializedSocketServer
函数,传入的是一个int,跟过去看看啥情况
在 AbstractSocketServerTest
函数里
继续跟进getNextAvailable,就不贴出来了,说明一下功能,就是从1100端口开始计算,返回一个当前的端口中空闲着的最小端口号
我们先不急着跟进 createSerializedSocketServer
函数,继续往下看,它调用了TcpSocketServer的startNewThread函数,这个函数具体实现在 AbstractSocketServer
中
直接start了,看一下 server.startNewThread的server是啥类型的
因为是TcpSocketServer的对象,所以start后执行就是TcpSocketServer里的run函数
现在我们跟进 createSerializedSocketServer
函数看看,注意形参是int类型的
参数和返回值都说清楚了,int就是用于监听的端口,返回一个新的socket server,新建的TcpSocketServer带入了 ObjectInputStreaLogEventBridge
对象
当Test类拿到TcpSocketServer对象后,就会执行其run函数,我们看一下详细内容
函数体有点长,就贴代码不贴图了
/** * Accept incoming events and processes them. */ @Override public void run() { final EntryMessage entry = logger.traceEntry(); while (isActive()) { if (serverSocket.isClosed()) { return; } try { // Accept incoming connections. logger.debug("Listening for a connection {}...", serverSocket); final Socket clientSocket = serverSocket.accept(); logger.debug("Acepted connection on {}...", serverSocket); logger.debug("Socket accepted: {}", clientSocket); clientSocket.setSoLinger(true, 0); // accept() will block until a client connects to the server. // If execution reaches this point, then it means that a client // socket has been accepted. final SocketHandler handler = new SocketHandler(clientSocket); handlers.put(Long.valueOf(handler.getId()), handler); handler.start(); } catch (final IOException e) { if (serverSocket.isClosed()) { // OK we're done. logger.traceExit(entry); return; } logger.error("Exception encountered on accept. Ignoring. Stack trace :", e); } } for (final Map.Entry<Long, SocketHandler> handlerEntry : handlers.entrySet()) { final SocketHandler handler = handlerEntry.getValue(); handler.shutdown(); try { handler.join(); } catch (final InterruptedException ignored) { // Ignore the exception } } logger.traceExit(entry); }
一进来就是一个循环,先判断了socket是否关闭,如果没有的话,就接受从客户端传递的数据,如果已经关闭则退出循环,退出循环后做一些清理工作。
我们来看看接受客户端的情况,如下图
serverSocket就是根据之前传入的int端口号来新建的一个ServerSocket对象。注意红框里,将clientSocket作为参数实例化 SocketHandler
类,然后放入handlers中,handlers是一个ConcurrentMap。最后调用了start函数
我们去看看这个 SocketHandler
具体做了什么
构造函数里,获取了 socket 中的数据流后,传入了 logEventInput
的 wrapStream
中,看看 logEventInput
是一个 LogEventBridge
类型的,还记得在之前的 createSerializedSocketServer
函数中的新建 ObjectInputStreamLogEventBridge
对象吗,这里的logEventInput其实就是这个新建的 ObjectInputStreamLogEventBridge
对象
那么我们跟进 wrapStream函数
发现有 AbstractLogEventBridge
和 ObjectInputStreamLogEventBridge
实现了,这里我们当然选择跟进 ObjectInputStreamLogEventBridge
类中
就将传入的inputStream用 ObjectInputStream
包装一下然后返回了
那么这个SocketHandler中的inputStream就是一个 ObjectInputStream
对象了
我们接着看SocketHandler的run函数
直接将inputStream带入了 logEventInput
的 logEvents
函数中
跟进去看看
肯定是 ObjectInputStreamLogEventBridge
里的 logEvents
如上图,inputStream传入 logEvents
后,直接调用了 readObject
函数,这里就触发了反序列化
UDP
查看UdpSocketServer
先看结构
与TcpSocketServer的结构类似,create*SocketServer函数用于创建接受不同类型数据的Socket Server
我们直接去看他的run函数
其他操作都很正常,接收数据后,提取二进制流赋值给bais,然后带入 wrapStream
处理成 ObjectInputStream
,然后传入 ObjectInputStreamLogEventBridge
中的 logEvents
函数,就和TCP中的触发一样了,直接调用的 readObject
函数
一些其他的尝试
在TCP和UDP中,我们注意到关键点都在于这个 logEventsInput
的类型,如果它是 ObjectInputStreamLogEventBridge
类型,那么在后面调用 logEvents
函数的时候,就会直接调用 readObject
函数造成反序列化
这个 logEvents
函数通过名字就可以判断出,它是用于将接收到的数据做一下日志记录的,所以在其他流程中,也仅仅是为这个记录操作提供前提条件,仅仅是将数据接收、简单处理一下等
那么 logEventsInput
如果是其他类型呢,其他类型的 LogEventBridge
又会对接收到的数据如何进行处理,在处理过程中会不会有问题存在?
我们先去看看 LogEventBridge
的子类有哪些( logEvent
由 LogEventBridge
定义)
LogEventBridge本身是一个接口, AbstractLogEventBridge
是 LogEventBridge
的抽象类,所以我们的关注点在于 *StreamLogEventBridge
,之前的反序列化是由于 ObjectInputStreamLogEventBridge
触发的,那么我们去看看 InputStreamLogEventBridge
的 logEvents
函数
XmlInputStreamLogEvnetBridge和JsonInputStreamLogEventBridge并没有实现logEvents函数
@Override public void logEvents(final InputStream inputStream, final LogEventListener logEventListener) throws IOException { String workingText = Strings.EMPTY; try { // Allocate buffer once final byte[] buffer = new byte[bufferSize]; String textRemains = workingText = Strings.EMPTY; while (true) { // Process until the stream is EOF. final int streamReadLength = inputStream.read(buffer); if (streamReadLength == END) { // The input stream is EOF break; } final String text = workingText = textRemains + new String(buffer, 0, streamReadLength, charset); int beginIndex = 0; while (true) { // Extract and log all XML events in the buffer final int[] pair = getEventIndices(text, beginIndex); final int eventStartMarkerIndex = pair[0]; if (eventStartMarkerIndex < 0) { // No more events or partial XML only in the buffer. // Save the unprocessed string part textRemains = text.substring(beginIndex); break; } final int eventEndMarkerIndex = pair[1]; if (eventEndMarkerIndex > 0) { final int eventEndXmlIndex = eventEndMarkerIndex + eventEndMarker.length(); final String textEvent = workingText = text.substring(eventStartMarkerIndex, eventEndXmlIndex); final LogEvent logEvent = unmarshal(textEvent); logEventListener.log(logEvent); beginIndex = eventEndXmlIndex; } else { // No more events or partial XML only in the buffer. // Save the unprocessed string part textRemains = text.substring(beginIndex); break; } } } } catch (final IOException ex) { logger.error(workingText, ex); } }
主要代码如下图
先在字符数组中截取特定标签之间的字符,然后传入 unmarshal 函数进行反序列化操作,不过这里的反序列化是由jackson框架操作
unmarshal函数:
但是里面的ObjectMapper没有调用enableDefaultTyping函数...
InputStreamLogEventBridge可以解析json和xml,json的反序列化没有找到利用点,而解析xml也是直接解析的 <Event></Event>
或是 <Event
标签里的数据,技术水平限制了我的想象,不知道代码中还有啥处理问题了....
漏洞通报: http://seclists.org/oss-sec/2017/q2/78
log4j简介: https://www.cnblogs.com/shanheyongmu/p/5650632.html
log4j-core源码分析: http://www.blogjava.net/DLevin/archive/2012/06/28/381667.html
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。