論戰被黑客入侵的MQ

來看一下場景:

有一個監控系統,需要把日誌實時推送到頁面上顯示,你可能覺得只需要一個消費訂閱通道就行了;
那再升級一下,這個監控系統同時監控了1000個應用,每個應用看到的日誌是不一樣的,那一個通道顯然不夠了。

由於歷史遺留問題,這裏採用的ActiveMQ來做消息中間件,在之前的方案中:前端是直連MQ的,基於Stomp協議,
一切都工作得很好,直到有一天發現了MQ裏的入侵代碼。。。

下面來演示這種場景:

容易被攻擊的ActiveMQ

前端

	var destination = "該應用的隊列名";
    var client = Stomp.client('ws://IP/stomp');
    
    var callbackMSG = function(message) {
        if (message.body) {
            alert("got message with body " + message.body)
        } else {
            alert("got empty message");
        }
    };
    
    var connect_callback = function(frame) {
      client.subscribe(destination, callbackMSG);
    };

    var error_callback = function(error) {
        alert(error.headers.message);
    };
    var headers = {
        login: 'xxx',
        passcode: 'xxx',
        // additional header
        'client-id': 'my-client-id'
    };
    client.connect(headers, connect_callback, error_callback);

可以看到,前端需要把MQ的用戶名密碼寫在代碼裏,雖然經過代碼壓縮,但還是很容易找到,所以是及其危險的。

PS: /stomp是用在nginx代理的url識別,本身不需要,直連時直接用ws://MQIP:61614

MQ配置

<transportConnectors>
	<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
	<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
</transportConnectors>

實際情況,61614端口經過了nginx代理:

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}
server{                                                                                                                 
        server_name localhost 域名;                                                                                                  
		listen 80;                                                                                                                                                                                                                                    
		location /stomp {                                                                                                          
			proxy_pass http://localhost:61614;                                                                                       
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;                                                                                 
			proxy_set_header Connection $connection_upgrade;   
		}		
}  

享受被攻擊的過程

可以看到,通過切換到WS協議,很容易發現MQ的密碼,和每10秒連接一次的心跳。
websocket協議
然後,每隔一段時間MQ就掛了,接着在日誌裏發現了入侵代碼和外網訪問的IP:

2020-05-14 17:41:53,198 | INFO  | /*1*/{{911191425+938578280}}_nimda Inactive for longer than 30000 ms - removing ...
2020-05-14 17:41:53,203 | INFO  | <%- 979207619+908582738 %>_nimda Inactive for longer than 30000 ms - removing ...
2020-05-14 17:41:53,205 | INFO  | sys.fn_sqlvarbasetostr(HashBytes('MD5' Inactive for longer than 30000 ms - removing 
2020-05-14 17:41:53,230 | INFO  | #set($c=800400123+845162035)${c}$c_nimda Inactive for longer than 30000 ms - removing ... 
2020-05-13 21:11:38,495 | WARN  | Transport Connection to: tcp://59.151.65.101:40204 failed: java.io.IOException: 
Unexpected error occurred: java.lang.OutOfMemoryError: GC overhead limit exceeded | org.apache.activemq.broker.TransportConnection.Transport | ActiveMQ Transport: tcp:///59.151.65.101:40204@61616

這裏只是一部分,最後因爲內存回收不過來,溢出導致掛掉了。

然而這些黑客不知道通過MQ拿不到什麼東西,MQ裏全是沒用的日誌。

修復方案

當然是不能直接把MQ暴露給用戶,於是想在中間加一層,然後做一些認證,這不就是把網關移到程序裏了嗎?這就是代理。

如果基於spring項目來實現,也是有現成的工具的,具體參考文檔:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket-stomp

但是,僅僅瞭解後端的協議還不夠,前端也得跟上,這應該就是全棧的優勢。

後端代理

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

增加ws協議配置

@EnableWebSocket
@SpringBootApplication
public class DemoWebsocketProxyApplication {}


import com.jimo.demo.ws.proxy.interceptor.WsHandShakeInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;


@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp/**").setAllowedOrigins("*")
                .withSockJS()
                .setInterceptors(new WsHandShakeInterceptor());
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue", "/topic")
                .setRelayHost("development01")
                .setRelayPort(61613)
                // 給客戶端的密碼
                .setClientLogin("xxx")
                .setClientPasscode("xxx")
                .setSystemLogin("xxx")
                .setSystemPasscode("xxx");
    }
}	

注意:

  1. 這裏是61613端口,stomp協議,不再是websocket協議了。
  2. endpoint(/stomp/**")代表前端訪問的端點,只代理這部分開頭的
  3. 我們設置了握手的攔截器,這是關鍵

攔截器:

public class WsHandShakeInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
		// TODO 在這裏做攔截
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                               Exception exception) {
    }
}

假如後端程序的端口爲8088.

前端

	 var sock = new SockJS('http://localhost:8088/stomp');
	 sock.onopen = function() {
		 console.log('open');
		 sock.send('test');
	 };

	 sock.onmessage = function(e) {
		 console.log('message', e.data);
		 sock.close();
	 };

	 sock.onclose = function() {
		 console.log('close');
	 };
	 
	 var stompClient = Stomp.over(sock);
	 var headers = {
        login: 'xxx',
        passcode: 'xxx',
        // additional header
        'client-id': 'my-client-id'
    };
	 stompClient.connect({}, function (frame) {
	   stompClient.subscribe('/queue/隊列名', function (msg) {
		 console.log(msg);
	   });
	 });

注意:

  1. 這裏的url是http協議,基於sockjs實現,然後再封裝了一層stomp協議客戶端,爲了傳輸用戶名和密碼
  2. 假如MQ沒設置用戶名密碼,那麼也不需要用stomp代理了,sockjs就夠用
  3. 具體的攔截認證這裏就不能透露了,你懂的

總結

安全很重要,對外的應用需要不斷和黑客鬥智鬥勇,還是做政府的項目好,完全部署在內網,啥都不用考慮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章