某园区系统登录绕过分析

最近看到某园区系统去年有个登录绕过+后台上传的组合漏洞,本着学习的心态分析了一下该漏洞,虽说前期了些踩雷,但是也有意外收获。

某园区系统登录绕过分析

最近看到某园区系统去年有个登录绕过+后台上传的组合漏洞,本着学习的心态分析了一下该漏洞,虽说前期了些踩雷,但是也有意外收获。

0x01 前置知识

该项目由struts2和spring两种框架组成,在web.xml中可以看到

image-20240111145041392

image-20240111145108614

请求以.action结尾会按struts2处理,/rest开头会按spring处理

poc都是以.action所以需要关注struts.xml这一配置文件

struts.xml 是 Struts2 框架的核心配置文件,该文件主要用于配置 Action 和请求的对应关系,以及配置逻辑视图和物理视图(逻辑视图就是在 struts.xml 文件中配置的 <result> 元素,它的 name 属性值就是逻辑视图名;物理视图是指 <result> 元素中配置的结果页面,如 JSP 资源的对应关系。

相当于是定义了请求路由对应处理类方法,同时还定义了struts2的处理拦截器

该项目主要定义了如下拦截器

image-20240111150740879

对其一一分析,在Intercptor类中发现了对路由做鉴权限制

image-20240111151011369

当请求路由不在if判断中的其他请求路由会判断session有效性,做鉴权。

0x02 漏洞分析

第一步首先是访问/admin/sso_initSession.action获取一个session,但是发现上面这个Intercptor类白名单放行的路由里并没有sso_initSession,按理是要鉴权,为啥可以直接访问呢。

在struts2中其实需要配置action中带interceptor-ref这一标签才会走过滤器

image-20240111152155971

sso_initSession.action中没有定义,所以该接口是可以未授权访问的。

根据配置sso_initSession.action对应的类方法是ssoAction#initSession()方法,跟进看看怎么个事

image-20240111152756210

这里会创建一个为空的user对象,并对session进行初始化,返回session的值。

值得注意的是该user对象其实只是类对象为空,而本身不是null,相对于是一个没权限的session,但是这个session却能绕过拦截器中userBean==null的判断,所以该session可以访问任意接口。

下一步就是调用用户创建接口来创建有权限的角色

具体逻辑是在userAction#save()方法

image-20240111160012932

这里的this.userBean其实是由http传参来构建的,在struts2中参数绑定是内部进行反射构造的,需要绑定的java bean对象必须要实现get/set方法。

主要逻辑是this.userManager.addUser(this.userBean);,前面有个判断如果密码被rsa加密了会解密。

跟进其实现

image-20240111161131183

image-20240111160952217

首先是调用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加密。

随后将其存入数据库中完成用户创建。

0x03 漏洞复现

首先访问/admin/sso_initSession.action创建低权限session

image-20240111183308280

随后访问/admin/user_save.action创建用户

image-20240111183622621

然后就可以使用该账号的登录

image-20240111183734191

0x04 举一反三

通过上面分析在创建新session时会有一个new UserBean()的操作

image-20240111184204572

于是全局搜索该代码字符串

image-20240111184412519

可以看到仅有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值

image-20240111193723778

该session可访问后台接口

image-20240111194029801

另外一个在service里需要跟进到具体调用,感兴趣的可以自己发现一下。

0x05 补充

此时我们发现创建的用户其实没有访问/admin的权限,

image-20240126183735306

image-20240126183236759

这主要是因为/WxxS模块是由spring框架开发的,而/admin模块是由struts2框架开发的,两个session对象不是同一个,在/admin模块中的拦截器取到的userBean对象为null,所以导致无法访问/admin下的鉴权接口,因而肯定有东西会将他们的session连接到一块。

我们来看看/WxxS模块登录接口

image-20240125173258733

可以看到数据包中返回的token,字段名为subSystemToken,然后在/admin目录中的loginAction也出现了该字段

image-20240125174657151

该接口会接收subSystemToken的传参,同时在session中找到该token的userBean对象,随后更新session并在返回包中返回image-20240125175040576

利用该接口的cookie我们就可以正常访问/admin下的路由了

image-20240126184444652

image-20240126184513538

最后是上传点,其在/recover_recover.action

image-20240125175330566

该类定义了名为recoverFile的File类对象,同时在isProgressCreated为true(默认为true)进入对recoverFile的操作

首先会进入this.validatePassword()方法

image-20240125175619457

可以看到接收一个password参数,将loginName和传入的password初始化出checkUser,调用isMatch方法与数据库中的loginName的用户信息做对比

image-20240125180104728

可以看到其实是判断两个密码是否相同,这里因为当前用户是我们自己创建的所以传入用户名+:dss:+明文密码的MD5值即可绕过该方法的判断。

随后来到this.recoverManager.recover()方法

image-20240125180419966

主要是通过ZipUtils.unZip()方法来解压压缩包文件,按照以往漏洞分析此处应该是有目录穿越可以解压到指定目录

image-20240125180741322

unzip()方法中果真没有对../的处理,而destDir默认在配置文件中找到

image-20240125180857728

所以构造的压缩包需要穿越到tomcat目录下才可以,即该系统默认为../../../../../../../../../../../../../opt/tomcat/webapps/upload/

4 条评论

不羡仙
大佬太强了,能给套代码学习一下吗
redfish
https://forum.butian.net/share/2912
一只柒柒
有源码吗
rev1ve
求源码