基于请求/响应对象搜索的Java中间件通用回显方法(针对HTTP)
Author:fnmsd@360云安全
Java中间件通用回显方法(针对HTTP)
作者:fnmsd
先感谢一下scz大佬给的博客推荐,受宠若惊+10086。
心血来潮有这么个想法,验证一下。
前言
看了很多师傅们在研究针对Java中间价+Java反序列化回显的研究:
又看到了c0ny1师傅的作品:《java内存对象搜索辅助工具》
配合IDEA在Java应用运行时,对内存中的对象进行搜索。比如可以可以用挖掘request对象用于回显等场景。
....
按照经验来讲Web中间件是多线程的应用,一般requst对象都会存储在线程对象中,可以通过
Thread.currentThread()
或Thread.getThreads()
获取。并且目前回显思路主要是基于加载类,执行static块或者构造方法(原生反序列化、FastJson、Jackson一类的都有):
- TemplatesImpl类的反序列化链,内嵌类的bytecode,defineClass。
- 其他反序列化链使用URLClassLoader进行远程加载类。
- JNDI远程加载类。
所以想到:我们能否写单个类,让它能够触发时,使用2的思路去寻找request和response,从request中获取命令参数,然后向Response中写入呢?
先说结论:可以实现,在Tomcat8/9、Jetty和Weblogic中都可使用(只测了这三,个人觉得其它Java中间件也没差),还测试了Shiro及Fastjson的场景。
Tomcat6+Shiro会报java.io.StreamCorruptedException: invalid type code:
错误,很迷,有空再解决吧。
精简后的编译完的class文件大小在2800-2900B左右,Shiro反序列化用的Cookie值大小可以控制在5000字节左右,勉强可以接受。
响应时间一般在3S内。
基本思路
javax.servlet.http.HttpServletRequest
javax.servlet.http.HTTPServletResponse
- java类中间件的request和response分别实现以上两个接口
- 从Thread.currentThread()起始搜索实现了如上两个接口的对象
- 通过HttpServletRequest的getHeader方法可以获取到请求头
- 通过HTTPServletResponse的getWriter方法可以获取到响应的Writer(开始用的ServletResponse的getOutputStream接口,但是会报重复获取的OutputStream的错误,此处就改为使用getWriter了)
- 都搜索到后,该执行执行,该输出输出
初始代码
有点多,都贴上有水字数的嫌疑,还是贴gist链接吧。┓( ´∀` )┏
https://gist.github.com/fnmsd/89118c2967cd53c244389564d2f8b3681
编译完的class 3888字节,好吉利~
为了类能小一些,没有做c0ny1师傅那么细致的搜索分类和剪枝。
为了方便,纯粹使用深度优先搜索(DFS),依次搜索字段
然而接下来为了瘦身,各种剪枝逻辑还得继续砍砍砍
PS:在搜索Tomcat的Request过程中,不知道为何会搜出一个并非当前Request的对象,所以这里限制了Request必须包含cmd头,才认为找到了真的Request,Response目前没有发现这个问题。
为代码瘦身
由于Nginx有Cookie 4096B长度的限制,Tomcat有8096B的长度限制(感谢c0ny1师傅),所以为了能使用Templates类的链,编译出的Class文件还是越小越好。
以下内容比较乱,是一个逐渐尝试减小Class文件的过程,没兴趣可以略过
整个分析过程使用classpy-0.4.jar来进行对Class文件的分析:
https://github.com/zxh0/classpy
1.初始
3888字节
2.去掉可有可无的静态字段
static Class ReqC = HttpServletRequest.class;
static Class RespC = HttpServletResponse.class;
static int max_depth = 50;
3734字节
- 去掉字符串连接:
3640字节
删除多余逻辑的限制逻辑
if(depth > 50||(req!=null&&resp!=null)){ return; }
3611字节
删除getModifiers的调用(删除一个方法大概60字节的样子?)
3551字节
减少了一个空判断,大概4字节
3547
减少局变量的定义
Class a = obj.getClass(); if(a.isPrimitive()||a.toString().startsWith("java.lang")){ return true; }
变为:
if(obj.getClass().isPrimitive()||obj.getClass().toString().startsWith("java.lang")){ return true; }
3525,少了22字节
去掉调试时用的printStackTrace
3488字节
删掉flush和waitFor
3451字节,貌似没少多少
删掉static块和start方法,只保留构造方法触发
3372
删除PrintWriter的变量赋值,删除Test by fnmsd字符串
3260
删除掉proc的定义,直接跟getInputStream一块调用
3220
合并Scanner相关逻辑为一句:
resp.getWriter().println(new Scanner(Runtime.getRuntime().exec(req.getHeader("cmd")).getInputStream()).useDelimiter("\\A").next())
3130
以下本应有判断来进行的剪枝,改为使用异常处理兜着
去掉java.lang类的搜索剪枝
obj.getClass().toString().startsWith("java.lang")
节省了toString和startsWith
3009
去掉数组包裹类型的为非primtive的判断
obj.getClass().getComponentType().isPrimitive()
2962
去掉全部isPrimitive的判断
2917
去掉搜索时字段值为null的判断
2908
给类名、字段名、方法名、局部变量名都改为一个字符
2850
这里改局部变量其实不起作用。
去掉是否为静态字段的判断
if((declaredField.getModifiers()&0x00000008) == 0)
2803(最终大小在2800-2900之间,不会差太多,后面不知道又改哪里了,到了2900左右)
瘦身总结
最终结果代码(使用构造方法触发,也可以改成Static,都一样):https://gist.github.com/fnmsd/8165cedd9fe735d7ef438b2e977af327(这份代码不是最终版的,有Bug,请继续往后看)
- 尽量减少引用的方法和类型(包括像字符串连接这种隐式调用)
- 尽量减少字段、局部变量的定义
- 能用异常处理兜底的处理,减少判断。
- 蚊子腿也是肉,把字段名、参数名、变量名改小点(这块不确定,可能只有字段名有效)
- 比较大的地方主要在ConstantPool和变量定义上(ConstantPool还好理解,变量定义不太明白,后续还是好好在学习学习Class文件结构)
测试
由于都是new一个对象或者static块执行,所以偷了个懒,简单写了个jsp页面,可以看到没有任何的输出内容,只是new了一下对象:
<%@ page import="aa.a" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% new a(); %>
Weblogic(打包有点问题,页面没改成最简版的):
Tomcat:
Jetty:
Tomat8+Shiro反序列化
这里用的Jdk7u21的链(TemplatesImpl触发)来new我写的类,rememberMe长度5036
懒得部环境的同学,可以用Vulhub里面的Shiro环境,(SpringBoot用的嵌入式Tomcat9)但是那个得用CommonsBeanutils1的链。
发现问题:回显只能在高并发的环境下使用/没有回显
残笑师傅给我反馈说:只有高并发的环境下才能看到回显结果,结果中包含了:没HTTP只有命令输出的情况、没回显的情况、有回显的情况、有回显多次的情况。
这个问题是因为搜索到了NIO的队列中,由于Request加了存在cmd头的限定,所以搜索到的Request一定是你的Request,但是Response就不好说了,也就是说如果你在发这个的攻击包的时候,如果还有其它人在访问,那么很有可能,你的命令输出结果就输出到其它访问的结果中了。
之前测试的时候,只测了单一线程发送的情况,所以没发现这个问题,emm,还是得测试充分的。
模拟测试:
模拟正常访问:Burp Intruder使用Null Payloads发送1000次,并发10,固定发送间隔500ms。
访问的其实就是个404页面
模拟攻击访问:Burp Repeater发送攻击报文
然后就可以发现,某几次攻击请求中,响应为空,但是一个普通的请求中出现了我们想要的结果:
某几次的攻击请求结果哪里都找不到,应该是找到的Response对象,还没等写入,就被销毁了。。
解决方法
目前翻阅了Tomcat、Weblogic、Jetty、JBOSS的代码,发现Request对象下面都带有对应的Response对象,所以既然可以准确定位到Request对象,那么Response也就有了。
(Jetty的resposne不是直接包含在request对象中,这三者都有无参的getResponse方法,直接反射调用就好了~)
PS:前几天还在p师傅的小密圈跟Kingkk师傅说觉得Weblogic的Request对象下面带Response好奇怪,结果中间件都这么干,emm,我Out了。。。
目前还是测了Tomcat/Weblogic/Jetty没有问题,JBoss懒得装了,有兴趣的师傅可以试一下。
10并发的正常访问情况下可以正常的触发Shiro反序列化回显:
改完的代码:
https://gist.github.com/fnmsd/5c98b20cef16cf4942de0eba34dc2ad7
保留了Response的搜索分支,基本可以去掉,只是以防万一碰到没有getResponse方法还可以用。
参考:
http://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/connector/Request.html#field_summary
https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/server/Request.html
https://docs.jboss.org/jbossweb/latest/api/org/apache/catalina/connector/Request.html
Weblogic的Request类名为:weblogic.servlet.internal.ServletRequestImpl
Tomcat回显不稳定
ph4nt0mer师傅反馈Tomcat回显不稳定,经过测试发现:
Tomcat除了org.apache.catalina.connector.Request
外,还有org.apache.catalina.connector.Request.RequestFacade
这个包装类继承了HttpServletRequest
接口。
而RequestFacade
不包含getReponse
方法,无法准确获取到Response对象。
修改如下逻辑:
- 如果通过反射调用
getResponse
方法失败,就认为没有搜索到正确的Request对象,重置r字段 - 由于改为仅从request中获取response,所以删掉原有的response搜索逻辑。
改过的类地址:
https://gist.github.com/fnmsd/4d9ed529ceb6c2a464f75c379dadd3a8
参考:
https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/connector/RequestFacade.html
最新评论