发布于12月5日12月5日 最近因为工作问题需要接触Shiro,然后就想起了Shiro的两个非常著名的逆序漏洞,Shiro550(CVE-2016-4437)和Shiro721(CVE-2019-12422)。这两个洞曾经在大大小小的攻防演练中大放异彩,也是很多保安公司面试官喜欢问的面试问题。我会趁着下班的间隙回顾一下这两个经典漏洞。 本文主要讲**shiro550 (CVE-2016-4437)**和**shiro721 (CVE-2019-12422)**,以及利用工具**ShiroAttack2(https://github.com/SummerSec/ShiroAttack2)**。我的能力总体来说是有限的。如果我写得不好,请给我一些建议。 ## shiro550 (CVE-2016-4437) ### 查找源代码 1.源代码:vulhub shiro/CVE-2016-4437。克隆vulhub项目,进入目录,执行docker-compose up -d。服务起来后,进入容器,使用ps命令找到正在运行的jar。 - 1.将jar包cp到主机上,然后从虚拟机中取出来,打开idea并创建一个目录**libs**把jar放进去,右键该目录选择添加为库 - ### 查看路由 1、通过**MANIFEST.MF**确定jar包运行的入口点。我们看到这里使用了spring,但是对我们没有任何影响。我们可以看到这里的代码量非常少,而且命名也非常清晰。我们直接使用达达的快速代理审核技巧来寻找路线和一组认证小技巧。看到**UserController**,看过mvc的道友一看就知道它不是人(控制器)。 2、**UserController**的代码如下。让我们从这里开始吧。我们知道shiro是一个提供身份验证、授权、密码学和会话管理的工具。很明显,这里唯一接收参数或者POST请求的函数就是**doLoginPage**,而且它还获取了密钥**rememberme** - `javaimportorg.apache.shiro.SecurityUtils;importorg.apache.shiro.authc.AuthenticationException;importorg.apache.shiro.authc.UsernamePasswordToken;importorg.apache.shiro.subject.Subject;//.多余的省略@Controller public class UserController { public UserController() { } @PostMapping({\'/doLogin\'}) public String doLoginPage(@RequestParam(\'username\') String 用户名, @RequestParam(\'password\') String 密码, @RequestParam(name=\'rememberme\',defaultValue=\'\') String RememberMe) { 主题subject=SecurityUtils.getSubject(); }尝试{ subject.login(new UsernamePasswordToken(用户名, 密码, RememberMe.equals(\'remember-me\')));返回“forward:/”; } catch (AuthenticationException var6) { return \'forward:/login\'; } } @RequestMapping({\'/\'}) public String helloPage() { return \'hello\'; } @RequestMapping({\'/unauth\'}) public String errorPage() { return \'error\'; } @RequestMapping({\'/login\'}) public String loginPage() { return \'login\'; } }` ### 查找身份验证 #### doLoginPage() 1、代码小,无需调试。可以直接看到**doLoginPage**函数。首先执行`Subjectsubject=SecurityUtils.getSubject()` 并传递SecurityUtils.getSubject();获取一个安全主体,并将其赋值给变量subject(在shiro中,Subject代表当前用户或系统的安全主体,可以是用户、程序、服务等)。 #### 用户名密码令牌 1、然后执行`subject.login(newUsernamePasswordToken(username,password,rememberMe.equals(\'remember-me\')));`,首先创建一个**UsernamePasswordToken**类的新对象,并将用户名、密码以及请求参数是否包含\'remember-me\'的判断结果(rememberMe.equals(\'remember-me\'))传递给的构造函数**用户名密码令牌**。 2、因为shirodemo-1.0-SNAPSHOT.jar是作为库添加的,所以没有办法直接用ctrl+鼠标左键跳转到对应的方法,所以我将整个shirodemo-1.0-SNAPSHOT.jar解压,将其lib目录下的内容全部复制到我的libs目录下,然后再进行操作。 3.首先看**UsernamePasswordToken**类的构造函数 - - 这很简单,只需用它来调用你自己的另一个构造函数即可。 Java 类支持多个构造函数,只要参数传递方式不同即可。我们简单看一下几个参数: - 用户名字符串直接作为参数传递,没有任何变化。 - `(char[])(密码!=null?password.toCharArray():null)` (char\\[\\]) 是强制把右边的变量变成字符数组。右边括号里的叫做三元运算或者三元表达式等,是一种简单的if语言。如果password !=null,则使用password.toCharArray(),否则使用null。如果我们正常输入用户名和密码,并检查rememberMe,则password !=null成立,并执行password.toCharArray()将密码从字符串更改为字符数组,并强制转换为(char\\[\\])类型。 - RememberMe boolean 直接传递参数,不做任何改变 - (String)null String 传入强制为null 的字符串 - 找到满足参数的构造方法,照着做,发现只有简单的赋值操作 - #### subject.login() 1、返回**UserController**,开始进入**subject.login**方法,正式开始进入shiro。代码位于shiro-core的**Subject.class**中 - 进入登录方法,发现Subject是一个接口。点击左侧图标,idea会自动找到具体的实现代码。还好,实现直接通过了。 - - 具体实现代码位于shiro-core的**DelegatingSubject.class**中,内容如下: - #####clearRunAsIdentitiesInternal() 1、第一行`this.clearRunAsIdentitiesInternal();` 关注拥抱 2、发现自己的`this.clearRunAsIdentities();`又被调用了,然后就跟了上去。 3. getSession(false) 方法通常用于获取当前用户的session。如果用户还没有会话(未登录),则返回“null”。如果会话不等于空,则执行session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);`。 `removeAttribute(Objectvar1)` 方法是在`org.apache.shiro.session.Session` 接口中定义的方法。该方法用于从会话中删除指定名称的属性,并返回被删除属性的值。 4、先看**getSession**。第一个if是和日志相关的,不用看。第二个是因为传入的**create**值为flase,所以不会被输入或读取,最后`returnthis.session;` 5.我还没有开始调试。不知道返回的session是否等于null,所以查看了`session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);` 6、**RUNASPRINCIPALSSESSIONKEY**是一个全局变量(一般都是大写字母,思路也会有颜色)。它的值是一个字符串,类名加上.RUNASPRINCIPALSSESSIONKEY 7. 回来看看`session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);`。跟进removeAttribute,发现有很多奇怪的实现。我不知道我会选择哪一个。我将跳过这个并继续阅读。过一段时间我就会知道。 ##### this.securityManager.login() 01. 退出clearRunAsIdentitiesInternal并继续向下到`this.securityManager.login(this,token);`。该方法除了传入当前对象外,还传入新的令牌类(即之前的UsernamePasswordToken)。有必要看一下。 02、跟进**this.securityManager.login**方法,发现进入了另一个界面。幸运的是,登录只有一种实现。只要跟着它看一下就可以了。 03.登录的实现代码位于shiro-core的**DefaultSecurityManager.class** 内容如下 04.第一行声明一个AuthenticationInfo类型的变量info。我们先来看看。 **信息=this.authenticate(token);** 05.以下是**authenticate**的代码,再次启动套娃,继续。 06.是另一个接口,但是只有两个实现。您可以看看下面 07.我很尴尬。一路上只记得看一下函数。第二个实现就是上面的**authenticate**,所以只看第一个实现。 08.第一个实现是**public Final AuthenticationInfo verify(AuthenticationToken token)**。函数有点长,我们看一下。 09、首先判断token是否为null,显然不是这样的。直接查看else 10.第一行是日志,不要读,第二行创建一个新变量,不要读,第三行进入try,开始调用函数,返回值到info 11. 跟进`this.doAuthenticate(token);`,和以前一样是一个接口,但是只有一个实现,保持和以前一样就可以了。 12、实现代码如下: 1、首先看this.assertRealmsConfigured();的代码。这似乎是对某个配置的检查。首先获取对象的realms属性,然后判断是否为空。如果为空,则会抛出异常。 2.再看一下Collectionrealms=this.getRealms();获取对象的realms属性 3.最后看`realms.size()==1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(),authenticationToken):this.doMultiRealmAuthentication(realms,authenticationToken);`。熟悉的三元运算判断上一步获取到的变量realms的大小是否等于1,如果等于1则执行doSingleRealmAuthentication。如果不等于1,则执行doMultiRealmAuthentication。 1、首先看doSingleRealmAuthentication 2.直接看`AuthenticationInfoinfo=realm.getAuthenticationInfo(token);` Realm也是一个接口,doMultiRealmAuthentication只有一个实现,直接遵循即可。 3、**getAuthenticationInfo**的实现代码如下。这里首先通过**getCachedAuthenticationInfo**获取变量info。它有两个if判断,分别检查info是否为null或不为null,并且每个判断都有自己的函数调用。我不知道如果不做任何改变会怎样,但它最终会返回一个信息。让我们继续阅读。 4.返回doSingleRealmAuthentication函数。如果info不为null,则直接返回info。 doMultiRealmAuthentication 的逻辑类似。不同的是,它获取info后,使用另一个函数将info作为参数传入,最终返回一个AuthenticationInfo类型的变量聚合体。 doSingleRealmAuthentication和doMultiRealmAuthentication的命名和代码逻辑非常相似,所以我们跳过doMultiRealmAuthentication,不再分析。 13. 这将返回到验证方法。如果信息=this.doAuthenticate(token);没有发生异常,就会进入**notifySuccess**方法。 14.继续跟踪**notifySuccess**,没有发现反序列化 15.然后返回信息。看到这里,我大胆猜测,这个信息应该包含一些用户信息或者其他需要用来验证的数据。它通过这层代码得到这个东西,应该就是后面触发漏洞的地方。 ##### cr
创建帐户或登录后发表意见