写给0基础的CVE-2018-1270分析
写给0基础的CVE-2018-1270分析
写在前面
自己的一些分析,比较粗浅,Spring好复杂,可能有很多错误,多多交流。文章比较长,建议使用电脑阅读。
由于利用环境比较特殊,所以基础知识的铺垫特别的多,熟悉Spring的同学可以直接跳到分析。
这个漏洞简单的说就是selector的值没有进行过滤就带进了SpEL表达式,同时SpEL表达式的权限比较高,可以执行任意命令。
基础知识
首先我们需要了解一下什么是Spring。
Spring
简单的说,Spring是一个Java开源框架,一般指的是Spring Framework。但是Spring Framework的配置非常繁琐,所以在此基础上,Spring Boot诞生了,既拥有Spring Framework各种特性,又简化了各种配置。这次漏洞发生在Spring Framework中,所以Spring Boot也会受到影响。
首先,我们需要知道如何使用Spring。我们只需要在我们的包管理工具中把它引入就可以了(比如Maven的pom.xml)。
图为maven的pom.xml
影响的范围
1 | Spring Framework 5.0 to 5.0.4 |
这次漏洞的触发的场景是Spring-Messaging + WebSocket + STOMP
。这一句话里有三个要点:1.什么是Spring-Messaging? 2. 什么是WebSocket?3. 什么是STOMP?
Spring-Messaging
Spring Messaging是Spring4.0为了集成JMS发布的一个新模块,为集成messaging API和消息协议提供支持,属于Spring Framework项目。其代码结构如下图所示:
1 | spring-messaging模块 = base |
这个漏洞主要涉及simp部分。
这里我们需要引入消息代理和消息队列的概念。
WebSocket
WebSocket 协议提供了通过一个套接字实现全双工通信的功能。也能够实现 web浏览器和server间的异步通信,全双工意味着server与浏览器间可以发送和接收消息。
由于HTTP具有无状态,单向请求的特性,使得Server向Client推送消息变得非常繁琐,需要使用轮询的方式(定时一遍遍的询问服务器有没有新的消息),效率非常低,非常浪费资源。WebSocket就在这个时候应运而生。
比较通俗易懂的说,WebSocket就像聊天室模式。Client向Server发起建立WebSocket的请求,一旦建立成功,就像点开了微信的会话窗口。在这个会话中,Client可以说话,Server也可以说话。这样的模式下使Server向Client推送消息变得简单可行。
所以WebSocket是一种在一个TCP连接上能够全双工,双向通信的协议。它是一种与HTTP不同的协议,但是以HTTP作为载体的。主要使用80(ws://)和443(wss://)。
1 | 扩展: |
STOMP Over WebSocket
STOMP (Simple (or Streaming) Text Oriented Message Protocol ) 是一种在客户端与中转服务端之间进行异步消息传输的简单通用协议;。它定义了服务端与客户端之间的格式化文本传输方式。
STOMP是一个简单的协议,这个协议可以有多种载体,可以通过HTTP,也可以通过WebSocket。在Spring-Message中使用的是STOMP Over WebSocket。
现在Client和Server已经有了一个会话。现在我们需要规定一种格式,能够让两边都理解说的是什么,这个东西就是通过STOMP来统一的。就好像在一个微信聊天里,我们规定聊天的双方都是用汉语交流,如果一个是用汉语,一个是用阿拉伯语,双方都不知道对方讲的是什么,那么这个天就聊不下去了。
STOMP && Message Queue
STOMP的每一个包简单的来说是由三个部分组成: COMMAND Header Body
结构可以简化如下
1 | COMMAND |
一共有这么几个COMMAND
1 | client-command = "SEND" |
Stomp协议中有两个重要的角色:STOMP客户端与任意STOMP消息代理(Broker)。如下图:
STOMP是如何进行订阅的?
客户端使用SUBSCRIBE订阅命令,向Stomp服务代理订阅某一个虚拟路径上的监听。这样当其它客户端使用SEND命令发送内容到这个路径上时,这个客户端就可以收到这个消息。在使用SUBSCRIBE时,有一个重要的ACK属性。这个ACK属性说明了Stomp服务代理端发送给这个客户端的消息是否需要收到一个ACK命令,才认为这个消息处理成功了。
Message Queue
Message Queue(消息队列)是一种应用程序对应用程序的通信方法。应用程序通通过读写出入队列的消息来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,排队指的是应用程序通过队列来通信。队列的使用除去了接受和发送应用程序同时执行的要求。
简单来说消息队列就相当于一个书架,在书架上有人放书有人拿书,但是放书的人和拿书的人互不通信。书架起到了一个中间人的作用。
通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。
Broker:消息服务器,作为server提供消息核心服务
Producer:消息生产者,业务的发起方,负责生产消息传输给broker
Consumer:消息消费者,业务的处理方,负责从broker获取消息并进行业务逻辑处理
Queue:队列,PTP模式下,特定生产者向特定queue发送消息,消费者订阅特定的queue完成指定消息的接收
Message:消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输
消息队列是一种技术手段,而消息代理是这种技术手段的一种实现方案。比较常见的消息代理有RabbitMQ,ActiveMQ等。
我们再回到Spring,Spring 的消息功能是基于消息代理(broker)构建的。我们在这里使用这个Spring官方的Demo作为POC。https://github.com/spring-guides/gs-messaging-stomp-websocket
完整的Demo代码在complete文件夹中,其中complete/src/main/resouces/static/app.js为STOMP的Client。
建立连接的代码如图所示。在这里可以看到,SockJS与/gs-guide-websocket建立连接,通过/topic/greetings订阅消息,而发送消息到/app/hello这个接口。
app.js中为什么出现三个接口?
第一个接口(“/gs-guide-websocket”)是建立STOMP连接,使用由SockJS实现的Websocket,在这里是由SockJS包装的Websocket,所以使用的是Stomp.over(ws)。如果使用原生的Websockets就使用Stomp.client(url)
第二个接口(“/topic/greetings”)利用STOMP的subscribe()让客户端进行订阅。这个方法有两个必要的参数:目的地(destination),回调函数(callback),还有一个可选的参数headers。其中destination是String类型,对应目的地,回调函数是伴随着一个参数的function类型
第三个接口(”/app/hello“)利用STOMP的send()来发送STOMP消息,请求向所有订阅过”/topic/greetings“的客户端推送消息。这个方法必须有一个参数,用来描述对应的STOMP的目的地,另外可以有两个可选的参数:headers,object类型包含额外的信息头部:body,一个String类型的参数。
这里有两个知识点SockJS的Fallback和Spring-Message的消息传递。
SockJS Fallback
由于公网环境比较复杂,一些不可控的代理可能会限制WebSocket的交互,一般分为两种情况,一种为不允许带有WebSocket的Upgrade
头的HTTP Request通过,另一种是不允许闲置的长连接。
当然了,这个问题肯定是有解决办法的,比如使用WebSocket建立连接,然后使用HTTP的一些去模拟WebSocket交互,并且提供同一应用层的API。
SockJS就是为了解决这种问题而诞生的。
The goal of SockJS is to let applications use a WebSocket API but fall back to non-WebSocket alternatives when necessary at runtime, i.e. without the need to change application code.
消息传递
消息传递是进程间数据传递的一种方法,进程采用消息(message)的方法,由发送进程向接收进程的消息队列发送一个消息,接收进程在合适的时候取出。
消息是异步发送的,客户端不需要等待服务处理消息,甚至不需要等待消息投递完成。客户端发送消息,然后继续执行,这个是因为客户端假定服务最终可以收到并处理这条信息。
在异步消息中有两个主要的概念:消息代理(message broker)和目的地(destination)。当一个应用发送消息时,会将消息交给一个消息代理。消息大理可以确保消息被投递到指定的目的地,同事解放发送者,使其能够继续进行其他的业务。目的地只关注消息应该从哪里获得,而不关心由谁取走消息。
Spring-Message 消息传递
SpEL
Spring Expression Language (SpEL)是一种表达式语言,是一种可以与一个基于Spring的应用程序中的运行时对象交互的东西。有点类似于ognl表达式。总得来说SpEL表达式是一种简化开发的表达式,通过使用表达式来简化开发,减少一些逻辑、配置的编写。
SpEL具有很高的权限,可以执行任意命令。
复现
给src/resources/static/app.js中的connect方法中调用的subscribe方法传入一个selector。exec()中执行任意命令。
分析
Client建立WebSocket连接后点击connect按钮发送CONNECT和SUBSCRIBE。
SUBSCRIBE已经将payload发送至服务器,但是并没有触发,而是在发送消息(SEND)时触发。
Spring Farmework处理订阅的逻辑在org.springfarmework:spring-messaging下,代码路径如图所示。
在这个类下的addSubscriptionInternal方法处设置断点,点击connect按钮,可以看见以下信息。
selector中的SpEL语句已经生成SpEL表达式(红框中)。还有此次会话的sessionid,后面将会通过sessionid取出消息。
一直跟进expression变量,传递关系为
1 | ps: ClassName.MethodName |
在Subscription的构造方法中将selector写进Subscription对象的属性中。
返回的Subscription对象在SessionSubscriptionInfo.addSubscription中通过add方法添加到subs中。
接下来,发送数据。Spring给消息订阅者分发消息的逻辑在org.springframework.messaging:simp/broker/SimpleBrokerMessageHandler.java中的sendMessageToSubscribers方法。
在this.subscriptionRegistry.findSubscriptions(message)
方法下断点,可以看到传进来的message对象包含连接的信息。
跟进message
变量
调用链为 : this.subscriptionRegistry.findSubscriptions -> findSubscriptionsInternal -> filterSubscriptions
在filterSubscriptions方法中获取连接配置,包括SpEL表达式。
红框处为获取连接配置的语句,其中含有我们的Payload(SpEL表达式)。
下面的try语句中执行表达式。
1 | try { |
修复
使用SimpleEvaluationContext
代替具有任意命令执行权限的StandardEvaluationContext
总结
由于场景比较特殊,黑盒测试了数个Spring后台,并没有很好的效果。但是让研究人员的关注点转到了SpEL上,不失为一件好事。CTF中遇见Java的情况并不多,但是在实际工作中大部分的后台还是Java。近期会陆续分析一些Java的洞,学习一下Java相关的知识。
资料
https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/core.html#expressions
https://github.com/sockjs/sockjs-client/
https://paper.seebug.org/562/
https://blog.csdn.net/pacosonswjtu/article/details/51914567
https://github.com/spring-guides/gs-messaging-stomp-websocket
https://www.ibm.com/developerworks/cn/java/j-spring-boot-basics-perry/index.html
http://www.ruanyifeng.com/blog/2017/05/websocket.html
https://www.websocket.org/aboutwebsocket.html