审计反向的过程之前,先记录一下检测 shiro 框架的方法,以及利用攻击方法
shiro检测
人工一点点瞧还是太 old school 了,攻防讲究一个效率为王
yakit插件(burp也同理的)
这里使用 yakit + foxyproxy
两个工具的安装就不多 bb 了,安装好之后给 foxyproxy 配置和 yakit 劫持监听的端口一样的端口然后切换过去
之后本地起一个 shiro ,用 IDEA 方便,然后浏览器挂上代理,yakit 劫持打开,被动检测模块里面勾选 shiro 插件(密钥就去搜然后安装一个)
之后访问本机的 shiro 服务,就可以测试一下 shiro 的检测了
检测到东西会在左上角这里,点开可以看到漏洞情报
如果抓不到访问自己 shiro 的包,就把浏览器访问的网址填成电脑内网 ip,不用 127.0.0.1

EHole
直接 -u 就有了,已经是专门指纹识别的工具了,直接用就行

shiro攻击
shiro-attack
一款针对 shiro-550 的攻击工具
https://github.com/SummerSec/ShiroAttack2
下载下来是个 jar 文件,直接 java -jar 就能启动了
使用也不难,给目标就能从爆破 key 到爆破利用链一口气梭哈
如下图,key 和链子都齐活了以后,可以直接命令执行等操作了

审计流程(反序列化)
读取 cookie – base64 解码 -AES 解密 – 反序列化
base64解码
之前正向的流程用到的接口 rememberSerializedIdentity
获取 cookie 的自然就是 getRememberedSerializedIdentity
protected abstract byte[] getRememberedSerializedIdentity(SubjectContext subjectContext);
确实还是个接口,直接跟进(选择 cookie 开头)如下
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;
}
}
重点还是后半段,读取 http 请求包 ,并且调用 getCookie().readValue() 去获取 base64 值
下面一行是对 cookie 值进行判断,如果已经是被删除的数据,就返回空,判定 rememberme 凭据无效
再下面就是进入一个 if 判断,如果 base64 不为空进入
byte[] decoded = Base64.decode(base64);
进行解码,后面可以看到返回了 decoded
那就去找一下这个东西给返回的 decoded 哪个地方会拿来调用
查看 usages,找到如下
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;
}
看到这一行:
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
调用刚刚 base64 解码的功能然后返回值 decoded 赋给 bytes
之后进入 if 判断,如果 bytes 不是空的
principals = convertBytesToPrincipals(bytes, subjectContext);
根据命名推测是 AES 解密并且反序列化的功能,直接跟进一下看看干了啥
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
看里面这两个命名就知道是解密和反序列化了
先解密,然后结果传参给反序列化并且返回
AES解密
跟进 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;
}
解密最后返回 serialized ,也对应了解密后是返回到了序列化后的状态
解密过程没啥可分析的,转回去看反序列化,跟进
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}
再跟进
T deserialize(byte[] serialized) throws SerializationException;
是个接口,查询一下,跟进 default 开头的
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);
}
}
调用了字节输入流,和序列化的对应,之后就是正常的反序列化操作了,利用对象输入流反序列化
T deserialized = (T) ois.readObject();
java 反序列化漏洞最关键的点 readObject 就在这里了
小结
正向获取信息生成 cookie 和反向 获取 cookie 生成数据的流程审计了一遍,主要的安全隐患点(犯错误的点)有两个
- AES 的 key 硬编码,跟个弱口令似的(复杂但是很轻易就可知的密钥也是弱口令)
- 序列化和反序列化过程中缺失安全校验
