鉴权流程
致远的主要鉴权逻辑实现均在下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/
之后的路径是否在匿名访问白名单中,如果命中就可以通过验证
该白名单通过静态代码块的方式配置
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
漏洞分析
下载对应的版本后,可以看到目录结构如下
这里直接看checkRoleAccessInfo.properties
这里意图很明显了,对一些类的方法做了角色校验,可能是未授权或者越权操作。
由于涉及到rest
接口,所以还需要翻一下致远的文档
查阅文档后发现,如果需要调用到rest
接口,则必须使用token
进行调用,而该token
又必须由致远OA系统管理员
登录后台之后通过创建REST用户
的方式才可以生成token
这里笔者的版本是7.1sp1
,可以在后台创建rest
用户
通过文档里提到的方式构造一下请求就能拿到token
拿到token后,我们就可以根据补丁的配置文件来推敲出重置密码
的漏洞点了,看样子是位于com.seeyon.ctp.rest.resources.MemberResource#changePassword
方法,跟进
通过传入的id
和password
参数来更改目标用户的密码,值得注意的是,这里的id
是memberId
,也就是数据库里MEMBER_ID
字段,如下
致远OA内置的用户MEMBER_ID
是默认的,比如system
系统管理员用户的为-7273032013234748168
header里携带token即可构造请求更改system
用户的密码了,这里不放poc了,感兴趣的自己去跟下
到这里,貌似确实完成了漏洞复现的目标,但是会发现,未免太鸡肋了一点?毕竟需要系统管理员创建的rest
用户来生成token
,而且由于前文提到的黑名单路径,poc的路径也正好命中了正则,导致无法前台未授权访问该重置密码
接口,那么自然而然就联想到了越权。
越权通常是由普通用户权限
越权到管理员操作
,在这个漏洞补丁案例里,也是一样的。可我们刚刚不才说了,不能通过携带SESSION
进行访问吗?此时我们需要再回过头来看一下致远的CTPSecurityFilter
这里其实有代审经验的小伙伴一眼就GET到越权的姿势了,既然走进RestAuthenticator
会去检查SESSION
不能使用接口的话,不让他走进RestAuthenticator
认证器就好了,只需要找一个仅校验COOKIE
的认证器即可,而isSpringController
正好符合要求
而需要命中isSpringController
的条件也很简单,只需要结尾是.do
结尾或者包含.do;jsessionid=
即可,那么最终利用懂的都懂~~