在 Shiro <= 1.2.4 中,AES 加密算法的key是硬编码在源码中,当我们勾选remember me 的时候 shiro 会将我们的 cookie 信息序列化并且加密存储在 Cookie 的 rememberMe字段中,这样在下次请求时会读取 Cookie 中的 rememberMe字段并且进行解密然后反序列化
由于 AES 加密是对称式加密(Key 既能加密数据也能解密数据),所以当我们知道了我们的 AES key 之后我们能够伪造任意的 rememberMe 从而触发反序列化漏洞
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
下载好以后直接解压
然后进入samples/web目录,直接修改pom文件
javax.servlet
jstl
1.2
runtime
org.apache.commons
commons-collections4
4.0
之后直接用idea打开
环境
手动安装哪里有apt instal 香
kali2021
192.168.164.128
修改ssh配置,允许远程连接/etc/ssh/sshd_config
,添加
PermitRootLogin yes
或者
sed -i 's/#PermitRootLogin no/PermitRootLogin yes/g' /etc/ssh/sshd_config
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config
service ssh start
apt-get install tomcat9 tomcat9-docs tomcat9-examples tomcat9-admin
#whereis tomcat9
修改/usr/share/tomcat9/bin/catalina.sh
添加如下代码
CATALINA_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=192.168.164.128 -agentlib:jdwp=transport=dt_socket,address=0.0.0.0:60222,suspend=n,server=y"
service tomcat9 start
新建tomcat server remote
你以为这样就完事了,调试还需要本地的tomcat,是不是感觉远程了个寂寞?
https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v9.0.46/bin/apache-tomcat-9.0.46-windows-x64.zip
解压在idea里面配置即可
首先分析filter
在web.xml中
org.apache.shiro.web.servlet.ShiroFilter
会调用
org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter
之后调用doFilterInternal
初始化进行处理
createSubject
获取上下文信息
这个漏洞的主要成因是加密密钥采用硬编码,所以可以通过定位硬编码找到处理流程
.m2 epositoryorgapacheshiroshiro-core.2.4shiro-core-1.2.4.jar!orgapacheshiromgtAbstractRememberMeManager.class
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
首先会在 AbstractRememberMeManager#getRememberedPrincipals 中将上下文中获取数据传入getRememberedSerializedIdentity 函数中,
跟入getRememberedSerializedIdentity
shiro-web-1.2.4.jar!orgapacheshirowebmgtCookieRememberMeManager.class#getRememberedSerializedIdentity
在SimpleCookie#readValue
获得rememberme,调用ensurePadding补全base64,之后进行base64解码,返回数组
跟入AbstractRememberMeManager#convertBytesToPrincipal
发现调用decrypt,继续跟入
跟入getDecryptionCipherKey
返回了aes key的数组,看下哪里设置的,
在setDecryptionCipherKey处
在setCipherKey中传入的
搜索之后发现,在构造函数中进行调用
public AbstractRememberMeManager() {
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
流程就是AbstractRememberMeManager构造函数进行设置,setCipherKey 中调用了setDecryptionCipherKey设置了decryptionCipherKey属性getDecryptionCipherKey 直接返回了该数组
跟入decrypt
这里要重点看下,因为是取了硬编码的长度来截取base64解密的前16位,之后使用16位之后的进行aes解密
之后调用decrypt进行解密
跟入crypt
调用crypt,使用doFinal进行解密,返回解密结果
在AbstractRememberMeManager#decrypt 中赋值给bytesource传入serialized数组
返回AbstractRememberMeManager#convertBytesToPrincipals
调用deserialize进行反序列化操作,跟入
进入DefaultSerializer#deserialize 对数据进行反序列化
调用readobject
传入rememberme参数
rememberMe=1
跟入JcaCipherService#decrypt
因为ciphertext的长度为0
所以在
System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);
会抛出异常
回到AbstractRememberMeManager#getRememberedPrincipals
跟入
调用forgetIdentity 跟入
调用forgetIdentity
跟入removeFrom
抛出异常最终导致输出deleteMe
在上面的抛出异常下面调用decrypt
跟入decrypt
跟入crypt
就是在doFinal的位置
因为解密异常,所以会抛出异常,也就又回到了AbstractRememberMeManager#getRememberedPrincipals
返回deleteMe
前面的报错都不会产生,在经过反序列化之后,把返回的类转换成PrincipalCollection类型
前面已经分析了解密流程
所以构造的流程就是
1.获取到 反序列化的数据
2.设置AES加密模式,使用AES.MODE_CBC的分块模式
3.设置硬编码的 key
4.使用随机数生成 16 字节的 iv
5.使用 iv + AES加密(反序列化数据) 拼接
6.最后base64加密全部内容
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.io.*;
public class main {
public static void main(String[] args) throws Exception {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("detect.ser"));
obj.writeObject(simplePrincipalCollection);
obj.close();
String path = "detect.ser";
byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aes = new AesCipherService();
ByteSource ciphertext = aes.encrypt(getBytes(path), key);
System.out.printf(ciphertext.toString());
}
public static byte[] getBytes(String path) throws Exception{
InputStream inputStream = new FileInputStream(path);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int n = 0;
while ((n=inputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
return bytes;
}
}
key正确
key错误
https://www.yuque.com/tianxiadamutou/zcfd4v/op3c7v#aad3715c
https://blog.csdn.net/god_zzZ/article/details/108391075