Apereo CAS反序列化漏洞中数据加解密研究
- 0x01、简介
- 0x02、网上获取资料
- 0x03、初步运行失败
- 1、分析:
- 2、Tips:
- 0x04、分析原因
- 1、自己写解密算法 / 直接使用cas工程的相关jar包、java文件,调用解密函数
- 2、为什么会解密失败?
- 0x05、断点调试
- Tips:在步入得到过程中,有的方法[步入]功能不进去,需要使用[强制步入]功能
- 0x06、代码分析
- 1、key/iv初始化函数确定
- key是固定的,iv是根据密文计算得来的
- 2、key初始化函数分析
- 3、iv初始化函数分析
- 0x07、总结
- 以此可以推断出,密文的实际构造情况为
- 运行通过
0x01、简介
Apereo CAS,全称为 Central Authentication Service,是一种开源的单点登录(SSO)解决方案。它提供了一个可扩展的、可定制的平台,用于统一身份验证和访问控制,支持多种认证协议和技术。Apereo CAS 可以轻松地集成到现有的应用程序和服务中,为用户提供单点登录和数据交互能力。该系统具有高度的安全性和可靠性,同时也支持多种操作系统和编程语言。Apereo CAS 是一个成熟的、高度稳定的 SSO 解决方案,已经被广泛地应用于大型机构、企业和政府机构中。
4.1.7版本之前存在AES默认密钥的问题,利用这个默认密钥我们可以构造恶意信息触发目标反序列化漏洞,进而执行任意命令。
最近在写攻防中用到的工具(红队版:一键开天门;蓝队版:一键守天门)
其中蓝队版的一个功能:一键解密流量工具,为实现一键智能化
(毕竟蓝队猴子怎么会看得明白什么是shiro、什么是cas、什么是哥斯拉\蚁剑\冰蝎等等呢(对不起,不是所有人但是真的有))
因此需要研究一下CAS漏洞利用中execution值的加解密算法!结果网上没有一个深入研究构造结构体的,大多都是误导文章,而且都是直接套用cas原jar包
结论:UUID + _ + 头部长度标识(7byte) + iv长度标识(1byte) + iv值(16byte) + keyName(10byte) + AES密文
0x02、网上获取资料
针对于网上搜集到的资料进行总结:
- execution值处理流程过程为: 剔除头部UUID字段+base64解密+AES解密+Gzip解密;
- AES存在默认密钥;
- AES加密模式为 AES/CBC/PKCS7;
- 【很多文章里都没有提到】 AES加解密相关的配置会 先去配置文件中获取 ,没有配置密钥信息的会使用 jar包默认的密钥信息 ;
- 密钥库 的密码为 changeit;
- 默认 keystore文件 位于 spring-webflow-client-repo-1.0.0.jar!/etc/keystore.jceks;
- 网上有很多exp工具,直接使用cas工程的相关jar包、java文件,来调用加密函数,不需要自己写;
- cas工程使用的是 Cryptacular 第三方库进行的AES加解密。
真的如网上所说的这些信息就够了么?开始调试
0x03、初步运行失败
1、分析:
- 剔除头部uuid;
- AES\CBC\PKCS7Padding解密,key为changeit;
- iv向量为多少?网上没提到,猜测随机生成;
import com.hotboy.utils.aesUtils;
import com.hotboy.utils.strUtils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class casDecryptTest {
public static byte[] decrypt(String input,aesUtils aes){
String input = "7b951c2a-e78f-4286-95fe-970782352a84_AAAAIgAAABDE2HZ3uiB2bzFHXNO5uObYAAAABmFlczEyOCPJEIAG4U8FA%2b%2bSoqcRzovVlpSWfd/raZVfVf3gXUc8f9Xz%2beN25UhRBwwmMRAv%2byjSVLKbWeRzPkGeVvof/44rS4PcFfF9pzwo%2bEbqJz6ZBCo5%2blAczVCAp8UBjRy7R/jkb/YSZj6YBHDMJ0ejjqFly779A9b3opzyMwCIJod0yvs0qtYNd0qXhd8yY/XpatlelngVKxqLDp0lrwXmP7W3YblsIV/r3bJv2mHk1qAgVL54yTX4en4i17z37qKv6CBkRxZN5ORAERYUm4E%2bsmckjnQEKcjb2bqMWSi7WKxc57DPIBwnjmUJ8plB8aoNsjKxuOYri%2bsBpkZDcFucuyiTOwPOlGm4CaUYHxpoCiaLSJb%2bsj/0Iml107F8L%2bH/pzwtH1BHQef1eVtcml0AKGpVf0YzR9UUua8PysUxbqQpFa36nC3RgIoz97v9Hi6oCkBg9WlarS0QVE7lUSG6SquiT/hzPz9TvP%2b3Yw48BrU04JltH76rboR07zDvgMy3sBk32hKb8w2qbBk5Vo3xVRqwS8Z6fUm1Zl9BnRXr1/kIFP58dLbkwraq%2bD4/%2bbtAMb4F7LB8c%2b1jihZU3vHI7gvpQTtrLr4z%2bqtm8C8NN4rkcbT77QLfKM%2bMCqRhSFzoGyy9Qs%2bhO3Xi34tHUh6oANU3FPP/Cd%2b3/B6w%2bw9W13ecESXG8H6w374I64UiWRQRSnWqciO%2b4BVhkdRtfOF2d7UCw9zL/vqwrTcMgUtbCPtbD1Wj/ucxvut5oeeBPlfPicGz0Ohr9FI0C9j1myRP6DZ6Uv2SlomPnNhrY/z7C6SbjkSF9CLvOhN92XA9OklEkm1HOIm7uFhQ5JL1Fv60muZW/ifqjUrR2kkhUv/LA4nixpcjUpOYRdbT2O7/wIXs7jhInwlFVACx3TL4KrRO6Zb79uGJJdMlHdWREfPr81dxJ9G8u%2bYNYz2djvrbTV%2b/BgZxMRmVugXLH7%2bQRAOhozyvDm5XMnGusjga/NLBFQO4A341puj2QzTdG2R%2bzUexFLeXc2TZ5XgrKqVvjIKeNYPECSKDiZZc%2bj1ivpQt4bUNS2Nx7Hup2%2bUhiwxA8pVtxiVY0YU3QWvphUXUSdu5nFp7qOGz0Yy9m5wOU4kIKyIlJnEeaKVMTETd7TtzhQ5sYEATKpzGUrezaaHei0%2bbkRjGAgK7q8/Wkb/tueJZ2af3IeHOeyulA/%2bHRpvDDMzS0AiGvhvLMVu7PNUKpIKrGuPgIPuXTy6N2WoYQiewnAKekaS03DukE2g%2bTTMFVJ2OXUBpF5MHxAs68NoJCw0s/UgdzKaYWUHH6dS9AjMiYx0eSS7RtUf6bbgMZkLQzL%2bW4CV8gRvacJ7Jn0bsg3zZBGt/8CaVfhNpXU4g3MqAYz9w0iinx0mUPgP7%2bYwf2D2Qg/KkGeY8Qg3sJH4T7oEjs4PjeqbYpPGxjQZd7Uhlv%2b8TkorNmfGXBrl6M9Deow3lWGX/zl1u9uH%2bbNTiSRJAb1dGqqiVtGE3j1Ld/RRgxCI9/TB93dBdovtdKVmhZyjfsIWQS5ypneaYgFbv/l0WBG8GvwGs3QBbYDQbNC8lF5OQ7OS0xCYWBecHqvOlEjU68Yb8crUe3f0q9YXkDum%2b3OlDwmg/SQylqrmO9taYI%2brU8JByMu/ZnTjiWPMOyUx9Codsj5ml6PxMK3OcZHBj7G9BLJJz4XHPisOZxt0LUjKMeH/0itQmxeEnPn%2blvcOWp%2bYNsF6UjmYdnKkSEd9s61jN9lPwCB3m10w%2bRWEIPbbgm6Gn/Yelf1dd4T%2b4e70tJeTWI%2bImR%2be97HxOLEyw3D7aCSzS0LSzTvtGsdk7XrvmiYTzK3aR/i/SQypZCNgpi4vncxLAQ8iJ9xT541Z0gqhWYfJB7XS087RhmnhGfwISZ5rkjYBZPr12Ho5M4xaRKfpu2qWgbnvuBBqMNJWu5JOPMbgVMm3C3DzcRnTs9oVnI81go4j1yJ0tOAguj8iviikAk1/2s3EIgFGnA6gxSZV6XlDwhqpBFzMwQNne/O0bvezmLwQ/Rx%2bjbNSo8ZhmdQ%3d%3d";
byte[] res = null;
String code = "";
input = strUtils.urlDecode(input);
Pattern pattern = Pattern.compile("([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_(.*)");
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
code = matcher.group(2);
}
if( code == null || code == "") {
return res;
}
try {
byte[] encryptData = strUtils.base64Decode(code.getBytes(StandardCharsets.UTF_8));
byte[] key = "changeit".getBytes(StandardCharsets.UTF_8);
byte[] iv = aes.generateRandomBytes(16);
String mode = "CBC";
String padding = "PKCS7Padding";
byte[] result = aes.decrypt(
encryptData,
key,
iv,
mode,
padding
);
aes.mode_AES.set(mode);
aes.padding_AES.set(padding);
aes.key_AES.set(Arrays.toString(key));
aes.iv_AES.set(Arrays.toString(iv));
res = result;
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
}
2、Tips:
你们直接运行不了,很多都是单独封装的模块,可以根据文中意思自己大概写一下。Sorry~ 娜扎
0x04、分析原因
- 工程内,自己已经使用了基于java标准API的crypto封装了更完善的AES加解密方法,并且支持自动检测结果是否存在gzip\class\反序列化特征并再处理,不同于Cryptacular,支持低级别的数据加密。
- 作为高级软件开发工程师,怎么能容忍自己的代码变成屎山呢?
- 突然想起来,自己傻Der了~,Aes的Key怎么可能是changeit,Key应该是16位;
- 全网都在提 “changeit” 硬编码,以下代码,被误导了:
public EncryptedTranscoder() throws IOException {
final BufferedBlockCipherBean bufferedBlockCipherBean = new BufferedBlockCipherBean();
bufferedBlockCipherBean.setBlockCipherSpec(new BufferedBlockCipherSpec("AES", "CBC", "PKCS7"));
bufferedBlockCipherBean.setKeyStore(createAndPrepareKeyStore());
bufferedBlockCipherBean.setKeyAlias("aes128");
bufferedBlockCipherBean.setKeyPassword("changeit");
bufferedBlockCipherBean.setNonce(new RBGNonce());
setCipherBean(bufferedBlockCipherBean);
}
- 因为没有用过Cryptacular库,学习一波才明白,使用"changeit"这个pass初始化密钥库,然后根据Alias(“aes128”)这个keyName获取Aes真正的key。
方法 | 作用 |
---|---|
setKeyStore | 初始化密钥库 |
setKeyAlias | 设置获取密钥类型 |
setKeyPassword | 设置密钥库密码(changeit) |
- 那必然是需要部署相关jar包、java文件调试了(Tips:直接使用网上exp工具项目,他们已经抽取完毕了)
0x05、断点调试
- java中进行AES解密,基本上所有第三方包,都会进入AES解密时的java标准api
方法 | 作用 |
---|---|
cipher.init() | 初始化密码(Cipher)对象,需要传入加密的 mode / key / iv |
cipher.doFinal() | 执行加密或解密操作 |
- 想要获取key及iv向量,关键的就在cipher.init(),找到位置打断点获取key/iv的值,但是无法直接全局搜索cipher.init(因为该方法在jar包中,编辑器不支持搜索jar中class的内容)
- 手动点开jar,根据可能的文件名点开查看寻找;(不推荐)
- 调用外层java中的解密方法,手动根据方法调用找方法(如果开发者再次封装的不繁杂的话可以)
- 调用外层java中的解密方法,打断点先大跳(步过)粗略走流程,然后根据可能位置逐步步入判断
- 采用了第二种方案,结果发现封装的裂开,很容易走错。采用第三种方案:
最终在找到在BufferedBlockCipherBean.class中,调用到了cipher.ini()
Tips:在步入得到过程中,有的方法[步入]功能不进去,需要使用[强制步入]功能
0x06、代码分析
1、key/iv初始化函数确定
key初始化方式的确如之前分析的根据keyName在密钥库中寻找lookupKey
key初始化位置在:params = new ParametersWithIv((cipherParameters)params, header.getNonce())
通过打断点获取到了key真正的值(byte[]):[78, -47, -80, -25, 76, 55, -57, -111, -81, -3, -54, 62, 118, 15, 113, 0]
iv初始化位置在:params = new ParametersWithIv((cipherParameters)params, header.getNonce())
这一步会获取IV向量值,存放在params中,通过多次断点调试,发现针对于相同密文,他的IV是不变的,不同密文,他的IV值是会变的。因此得到一个结论:
key是固定的,iv是根据密文计算得来的
2、key初始化函数分析
- keys固定的,就不带着函数挨个进了,流程如下:
读取配置文件中的配置;默认无,则使用"changeit"这个固定pass初始化解密读取默认密钥库(spring-webflow-client-repo-1.0.0.jar!/etc/keystore.jceks),然后根据Alias(“aes128”)这个keyName遍历获取获取Aes真正的key
3、iv初始化函数分析
代码解读:
- 1、将原始密文读取如bb变量; // 如 [0,0,0,34,0,0,0,16,…]
- 2、大端序重新排列; // 不变
- 3、从bb根据第二部的大端序读取带4个字节兵解释为带符号的整数值,作为header头部长度; // 34
- 4、创建byte[] nonce,大小为iv长度标识; // byte[]{0,0,0,0,0,0…} 大小为16
- 5、从bb的iv长度标识后中读取对应长度放入nonce;
- 6、header头部处理完剩余的部分放入keyName字段; // aes128
0x07、总结
以此可以推断出,密文的实际构造情况为
UUID + _ + 密文
UUID + _ + header头部(34byte) + AES密文
UUID + _ + 头部长度标识(7byte) + iv长度标识(1byte) + iv值(16byte) + keyName(10byte) + AES密文
运行通过
package com.hotboy.content.blueTeam;
import com.hotboy.utils.aesUtils;
import com.hotboy.utils.strUtils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Potato
*/
public class casDecrypt {
public static byte[] decrypt(String input,aesUtils aes){
byte[] res = null;
String code = "";
input = strUtils.urlDecode(input);
Pattern pattern = Pattern.compile("([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_(.*)");
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
code = matcher.group(2);
}
if( code == null || code == "") {
return res;
}
try {
byte[] encryptData = strUtils.base64Decode(code.getBytes(StandardCharsets.UTF_8));
byte[] key = {78, -47, -80, -25, 76, 55, -57, -111, -81, -3, -54, 62, 118, 15, 113, 0};
byte[] iv = new byte[16];
System.arraycopy(encryptData, 8, iv, 0, 16);
String mode = "CBC";
String padding = "PKCS7Padding";
// 剔除header头部34个标志性字节
byte[] tmpEncryptData = new byte[encryptData.length - 34];
System.arraycopy(encryptData, 34, tmpEncryptData, 0, encryptData.length - 34);
encryptData = tmpEncryptData;
byte[] result = aes.decrypt(
encryptData,
key,
iv,
mode,
padding
);
aes.mode_AES.set(mode);
aes.padding_AES.set(padding);
aes.key_AES.set(Arrays.toString(key));
aes.iv_AES.set(Arrays.toString(iv));
res = result;
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
}
运行成功,成功获取源代码:
感谢Allan、Songqb、小严同学,虽然没给我解决任何问题:)