Java反序列化(六) | Shiroの起始篇(环境搭建+原理分析)
Shiroの起始篇(环境搭建+原理分析)
这几天都在做Shiro的复现学习, 原理并不复杂, 但是因为太久没使用Java所以导致出现的一些环境问题直接把我给整晕了, 几乎一半的时间是用来调环境的, 在这里记录一下吧
这篇文章主要内容分为两个部分:
- Shiro漏洞原理和漏洞源码解析
- 环境搭建+坑点
- 一些依赖包的声明和加载问题
0x01 - Shiro框架漏洞
1.2 框架介绍
Apache Shiro™是一个强大且易用的Java安全框架,能够用于身份验证、授权、加密和会话管理,用于执行身份验证、授权、密码和会话管理。只要rememberMe的AES加密密钥泄露,无论shiro是什么版本都会导致反序列化漏洞。
Apache Shiro框架提供了RemeberMe
功能,用户登录成功后会生成经过加密并编码的cookie。cookie的key为RemeberMe,cookie的值是经过对相关信息进行序列化,然后使用aes加密,最后在使用base64编码处理形成的。
在服务端接收cookie值时,按以下步骤解析:
检索RemeberMe cookie的值
Base64解码
使用ACE解密(加密密钥硬编码)
进行反序列化操作(未作过滤处理)
在调用反序列化的时候未进行任何过滤,导致可以触发远程代码执行漏洞。
总结一下就是服务器在接收到Client的请求后会获取Cookie中的RemeberMe
字段然后进行固定格式的解码然后对解码数据进行反序列化最终导致远程代码执行。
1.2 Shiro序列化利用条件
我们需要知道Server端的AES加密的硬编码密码,在现在的新版本中对这个问题的解决方法是去掉硬编码的密钥,使其每次生成一个密钥来解决该漏洞。但是当前还是有很多的开源框架使用一个自己设置的固定硬编码所以就会存在风险。
此外Shiro的最大变化在于shiro1.2.4之前版本中使用的是硬编码,AES加密的密钥默认在代码里,所以就是说只要是shiro1.2.4之前的版本只要用户没有修改客家代码那么我们就可以进行任意的反序列化。
1.3 识别Shiro漏洞
在请求包的Cookie中为?remeberMe字段赋任意值
返回包中存在set-Cookie:remeberMe=deleteMe
URL中有shiro字样
有时候服务器不会主动返回remeberMe=deleteMe,直接发包即可
1.4 源码探查
打开CookesRememberMeManager.java
注: 下面的函数几乎全部都是在CookesRememberMeManager.java里面, 看样子remenberme的管理机制几乎都在这执行了
将数据序列化后的对象进行加密的rememberSerializedIdentity
函数:
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
将数据反序列化的getRememberedSerializedIdentity
函数:
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
....一些其它代码
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}
查看调用了getRememberedSerializedIdentity
的函数:
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
再进一步跟进到处理来自getRememberedSerializedIdentity
的数据的函数convertBytesToPrincipals
:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
可以看到对传入的bytes
变量先执行decrypt
后再对其进行了反序列化:
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
可以看到解密的秘钥是decryptionCipherKey
变量,查找设置变量的地方:
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
最终可以看到加密解密函数使用的秘钥均为DEFAULT_CIPHER_KEY_BYTES
, 查找一下看到这是一个常量:
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
所以这是一个硬编码的加密方式
0x02 - Shiro漏洞本地复现环境搭建
源码下载: 传送门
- 将压缩包解压后使用IDEA打开
- IDEA打开后查看是否已自动将文件夹识别为Maven项目, 想我这里打开后已经自动识别为Maven项目, 如果没识别为Maven项目那么pom.xml文件显示为橙色而不是蓝色
- 如果未自动识别则打开解压缩的根目录下面的pom.xml文件后右击会出现添加为Maven项目然后就会像我这里一样变为一个Maven项目(因为我这里已经是Maven项目所以也就没显示了)
- 打开
\shiro-shiro-root-1.2.4\samples\web\pom.xml
然后指定javax.servlet
依赖包的版本为1.2, 并手动添加3.2.1
的CC依赖包
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
- 修改好后在pom文件右击然后选择重新加载Maven项目或者直接点击文件右上角的小标志
- 点击右上角添加项目
添加一个本地的Tomcat项目
- 添加项目部署
- 选择sample-web项目
- 确定后运行项目
- 运行项目后可以在下面的服务小工具栏的服务看到服务部署情况
- 如果打开项目成功那么会自动打开默认配置的浏览器并转到
http://localhost:8080/samples_web_war/
(没修改Tomcat配置的情况下)
接着就成功打开Shiro项目了:
0x03 - docker环境搭建
如果觉得麻烦的话可以使用docker建议
docker pull gqleung/cve-2016-4437
docker run --name shiro-cve-2016-4437 -p 8080:8080 gqleung/cve-2016-4437
0x04 - 踩坑记录
断点测试
如果我们想要断点调试需要在本地文件打断点, 但是我们直接搜索Shiro的cookie管理类文件CookieRememberMeManager的时候会发现有两个选择, 一个是本地的项目文件, 还有一个是Maven通过pom.xml加载的文件但是注意这个文件后面还带了一个Test, 类名为CookieRememberMeManagerTest而不是CookieRememberMeManager
如果我们想要打断点调试的话我们最好在本地文件打断点, 否则可能会失效, 下面是我在CookieRememberMeManager解密cookie中rememberMe
的CookieRememberMeManager函数下的断点, 然后我们发送一个带有rememberMe的数据包就可以看到截断进入调试状态了
依赖查看
我们可以查看\shiro-shiro-root-1.2.4\samples\web\target\samples-web-1.2.4\WEB-INF\lib下面有没有CC依赖, 可以看到CC包已经在lib目录下面了就说明CC依赖加载成功并且可以看到依赖版本
相同环境构造链
我们可以在D:\Code\java\test\shiro-shiro-root-1.2.4\samples\web\src\test\java下面添加我们构造我们的反序列化链, 这样子可以保证我们的Shiro环境和我们构造链的环境是一样的而不会缺少一些别的依赖包
写好构造链的java文件后添加应用程序
选择一个Java的JDK
选择我们添加了文件的项目samples-web
添加我们写了main函数的主类(我们这里选用的是Get_poc)
Tips:
如果显示缺少javassist依赖的话那就在上面添加CC依赖的pom.xml文件中添加下面的Maven依赖:
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
注意:如果我们import的时候有一些包自动补全显示有但是运行项目就报错不存在
我们可以在项目结构修改JDk版本(我们的JDK依赖包就是在这里加载的
我们可以在项目结构修改SDk版本(我们的JDK依赖包就是在这里加载的)
我们修改为1.8
然后再运行项目就成功了
如果报错的程序包和我一样那也将SDK改为1.8版本的即可, 如果是别的程序包建议多下载几个版本进行尝试看看
更多JDK版本下载传送门: https://www.oracle.com/java/technologies/downloads/archive/
0x05 - 可用依赖包查询
根据上面分析我们可以知道开启硬编码的加密方式我们可以直接从源码得到秘钥所以实际上就相当于我们可以任意进行反序列化, 但是我们需要知道的一点是shiro有哪些依赖项目才能够进行反序列化, 我们直接通过pom.xml文件来看一下shiro包中用到了哪些依赖包:
可以看到既有commons-collections
的依赖也有commons-beanutils
但是CC包在我们没有手动添加CC的dependency
依赖标签的情况下是不可用的
我们先来了解一下在不带任何框架的情况下shiro的一些自身依赖:
-
shiro-core、shiro-web,这是shiro本身的依赖
-
javax.servlet-api、jsp-api,这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这两个依赖
-
slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖
-
commons-logging,这是shiro中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 错误
0x06 - 我们需要知道
4.1 可用依赖包
我们需要知道一点: 虽然网上很多的示例演示shiro漏洞的时候都是用CC链, 但是实际上在一个纯净的shiro项目比如使用shiro的源码中的example-web这个服务的时候我们如果直接打CC链不会成功, 因为如果项目环境中没有import org.apache.commons.commons-collections
那么就不会真正的加载编译对应的类到JVM中(没理解错的话,也可能错了,所以下面的内容就当一个参考吧,看看就行)。
-
如果只是在
pom.xml
文件中添加了commons-collections
的dependency标签而没有在实际需要运行到的.java文件中import
的话那么这个依赖只会被标记为test
。 -
如果在
pom.xml
文件中添加有并且在实际需要运行到的.java文件中有import
声明但是没有用到的话会被标记为compile
。 -
如果同时满足
pom.xml有dependency标签
+需要运行的.java文件中有import声明
+需要运行的.java文件中使用到依赖
那么这时这个依赖文件包就会被标记为runtime
。
而我们真正能够利用到的只有compile
和runtime
这两种属性的依赖, 所以这就是为什么commons-collections
不能在纯净的shiro环境下使用的原因。
但是我们是可以使用CB链的, 因为在shiro-core
和shiro-guice
依赖中声明导入了beanutils.BeanUtils
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
并且对CB进行import
导入的.java文件被使用执行,所以就让CB变成了可被利用的compile
和runtim
中的一种。
但是有一点我们是可以确认的: 如果在target
中的WEB_INF/lib
目录下有依赖包的话那我们就肯定可用了
在本地运行的时候加载了哪些依赖包我们可以在target目录下面查看有哪些依赖包, 下面是我们手动添加了CC依赖后的文件目录
可以看到CC包已经加载进去了
4.2 Tomcat的反序列化
Shiro中对RemenberMe参数的反序列化调用链:
org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedSerializedIdentity
org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
org.apache.shiro.mgt.AbstractRememberMeManager#deserialize
org.apache.shiro.io.Serializer#deserialize
org.apache.shiro.io.DefaultSerializer#deserialize
最后就是在这里返回反序列化结果 return deserialized,调试的话也是在这里执行命令后返回异常.
java.io.ObjectInputStream#readObject()
org.apache.shiro.io.ClassResolvingObjectInputStream#resolveClass
org.apache.shiro.util.ClassUtils#forName
org.apache.shiro.util.ClassUtils.ExceptionIgnoringAccessor#loadClass
org.apache.shiro.util.ClassUtils.ClassLoaderAccessor
下面是org.apache.shiro.io.DefaultSerializer#deserialize代码:
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
在ClassUtils中会对类加载3次:
-
线程加载器
-
current加载器
-
app加载器
实际上前两处用的是同一个加载器, app加载器用于加载JDK的资源, 所以我们主要看第一次的加载.
我们自己写的代码都是在
WEB_INF
下面,app加载器是加载不到的ParallelWebappClassloader是Tomcat用于加载web程序的加载器,如果不实验双亲委派机制的话就会直接使用自己的类加载器去加载
Tomcat使用自己的加载器的时候使用的是findclass函数,不是自己的加载器就是只用forname,实际上和ObjectInputStream.resolveClass函数一样
在当前环境下的全部shiro代码都是在target的lib下面的,所以都是使用findClass
Class.forname是jdk内置的方法,可以进行路径转换所以不会导致数组加载出错,但是app加载器不可以加载数组。所以总的来说就是如果数组中的类都是jdk内置的依赖那就可以成功加载,但是如果成员是我们自己写的类那么就会加载出错。
Tomcat整体类加载体系结构:
0x07 - 构造利用链
加上来的话文章太长了, 所以后面还是分为几篇文章吧: