论战被黑客入侵的MQ

本文探讨了ActiveMQ直接暴露给前端的安全风险,包括密码泄露和频繁的恶意攻击。通过引入Spring WebSocket作为代理,实现了更安全的认证机制,并详细介绍了前后端配置过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

来看一下场景:

有一个监控系统,需要把日志实时推送到页面上显示,你可能觉得只需要一个消费订阅通道就行了;
那再升级一下,这个监控系统同时监控了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. 具体的拦截认证这里就不能透露了,你懂的

总结

安全很重要,对外的应用需要不断和黑客斗智斗勇,还是做政府的项目好,完全部署在内网,啥都不用考虑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值