作者:KINGX
公眾號:安全引擎
記錄在FastJson反序列化RCE漏洞分析和利用時的一些細節問題。
1. 關于 parse 和 parseObject
FastJson中的 parse() 和 parseObject()方法都可以用來將JSON字符串反序列化成Java對象,parseObject() 本質上也是調用 parse() 進行反序列化的。但是 parseObject() 會額外的將Java對象轉為 JSONObject對象,即 JSON.toJSON()。所以進行反序列化時的細節區別在于,parse() 會識別并調用目標類的 setter 方法及某些特定條件的 getter 方法,而 parseObject() 由于多執行了 JSON.toJSON(obj),所以在處理過程中會調用反序列化目標類的所有 setter 和 getter 方法。parseObject() 的源代碼如下:
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
舉個簡單的例子:
public class FastJsonTest {
public String name;
public String age;
public FastJsonTest() throws IOException{
}
public void setName(String test) {
System.out.println("name setter called");
this.name = test;
}
public String getName() {
System.out.println("name getter called");
return this.name;
}
public String getAge(){
System.out.println("age getter called");
return this.age;
}
public static void main(String[] args) {
Object obj = JSON.parse("{\"@type\":\"fastjsontest.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
System.out.println(obj);
Object obj2 = JSON.parseObject("{\"@type\":\"fastjsontest.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
System.out.println(obj2);
}
}
上述代碼運行后可以看到,執行parse() 時,只有 setName() 會被調用。執行parseObject() 時,setName()、getAge()、getName() 均會被調用。
2. 為什么會觸發getOutputProperties()
感覺上 parse() 進行反序列化創建Java類應該只會調用 setter 方法進行成員變量賦值才對,會什么會觸發TemplatesImpl類中的 getOutputProperties() 方法呢?
另外 _outputProperties 成員變量和 getOutputProperties() 明明差了一個_字符,是怎么被 FastJson 關聯上的?
如上一小節所述,parse() 進行反序列化時其實會調用某些特定的 getter 方法進行字段解析,而 TemplatesImpl類中的 getOutputProperties() 方法恰好滿足這一條件。
FastJson反序列化到Java類時主要邏輯如下:
- 獲取并保存目標Java類中的成員變量、setter、getter。
- 解析JSON字符串,對字段逐個處理,調用相應的setter、getter進行變量賦值。
我們先看第一步,這里由 JavaBeanInfo.build() 進行處理,FastJson會創建一個filedList數組,用來保存目標Java類的成員變量以及相應的setter或getter方法信息,供后續反序列化字段時調用。
filedList大致結構如下:
[
{
name:"outputProperties",
method:{
clazz:{},
name:"getOutputProperties",
returnType:{},
...
}
}
]
FastJson并不是直接反射獲取目標Java類的成員變量的,而是會對setter、getter、成員變量分別進行處理,智能提取出成員變量信息。邏輯如下:
- 識別setter方法名,并根據setter方法名提取出成員變量名。如:識別出setAge()方法,FastJson會提取出age變量名并插入filedList數組。
- 通過clazz.getFields()獲取成員變量。
- 識別getter方法名,并根據getter方法名提取出成員變量名。
可以看到在 JavaBeanInfo.build() 中,有一段代碼會對getter方法進行判斷,在某些特殊條件下,會從getter方法中提取出成員變量名并附加到filedList數組中。而TemplatesImpl類中的 getOutputProperties() 正好滿足這個特定條件。getter方法的處理代碼為:
JavaBeanInfo.java
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
...
for (Method method : clazz.getMethods()) { // getter methods
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}
// 關鍵條件
if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {
String propertyName;
JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation != null && annotation.deserialize()) {
continue;
}
if (annotation != null && annotation.name().length() > 0) {
propertyName = annotation.name();
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {
continue;
}
if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
}
}
}
...
}
接下來,FastJson會語義分析JSON字符串。根據字段key,調用filedList數組中存儲的相應方法進行變量初始化賦值。具體邏輯在 parseField() 中實現:
JavaBeanDeserializer
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // xxx
FieldDeserializer fieldDeserializer = smartMatch(key);
...
return true;
}
這里調用了一個神奇的 smartMatch() 方法,smartMatch()時會替換掉字段key中的_,從而 _outputProperties 和 getOutputProperties() 可以成功關聯上。
JavaBeanDeserializer
public FieldDeserializer smartMatch(String key) {
if (fieldDeserializer == null) {
boolean snakeOrkebab = false;
String key2 = null;
for (int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
} else if (ch == '-') {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
if (snakeOrkebab) {
fieldDeserializer = getFieldDeserializer(key2);
if (fieldDeserializer == null) {
for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
if (fieldDeser.fieldInfo.name.equalsIgnoreCase(key2)) {
fieldDeserializer = fieldDeser;
break;
}
}
}
}
}
3. 為什么需要對_bytecodes進行Base64編碼
細心的你可以發現,PoC中的 _bytecodes 字段是經過Base64編碼的。為什么要這么做呢? 分析FastJson對JSON字符串的解析過程,原來FastJson提取byte[]數組字段值時會進行Base64解碼,所以我們構造payload時需要對 _bytecodes 進行Base64處理。FastJson的處理代碼如下:
ObjectArrayCodec.java
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
final JSONLexer lexer = parser.lexer;
// ......省略部分代碼
if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue(); // ... 在這里解析byte數組值
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}
// 接著調用JSONScanner.bytesValue()
JSONScanner.java
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
}
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/636/
暫無評論