深入理解Shiro反序列化原理( 四 )


image
这个方法里面也分两步,第一步对字节数组进行AES解密,第二步进行反序列化 。

深入理解Shiro反序列化原理

文章插图
image
在解密时就会获取AES密钥,由于这个密钥在对象构造函数中初始化为了默认密钥,导致攻击者可以根据密钥进行伪造恶意的反序列化数据进行代码执行 。
我们再来看反序列化的方法 。
深入理解Shiro反序列化原理

文章插图
image
这里调用了readObject方法导致反序列化,注意这里的调用类并不直接是ObjectInputStream对象,而是自定义的一个继承ObjectInputStream的类,并重写了resolveClass方法 。
深入理解Shiro反序列化原理

文章插图
image
在原生java反序列化底层代码中该方法的作用是根据其读取到完全限定名调用Class.forName()进行类加载获取对应的Class对象 。这里重写该方法主要是为了使用指定的类加载器来进行类加载,因为在tomcat中打破了双亲委派的机制都是使用的自定义类加载进行类加载,我们跟进该方法也可以看到它首先就从进程中获取了不同的类加载器进行类加载 。
public static Class forName(String fqcn) throws UnknownClassException {Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);if (clazz == null) {clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);}if (clazz == null) {clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);}if (clazz == null) {String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +"system/application ClassLoaders.All heuristics have been exhausted.Class could not be found.";throw new UnknownClassException(msg);}return clazz;}正是由于这里自定义了类加载器,主要都是通过类名然后去找对应的class文件,然后通过defineclass进行类加载 。但是由于java中数组的类对象是由jvm创建的,没有对应的class文件,导致在利用时反序列化数组对象时回抛出如下异常 。这也是在shiro中利用cc链的一大限制,但并不是主要原因,其他原因在后面分析利用链时再说 。
深入理解Shiro反序列化原理

文章插图
image
刚刚为了使整个分析流程更加顺畅,所以没有提DefaultSecurityManager#getRememberedIdentity方法中抛出的异常 。
protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) {forgetIdentity(context);throw e;}public void forgetIdentity(SubjectContext subjectContext) {if (WebUtils.isHttp(subjectContext)) {HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);forgetIdentity(request, response);}}private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {getCookie().removeFrom(request, response);}从上面的调用链跟踪最后来到SimpleCookie#removeFrom,在这添加了一个cookie为rememberMe=deleteMe,这也是识别shiro框架的特征 。同时看整个异常的位置是在base64解密之前,就是从base64解密开始后面的AES解密以及反序列化过程只要抛出了没有被处理的异常最后都会被捕获,设置rememberMe=deleteMe 。
深入理解Shiro反序列化原理

文章插图
image
上面是rememberMe的解密过程,下面简单说一下它在登录认证过程中是如何产生的 。
在后端对登录请求的处理一般都会先调用SecurityUtils#getSubject获取对应的subject,然后调用login方法,传入由username和password初始化的AuthenticationToken对象 。
深入理解Shiro反序列化原理

文章插图
image
在认证成功后就会创建一个principals然后加密返回给客户端 。
上面对整个流程进行了粗略的分析,可以了解到在正常流程中rememberMe的值就是PrincipalCollection对象序列化数据的加密后的值 。所以我们在爆破key的时候就可以利用整个对象,但由于它是一个接口,所以我们一般都会利用他的子类SimplePrincipalCollection进行爆破,然后根据返回结果中是否含有deleteMe判断密钥是否正确 。
深入理解Shiro反序列化原理

文章插图
image
利用链在前面分析中找到了ObjectInputStream#readObject的调用点,我们利用还需要找到能利用的反序列化链,我们前面了解了CC链,以及URLDNS等 。如果直接尝试CC链可能会出现如下报错:
深入理解Shiro反序列化原理

文章插图
image
因为在shiro默认的依赖中不好看CC依赖,导致无法反序列化,然后我们补上CC依赖后再打可能又会遇到下面的报错,Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;],这就是由于无法创建Transformer数组导致的 。所以在打CC依赖的时候必须要找一条不包含数组的链,这个的原因在上面也说了 。


推荐阅读