最近看到某园区系统去年有个登录绕过+后台上传的组合漏洞,本着学习的心态分析了一下该漏洞,虽说前期了些踩雷,但是也有意外收获。
该项目由struts2和spring两种框架组成,在web.xml中可以看到
请求以.action结尾会按struts2处理,/rest开头会按spring处理
poc都是以.action所以需要关注struts.xml这一配置文件
struts.xml 是 Struts2 框架的核心配置文件,该文件主要用于配置 Action 和请求的对应关系,以及配置逻辑视图和物理视图(逻辑视图就是在 struts.xml 文件中配置的 <result> 元素,它的 name 属性值就是逻辑视图名;物理视图是指 <result> 元素中配置的结果页面,如 JSP 资源的对应关系。
相当于是定义了请求路由对应处理类方法,同时还定义了struts2的处理拦截器
该项目主要定义了如下拦截器
对其一一分析,在Intercptor类中发现了对路由做鉴权限制
当请求路由不在if判断中的其他请求路由会判断session有效性,做鉴权。
第一步首先是访问/admin/sso_initSession.action获取一个session,但是发现上面这个Intercptor类白名单放行的路由里并没有sso_initSession,按理是要鉴权,为啥可以直接访问呢。
在struts2中其实需要配置action中带interceptor-ref这一标签才会走过滤器
而sso_initSession.action中没有定义,所以该接口是可以未授权访问的。
根据配置sso_initSession.action对应的类方法是ssoAction#initSession()方法,跟进看看怎么个事
这里会创建一个为空的user对象,并对session进行初始化,返回session的值。
值得注意的是该user对象其实只是类对象为空,而本身不是null,相对于是一个没权限的session,但是这个session却能绕过拦截器中userBean==null的判断,所以该session可以访问任意接口。
下一步就是调用用户创建接口来创建有权限的角色
具体逻辑是在userAction#save()方法
这里的this.userBean其实是由http传参来构建的,在struts2中参数绑定是内部进行反射构造的,需要绑定的java bean对象必须要实现get/set方法。
主要逻辑是this.userManager.addUser(this.userBean);,前面有个判断如果密码被rsa加密了会解密。
跟进其实现
首先是调用this.validationUser()判断用户名是否存在,不存在的话继续往下
这里isEncrypt为true,还调用EncryptionUtils.getEncryptedText(),对密码进行加密
public static String getEncryptedText(UserBean checkedUser) {
return "true".equals(ConfigReader.getStringIsNull("user.loginpass.encryted")) ? encrypt(checkedUser.getLoginName() + ":dss:" + checkedUser.getLoginPass()) : checkedUser.getLoginPass();
}
public static String encrypt(String text) {
String password = null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(text.getBytes("UTF-8"));
byte[] b = md.digest();
StringBuilder sBuilder = new StringBuilder();
for(int offset = 0; offset < b.length; ++offset) {
int i = b[offset];
if (i < 0) {
i += 256;
}
if (i < 16) {
sBuilder.append("0");
}
sBuilder.append(Integer.toHexString(i));
}
password = sBuilder.toString();
} catch (Exception var7) {
logger.error(var7);
}
return password;
}
相当于用户名+:dss:+明文密码做md5加密。
随后将其存入数据库中完成用户创建。
首先访问/admin/sso_initSession.action创建低权限session
随后访问/admin/user_save.action创建用户
然后就可以使用该账号的登录
通过上面分析在创建新session时会有一个new UserBean()的操作
于是全局搜索该代码字符串
可以看到仅有33处,我们可以排除get相关的方法,最终在其中找到两处调用。
第一处是在VideoxxxAction中存在一个私有方法
private void loginxxxx() {
if (null == this.session.get("user")) {
UserBean userBean = new UserBean();
userBean.setId(1L);
userBean.setLoginName("system");
this.session.put("user", userBean);
this.getVideoPlanManager().setFlushModeEager();
}
}
当session中没有用户信息时,该方法会直接赋予system权限的用户信息,但是因为是私有方法无法直接通过Videoxxx_loginxxx.action直接访问,于是我们向上寻找其用例。
该action提供了init的公有方法
public String init() {
this.loginxxxx();
...
return "videoxxx_init";
}
其中就有调用我们上面的危险方法
访问上述接口获取session值
该session可访问后台接口
另外一个在service里需要跟进到具体调用,感兴趣的可以自己发现一下。
此时我们发现创建的用户其实没有访问/admin的权限,
这主要是因为/WxxS模块是由spring框架开发的,而/admin模块是由struts2框架开发的,两个session对象不是同一个,在/admin模块中的拦截器取到的userBean对象为null,所以导致无法访问/admin下的鉴权接口,因而肯定有东西会将他们的session连接到一块。
我们来看看/WxxS模块登录接口
可以看到数据包中返回的token,字段名为subSystemToken,然后在/admin目录中的loginAction也出现了该字段
该接口会接收subSystemToken的传参,同时在session中找到该token的userBean对象,随后更新session并在返回包中返回
利用该接口的cookie我们就可以正常访问/admin下的路由了
最后是上传点,其在/recover_recover.action中
该类定义了名为recoverFile的File类对象,同时在isProgressCreated为true(默认为true)进入对recoverFile的操作
首先会进入this.validatePassword()方法
可以看到接收一个password参数,将loginName和传入的password初始化出checkUser,调用isMatch方法与数据库中的loginName的用户信息做对比
可以看到其实是判断两个密码是否相同,这里因为当前用户是我们自己创建的所以传入用户名+:dss:+明文密码的MD5值即可绕过该方法的判断。
随后来到this.recoverManager.recover()方法
主要是通过ZipUtils.unZip()方法来解压压缩包文件,按照以往漏洞分析此处应该是有目录穿越可以解压到指定目录
在unzip()方法中果真没有对../的处理,而destDir默认在配置文件中找到
所以构造的压缩包需要穿越到tomcat目录下才可以,即该系统默认为../../../../../../../../../../../../../opt/tomcat/webapps/upload/