某远OA最新补丁分析

鉴权流程 致远的主要鉴权逻辑实现均在下com.seeyon.ctp.common.web.filter,这里我们先看CTPSecurityFilter public void doFilter(ServletRequest request, ServletResponse response, Fil

鉴权流程

致远的主要鉴权逻辑实现均在下com.seeyon.ctp.common.web.filter,这里我们先看CTPSecurityFilter

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        boolean accept = false;
        Authenticator authenticator = this.defaultAuthenticator;
        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            try {
                AnonymousRequestPolicy.accept(request, response);
            } catch (BusinessException var12) {
                throw new ServletException(var12);
            }

            HttpServletRequest req = (HttpServletRequest)request;
            HttpServletResponse resp = (HttpServletResponse)response;
            String uri = req.getRequestURI();

            try {
                if (this.isSpringController(uri, req)) {
                    authenticator = this.controllerAuthenticator;
                    accept = authenticator.authenticate(req, resp);
                    if (accept && this.isAjax(uri, req)) {
                        authenticator = this.ajaxAuthenticator;
                        accept = authenticator.authenticate(req, resp);
                    }
                } else if (this.isRest(uri, req)) {
                    authenticator = this.restAuthenticator;
                    accept = authenticator.authenticate(req, resp);
                } else if (this.isV3xAjax(uri, req)) {
                    authenticator = this.v3xAjaxAuthenticator;
                    accept = authenticator.authenticate(req, resp);
                } else if (this.isSOAP(uri, req)) {
                    authenticator = this.soapAuthenticator;
                    accept = authenticator.authenticate(req, resp);
                } else if (this.isServlet(uri, req)) {
                    authenticator = this.servletAuthenticator;
                    accept = authenticator.authenticate(req, resp);
                } else if (this.isJSP(uri, req)) {
                    authenticator = this.jspAuthenticator;
                    accept = authenticator.authenticate(req, resp);
                } else if ("/seeyon/".equals(uri)) {
                    accept = true;
                }
            } catch (Exception var11) {
                logger.error(var11.getLocalizedMessage(), var11);
            }
        }

        if (accept) {
            filterChain.doFilter(request, response);
        } else {
            AppContext.clearThreadContext();
            if (response instanceof HttpServletResponse) {
                try {
                    authenticator.afterFailure((HttpServletRequest)request, (HttpServletResponse)response);
                } catch (Exception var10) {
                    throw new IOException(var10);
                }
            } else {
                this.sendErrorWhenNotHttp(response);
            }
        }

    }

将致远所有的请求分成了7个类型

private boolean isSpringController(String uri, HttpServletRequest request) {
        boolean result = uri.endsWith(".do");
        return !result && uri.indexOf(".do;jsessionid=") > 0 ? true : result;
    }

    private boolean isAjax(String uri, HttpServletRequest request) {
        return uri.endsWith("ajax.do");
    }

    private boolean isV3xAjax(String uri, HttpServletRequest request) {
        return uri.endsWith("getAjaxDataServlet");
    }

    private boolean isRest(String uri, HttpServletRequest request) {
        return uri.startsWith("/seeyon/rest/");
    }

    private boolean isSOAP(String uri, HttpServletRequest request) {
        return uri.startsWith("/seeyon/services/");
    }

    private boolean isServlet(String uri, HttpServletRequest request) {
        return ServletAuthenticator.accept(request);
    }

    private boolean isJSP(String uri, HttpServletRequest request) {
        return uri.endsWith(".jsp");
    }

根据不同类型的url,会走进对应的认证器进行权限校验,如果都不符合,则会走进默认的defaultAuthenticator

关于这个漏洞我们只需要关注两个鉴权逻辑

isSpringController

uri以.do结尾或者包含.do;jessionid=则返回为true,在SpringControllerAuthenticator#authenticate方法中,如果拿不到user对象的时候会判断当前访问的路径是否在白名单内。

if (user == null) {
            AppContext.removeThreadContext("SESSION_CONTEXT_USERINFO_KEY");
            isAnnotationNeedlessLogin = this.isNeedlessCheckLogin(context);
            if (!isAnnotationNeedlessLogin) {
                LoginTokenUtil.checkLoginToken(request);
            }

isRest

uri以/rest/开头,则为RESTFUL类型请求,如果是则返回true

RestAuthenticator#authenticate方法中,通过isIgnoreToken判断/seeyon/rest/之后的路径是否在匿名访问白名单中,如果命中就可以通过验证

1.png2.png该白名单通过静态代码块的方式配置

static {
        sessionUserBlacklist.add(Pattern.compile(".*\\sorgMember.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sorgAccount.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sorgDepartment.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sorgPost.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sorgLevel.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sexternalAccount.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sorgPartTimeJob.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sjoinOrgAccount.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sjoinOrgDepartment.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sjoinOrgPost.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sjoinOrgMember.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sjoinOrgRole.*"));
        sessionUserBlacklist.add(Pattern.compile(".*\\sjoinMetadataDepartment.*"));
        anonymousWhiteList = Arrays.asList("token", "application.wadl", "jssdk", "authentication", "orgMember/avatar", "orgMember/groupavatar", "m3/appManager/getAppList", "m3/appManager/download", "m3/message/unreadCount/", "m3/login/refresh", "m3/common/service/enabled", "uc/systemConfig", "product/configure", "product/hasPlugin", "product/dongle/data", "password/retrieve", "m3/common/system/properties", "m3/trustdo/sdk/login/event", "m3/trustdo/sdk/keyId", "m3/trustdo/sdk/cert/event", "m3/trustdo/sdk/login/name", "m3/trustdo/sdk/server/info", "meeting/meetingInviteCard");
        guestWhiteList = Arrays.asList("cmpNews", "doc", "footprint");
        visitorWhiteList = Arrays.asList("meeting");
    }

可以看到/token是在白名单内,同时存在黑名单路径。

并且该认证器中,判断rest接口不能通过SEESION的方式访问,强制使用token

3.png

漏洞分析

补丁链接:https://service.seeyon.com/patchtools/tp.html#/patchList?type=%E5%AE%89%E5%85%A8%E8%A1%A5%E4%B8%81&id=175

下载对应的版本后,可以看到目录结构如下

4.png

这里直接看checkRoleAccessInfo.properties

5.png

这里意图很明显了,对一些类的方法做了角色校验,可能是未授权或者越权操作。

由于涉及到rest接口,所以还需要翻一下致远的文档

https://open.seeyon.com/

查阅文档后发现,如果需要调用到rest接口,则必须使用token进行调用,而该token又必须由致远OA系统管理员登录后台之后通过创建REST用户的方式才可以生成token

6.png

这里笔者的版本是7.1sp1,可以在后台创建rest用户

7.png

通过文档里提到的方式构造一下请求就能拿到token

拿到token后,我们就可以根据补丁的配置文件来推敲出重置密码的漏洞点了,看样子是位于com.seeyon.ctp.rest.resources.MemberResource#changePassword方法,跟进

8.png

通过传入的idpassword参数来更改目标用户的密码,值得注意的是,这里的idmemberId,也就是数据库里MEMBER_ID字段,如下

9.png

致远OA内置的用户MEMBER_ID是默认的,比如system系统管理员用户的为-7273032013234748168

header里携带token即可构造请求更改system用户的密码了,这里不放poc了,感兴趣的自己去跟下

到这里,貌似确实完成了漏洞复现的目标,但是会发现,未免太鸡肋了一点?毕竟需要系统管理员创建的rest用户来生成token,而且由于前文提到的黑名单路径,poc的路径也正好命中了正则,导致无法前台未授权访问该重置密码接口,那么自然而然就联想到了越权。

越权通常是由普通用户权限越权到管理员操作,在这个漏洞补丁案例里,也是一样的。可我们刚刚不才说了,不能通过携带SESSION进行访问吗?此时我们需要再回过头来看一下致远的CTPSecurityFilter

10.png

这里其实有代审经验的小伙伴一眼就GET到越权的姿势了,既然走进RestAuthenticator会去检查SESSION不能使用接口的话,不让他走进RestAuthenticator认证器就好了,只需要找一个仅校验COOKIE的认证器即可,而isSpringController正好符合要求

11.png

而需要命中isSpringController的条件也很简单,只需要结尾是.do结尾或者包含.do;jsessionid=即可,那么最终利用懂的都懂~~

Comment