之前写了一篇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上能找到
我们关注最核心的
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远程代码执行漏洞分析预警
旻牛太强了……bwapp教程还没刷完,有没有公众号什么的,想持续学习`(*∩_∩*)′
公众号真的没有时间运营...持续学习关注本站就行或者冰总的bithack.io,里面也有很多优质资源