IDEA动态调试分析Spring RCE CVE-2018-1270

漏洞学习,代码审计 2019-04-19

 之前写了一篇PHPSTORM配合Xdebug插件来做bWAPP项目的代码审计文章,后来有小伙伴说想看Java动态调试分析漏洞的文章。
   
 想了半天自己确实从来没写过一篇完整的Java项目漏洞分析的文章,找了一些近期比较出名的Java框架/中间件/容器漏洞看看,一时间也没想好到底找谁水一篇文章。后来想想现在春暖花开,不如直接分析Spring框架漏洞好了,于是乎找到了差不多一年前应急过的CVE-2018-1270。

 还记得当时是清明节前后出的漏洞,写完漏洞通告以后假期还在和客户讲解相关漏洞的修复方案......

 今天也会尽量从最基础的角度帮助有一定安全基础却没有做过Java漏洞分析的同学熟悉一下整个过程~

作者:古月蓝旻

漏洞简述

2018年4月5日,Pivotal Spring官方发布安全公告,Spring框架中存在三个漏洞,其中编号为CVE-2018-1270的漏洞可导致远程代码执行。

关于该漏洞的简介,网上五花八门都有,有不少都把spring-messaging模块和STOMP混为一谈,对于Spring开发者而言经常看得一头雾水。

Spring框架中通过spring-messaging模块来实现STOMP(Simple Text-Orientated Messaging Protocol),STOMP是一种封装WebSocket的简单消息协议。攻击者可以通过建立WebSocket连接并发送一条消息造成远程代码执行。spring-messaging和spring-websocket模块都能提供WebSocket支持的STOMP,一旦有了这些依赖项,就可以通过WebSocket使用SockJS Fallback公开STOMP端点

推荐一篇文章辅助理解STOMP Spring消息之STOMP

这里引用P牛在vulhub的描述:

spring-messaging是基于sockjs(可以理解为一个通信协议),而sockjs适配多种浏览器:现代浏览器中使用websocket通信,老式浏览器中使用ajax通信。

连接后端服务器的流程,可以理解为:

1. 用STOMP协议将数据组合成一个文本流
2. 用sockjs协议发送文本流,sockjs会选择一个合适的通道:websocket或xhr(http),与后端通信

正是由于第2条的存在,我们才可以使用http来复现该漏洞,称之为“降维打击”

如同提及Struts2 RCE类漏洞总要提及OGNL表达式语言
Spring的RCE类漏洞往往和SpEL表达式语言有关

Spring表达式语言全称Spring Expression Language,支持查询和操作运行时对象导航图功能.。语法类似于传统EL,而且供额外的功能,能够进行函数调用和简单字符串的模板函数。

SpEL用法

String expression = "T(java.lang.Runtime).getRuntime().exec("calc.exe")";
String result = parser.parseExpression(expression).getValue().toString();

简单来说,本次漏洞根源所在的spring-messaging 5.0.4版本中的DefaultSubscriptionRegistry类里
由于使用的StandardEvaluationContext 权限太大,可以执行任意 SpEL 表达式,所以官方在 spring-messaging 5.0.5 之后添加了 SimpleEvaluationContext,用于实现简单的数据绑定,保持灵活性但不带来安全隐患

好了,简单介绍完该漏洞后,我们准备复现一下

测试环境

老规矩,交代一下环境

操作系统:Windows 10
JDK版本: JDK 1.8
IDE版本: IDEA 2017.3
漏洞组件版本:spring-messaging 5.0.4

好了,我们从头把整个环境搭建一下吧

环境搭建

本次我们使用IDEA来调试Spring相关代码,首先我们需要的是将Spring的源码导入IDEA中,且必须要使用包含漏洞版本的jar包

嗯,这个问题对于新手而言非常令人头大,去哪下载源码呢?漏洞jar包去哪里下载?如何导入IDEA呢...

嗯,如果只是想单纯在本地复现一下这个漏洞,测试一下POC的话,直接去Vulhub下载docker镜像就可以了,里面还有分析文档和poc

Spring Messaging 远程命令执行漏洞(CVE-2018-1270)

直接一条命令即可启动相关环境,方便又快捷

这里顺便多提一句,如果想查看docker容器中的文件或者想提取其中的jar包的话,可以这么操作

docker exec -it  CONTAINER_ID /bin/bash   # 获取指定ID容器的bash

docker cp CONTAINER_ID:/targer_file_path   path  #将指定ID容器中的目标文件targer_file_path复制到宿主机指定路径path

如果想看jar包里的源码,使用JD-GUI或者IDEA等等直接打开都可以,可惜不能运行

但是我们本次还是想利用IDEA在本地进行调试,期望的方式是:

通过IDEA导入相关项目源码,下断点跟踪变量值和调用栈变化

类似于我上篇PHPSTORM+Xdebug的方式。为了实现上面的目标,我们分解一下相关步骤

Step1: 下载项目漏洞源码

对Java各大框架略有了解的小伙伴应该都知道Spring框架是开源的,所以我们是可以直接获取到相关源码的,我们可以直接访问其官网https://spring.io来尝试找到源码的下载地址

当然Spring旗下包含诸多框架,面向不同使用群体和业务场景https://spring.io/projects里其实大致都能看到

像我们本次分析的漏洞CVE-2018-1270,虽然介绍了是spring-messaging模块的问题,但是它本身隶属于项目SpringFramework

所以我们在官网关于SpringFramework的相关介绍,能找到github的源码

当然,找到这里会发现一个问题,SpringFramework这个项目本身量级比较重,功能很丰富,我们只是想简单复现一下spring-messaging的问题,有没有简单一点的?

有的,Spring官方非常贴心的提供了各类项目的guides,我们只需要下载guides中的相关项目,就能简单把项目run起来,比如我们本次的spring-messaging模块,只需要将向导中的gs-messaging-stomp-websocket部分代码克隆到本地即可

git clone https://github.com/spring-guides/gs-messaging-stomp-websocket

当然我现在是站在9102年的视角来下载代码,相关的漏洞肯定早就被修复了,所以我们使用git checkout回退到有漏洞的代码分支


cd gs-messaging-stomp-websocket
git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3

好了,问题又来了,这个6958af0b0xxxx怎么来的?

如果你熟悉git语法的话,就应该知道,每次commit以后都会修改该值,用于标识不同时期和内容的代码,方便使用git checkout进行回退

通过查看gs-messaging-stomp-websocket项目的commits,根据关键节点时间2018年4月和新release发布的说明,可以定位到项目源码修改之前的commit,这个时候的代码就是存在漏洞的代码

好了,到这里我们就完成了第一步,下载漏洞源码

Step2: 源码导入IDEA

首先打开IDEA以后,选择“import project”

这个时候留意下图红框圈起的部分

其实IDEA导入不同类型项目所需的文件不同,比如我们常用的J2EE类项目,往往使用Maven管理获取项目所需依赖jar包,Spring作为J2EE类项目中扛把子的存在,使用Maven属于常规操作,因此我们需要的是导入源码中的pom.xml。IDEA通过自带的Maven会自动帮助我们下载所需的jar包文件

我们下载的gs-messaging-stomp-websocket项目的pom.xml在其中的complete文件夹下,很方便就能找到

双击pom.xml,然后一路Next最后Finish,你啥也不用改,一款强大的IDE就是这么6

当然你会看见IDEA打开以后,左边似乎已经加载完成了,实际上根本没有,因为最下方暗示你,这个时候IDEA在疯狂下载pom.xml中提及的jar包

这一步其实最容易失败了,因为下载的很多包由于某些不可描述的原因,下载速度奇慢无比,这里我推荐配合SS,在IDEA的设置里设置代理Proxy加快下载速度

下载完其实长这样

到这里我们第二步就完成了,马上看看能不能运行

Step3: 项目运行

如果你前面的步骤都是正确的话,那么你点击菜单中的Run的时候,会非常开心地发现,我们可以“Run 'Application'”

如果你使用的JDK1.7,你会非常开心的发现,报错了...

毕竟8102年的项目,还是用回JDK1.8以上版本吧,配置一个JDK1.8

不需要重新import项目,直接菜单File-Project Structure修改Project SDK

同时还要修改菜单Run-Edit Configurations里面的JRE

终于把项目run起来了,看下方控制台的输出,我们知道,启动的是8080端口

访问一下http://localhost:8080

大赞啊,非常nice~

漏洞复现

方式一:开发者工具修改app.js复现

好了,我们尝试复现一下这个漏洞哈,我这边参考先知上的教程

spring-messaging Remote Code Execution 分析-【CVE-2018-1270】

根据教程的描述,我们直接通过开发者工具F12修改其中的app.js里面的connect函数

如何修改,直接定义一个header,里面写入json格式的“selector”,值为SpEL表达式,内容是弹计算器;最后在加入该header

 var header  = {"selector":"T(java.lang.Runtime).getRuntime().exec('calc.exe')"};

最后记得保存一下该app.js

这个时候,依次点击Connect,输入任意字符串,单击Send

震惊,计算器就这么毫无防备地出现了

漏洞复现成功~

嗯,有没有感觉有点奇怪,CVE-2018-1270是个RCE,为什么是通过修改本地客户端js的方式实现漏洞利用?

先知分析文章的第一个回复也是类似这样,感觉这不像一个远程利用漏洞,有没有符合题意一点的利用方式?

如果你这个时候顺便去Console看一眼,你会发现整个通讯过程很多是js完成的,不是app.js发起,就是调用stomp.min.js来发起

方式二:burp发包复现

好了,咱们能不能用burp模拟这个过程呢?

从前面的分析我们可以看到

STOMP是将数据组合成文本流,spring-messaging通过sockjs协议发送该文本流,走的是websocket或xhr其中一种通道

那么具体走了哪一种呢?是websocket还是xhr(http)?

其实非常简单,浏览器自带的开发者工具看一下“Network”就知道了

其实可以发现当你点击完Connect按钮以后,剩余的通讯过程双方全是通过Websocket进行了,当然通讯的内容我们也是可以直接在里面看的,切换到“Frames”选项卡,可以看见完整的通讯内容

当然,这个只能看不能改,能不能类似burp改http包来改包呢?

毕竟我们知道:burp是能拦截修改websocket数据包的,所以自然可以用于发现并修改整个connect和send的过程websocket的相关情况。同时改app.js这种方式本质上是发送的内容中加了header字段,里面写了selector和SpEL表达式实现命令执行。讲道理是可以用burp替代浏览器完成相关发包操作的。

如果这个时候你用burp抓一下Chrome浏览器的数据包,你会非常震惊地发现:

啥包都没抓到

如果遇到这个情况,你可以改用Firefox浏览器,就可以抓取到相关数据包

步骤1
先单击“Connect”按钮建立连接,依次抓取并释放,直到抓到内容为以下的websocket包时

["SUBSCRIBE\nid:sub-0\ndestination:/topic/greetings\n\n\u0000"]

在greetings后面添加payload(适用Windows系统),注意双引号使用反斜杠进行转义

\nselector:new java.lang.ProcessBuilder(\"calc\").start()

步骤2
在文本框中输入任意字符串均可,然后点击“Send”

此时可以发现,计算器直接弹出,赞~

如果计算器没弹出,说明步骤1没有改对websocket包,需要先Disconnect,再次Connect抓包改包

方式三:执行python脚本复现

使用burp修改websocket包这种方式适合漏洞复现,有点感觉,但是CVE-2018-1270这个漏洞,网上已经有POC和EXP流出了,有没有可以用python直接执行exp的脚本这种方式实现命令执行呢?

当然也是可以的

首先exp的脚本,P牛在vulhub上已经贴出CVE-2018-1270 EXP脚本

我们可以直接下载下来,使用的时候注意几点:

1. 使用spring的guides项目构建的环境无需修改路径,实际项目可能路径会发生变化;
2. 修改sockjs中的目标URL地址;
3. 修改sockjs的send方法中的seletor内容,修改想执行的命令
4. 建议使用python3.6及以上版本运行该exp;

整个过程如果顺利的话,就是这样,py里字符串是vulhub

简单看下脚本内容,实际上可以发现本EXP就是使用python将sockjs的send方法实现了一遍,实现了给指定url发送含有SpEL表达式的websocket请求。

对于本EXP(实际上是POC),P牛自己也阐述了大致流程和其中的局限性,这里引用一下:

1.基础地址,在vulhub中为http://your-ip:8080/gs-guide-websocket
2.待执行的SpEL表达式,如T(java.lang.Runtime).getRuntime().exec('touch /tmp/success')
3.某一个订阅的地址,如vulhub中为:/topic/greetings
4.如何触发这个订阅,即如何让后端向这个订阅发送消息。在vulhub中,我们向/app/hello发送一个包含name的json,即可触发这个事件。当然在实战中就不同了,所以这个poc并不具有通用性。

当然使用脚本的话好处在于可以根据实际需要,灵活调整内容,实现自动化批量扫描测试。

好了,到这里复现的步骤基本就结束了,下面准备进入正题部分。

动态调试

好了,上面说的其它内容太多了,我们直接说下如何做动态调试

一般动态调试的常规步骤是下断点、Debug运行、查看调用栈、跟踪变量、单步调试...

好了问题来了,断点下在哪里?

嗯,因为我们是复现漏洞,找payload对整个运行项目的影响情况,所以直接根据一些漏洞信息在指定包的指定函数下断点就可以了

比如我们本次的CVE-2018-1270漏洞,根据漏洞简述我们知道是spring-messaging模块出了问题,具体一点也知道是DefaultSubscriptionRegistry类的addSubscriptionInternal方法出现了问题,我们就去这里找。

具体一点就是External Libraries里面的spring-messaging-5.0.4.RELEASE.jar包,相关路径如下

spring-messaging-5.0.4.RELEASE.jar\org\springframework\messaging\simp\broker\DefaultSubscriptionRegistry.java

然后定位到问题方法addSubscriptionInternal,双击下断点,不确定的话可以多下几个断点

然后记得不要直接运行,依次点击菜单Run-Debug,重新以Debug模式运行项目,否则断点就白下了,一般调试器是在项目创建的时候IDEA就直接配置好了

这个时候我们使用上面任意一种方法,发送我们的payload吧

然后就成功被断点拦截

我们主要看下面两个窗口,左下是方法调用栈,右下是变量值,还可以进一步展开

当然下断点的时候经常发现没抓到,这个代表断点可能没有下对,或者断点处代码没被触发

我们可以使用F7(单步步入)、F8(步过)、F9(重新运行)等多种方式

针对新手可以使用F7一步步跟踪代码运行情况方便熟悉调试过程

针对本次漏洞,我们需要知道一点就是我们想让SpEL对表达式执行,要调用Expression类的getValue()方法

所以我们关注到了DefaultSubscriptionRegistry类的164行

if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) 

其中的expression对象是怎么来的呢?看到154行

 Expression expression = sub.getSelectorExpression();

正是通过Expression类创建的,好这里我们下两个断点

到这里我们只需要关注我们构造的SpEL表达式能否在这里出现就可以了

好了,再次运行,执行exp.py

154行断点一路单步调试,当调试到164行时,我们看下expression对象的变量值

果然里面是我们的SpEL弹计算器的T表达式,接着一路调试,果然弹出计算器

弹出计算器成功,这里我们可以勉强说自己完成了一次简单的调试(函数调用栈部分此次不重点关注)

有兴趣可以深入跟踪一下seletor是如何把值一路传递到expression对象的

代码对比

这个是我们看了别人或者官方通告以后知道问题出在了哪个地方,如果暂时没有分析文章或者官方只给了补丁怎么办呢?如何做第一个吃螃蟹的人写分析文档?

途径有很多,可以相互结合这里简单说一下:

1. 看patch,尤其是github上关注官方commit变化
2. 看官方给出的漏洞详情和修复方案,可能有提示信息
3. 自己下载不同版本的源码包自己diff

补丁分析

先说第一种看patch,实际上是补丁分析,本次补丁在github上能找到

CVE-2018-1270补丁

我们关注最核心的

if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {

发现补丁里使用evaluationContext替代了context对象,而前者由 SimpleEvaluationContext类创建,后者由StandardEvaluationContext创建,从后面的资料查询我们知道SimpleEvaluationContext类在实现灵活绑定之余也消除了安全隐患

官方通告

官方给出的修复方案是升级版本

Spring Framework 5.0到5.0.4升级到5.0.5版本

Spring Framework 4.3到4.3.14升级到4.3.15版本

官方通告里说spring-messaging有问题,而spring-messaging版本跟随Spring Framework版本,所以重点关注spring-messaging的jar包即可

源码包diff

开源项目,尤其是J2EE类的开源maven项目,可以去公网上的一些maven仓库下载到不同版本的源码包,比如spring-massaging源码包可以去以下网站下载

http://central.maven.org/maven2/org/springframework/spring-messaging/5.0.4.RELEASE/

http://central.maven.org/maven2/org/springframework/spring-messaging/5.0.5.RELEASE/

注意下载的是xxxx-source.jar包看源码,别下编译好的jar包

下载完成后,对比文件也是件麻烦事,diff命令或notepad++自带的compare插件对比单个文件可以,但是一个jar包就很尴尬了

这里推荐使用Beyond Compare,同时把两个jar包拖入即可

注意先设置一下规则,把对比时间戳去掉,否则所有文件都会报不同

其中标红的文件或文件夹说明两者不同

双击文件可以同时拖动查看,比如我们这里的问题类DefaultSubscriptionRegistry中的SpEL表达式执行处,可以明显看出差异

以上分析手段需要灵活结合运用猜测问题的大致范围

小彩蛋

之前查CVE-2018-1270漏洞的时候,发现这里还有一处存储型XSS,真是意外收获了,触发的方式其实挺简单的

建立Connect后,在输入的字符串处随意输入一个XSS的payload

<script>alert(/meetsec/)</script>

有兴趣的也可以动态调试分析一波~

又写了一篇中长教程,对于自己而言算是弥补了一个小遗憾,当然时间有限,里面也有很多没有提到的地方,希望各位能根据分析漏洞的实际情况,灵活进行调整,不要一味照搬照抄,实践出真知~

参考链接

SpringMessaging命令执行漏洞 - CVE-2018-1270

CVE-2018-1270:spring-messaging远程代码执行漏洞分析预警

spring-messaging Remote Code Execution 分析-【CVE-2018-1270】

Spring Messaging 远程命令执行漏洞(CVE-2018-1270)


本文由 古月蓝旻 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

2 条评论

  1. 陈cy
    陈cy

    旻牛太强了……bwapp教程还没刷完,有没有公众号什么的,想持续学习`(*∩_∩*)′

    1. 古月蓝旻
      古月蓝旻

      公众号真的没有时间运营...持续学习关注本站就行或者冰总的bithack.io,里面也有很多优质资源

添加新评论