0x01 漏洞时间线
- 10月16日 长亭安全研究员发现该漏洞
- 10月31日 官方发布新版本修复漏洞
- 10月31日 17:59 长亭安全应急响应中心发布通告
- 10月31日 18:50 beichen 已经复现
- 10月31日 threedream师傅疑问,beichen确认
1
2
3假设存在URL为/barspace/bar.action的请求,而且/barspace的命名空间下没有名为bar的Action,则默认命名空间下名为bar的Action也会处理用户请求。但根命名空间下的Action仅仅处理根命名空间下的Action的请求,这是根命名空间和默认命名空间的差别。
命名空间仅仅有一个级别。假设请求的URL是/bookservice/search/get.action。系统将先在/bookservice/search的命名空间下查找名为get的Action,假设在该命名空间内找到名为get的Action。则由该Action处理用户的请求。假设未找到。系统将直接进入默认的命名空间中查找名为get的Action,而不会在/bookservice的命名空间下查找名为get的Action 。
0x02 Namespace & Action 机制
上文三梦师傅提到Namespace 下不存在对应的Action,那么会往上找Action,从而产生问题。
Namespace 有default-interceptor-ref,如果这两个Namespace 不一致,而且对应的Action 没有配置interceptor,用的Namespace 的default interceptor,那么就可能存在权限问题。
1 | <package name="setup" extends="default" namespace="/setup"> |
如上,
- /setup 默认的interceptor 是validatingSetupStack
- /json 默认的默认的interceptor 是validatingStack,这个interceptor-stack 不包含setupStack,所以可以绕过安装检测
1 | <interceptor-stack name="defaultSetupStack"> |
2.1 Namespace String
Namespace 的逻辑为uri path 中到最后一个反斜杠的子串
在ServletDispatcher中,决定了Namespace 和ActionName
1 | this.serviceAction(request, response, this.getNameSpace(request), this.getActionName(request), this.getRequestMap(request), this.getParameterMap(request), this.getSessionMap(request), this.getApplicationMap()); |
具体逻辑如下
ServletDispatcher
1 | protected String getNameSpace(HttpServletRequest request) { |
1 | public static String getNamespaceFromServletPath(String servletPath) { |
2.2 Action String
action string 对应的是最后一个/到最后一个. 之间的子串
1 | protected String getActionName(HttpServletRequest request) { |
1 | protected String getActionName(String name) { |
2.3 Namespace & Action
1 | getActionConfig:137, PluginAwareConfiguration$RuntimeConfigurationImpl (com.atlassian.confluence.setup.webwork) |
漏洞逻辑如下:
1 | public synchronized ActionConfig getActionConfig(String namespace, String name) { |
这里可能有两个问题
- 如果namespace实际不存在,也会从””里面获取,这里有46个action,可能会存在问题
- namespace 存在extends继承,但是不会继承其interceptors,到值权限问题。
比如:
1 | <package name="setup" extends="default" namespace="/setup"> |
1 | <package name="admin" extends="setup" namespace="/admin"> |
在xwork.xml的定义中
- admin namespace extends setup namespace
- setup default interceptor validatingSetupStack,对应33个interceptor
- admin default interceptor validatingStack,对应32个interceptor
其中dosetuplicense action 没有自定义interceptor,所以继承的namespace的default interceptor,导致处理的interceptor 不同。
- 在setup namespace 中
- 在admin namespace 中
0x03 Confluence的认证机制
在之前的一篇分析文章中,参考【2】,有提到Confulence的账号权限逻辑。现在看当时的分析还是太浅尝辄止了,这里再过一遍。
3.1 CVE-2021-26084 管中窥豹
还是以CVE-2021-26084 的doenterpagevariables.action 为授权为例进行跟踪。
3.1.1 doenterpagevariables
doenterpagevariables 继承的pages namespace 的default interceptor: validatingStack
1 | <package name="pages" extends="default" namespace="/pages"> |
createpage 用到自定义的defaultStack interceptor
1 | <action name="createpage" class="com.atlassian.confluence.pages.actions.CreatePageEntryAction" method="doDefault"> |
实际上validatingStack包含了defaultStack,为什么反而能为授权访问呢?
1 | <interceptor-stack name="validatingStack"> |
3.1.2 ConfluenceXsrfTokenInterceptor
实际上interceptor的不同只是授权的一个原因,还有原因是里面的
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
在此处result=input
说明下前文struts2 架构图中的interceptors 为什么只有3个。这是因为一般的interceptors invocation接口,默认最后都要return invocation.invoke(),以此交由下一个interceptor处理。当然如果当前interceptor 也可以直接返回其他的code,结束interceptors。
具体可以看DefaultAcationInvocation#invoke的逻辑,当某一个interceptor有返回resultCode,而不是invocation.invoke(),会进入executeResult,将executed 置为true,退出当前循环。
1 | public String invoke() throws Exception { |
而在ConfluenceXsrfTokenInterceptor 中,extends 的 XsrfTokenInterceptor,其实现如下
1 | public String intercept(ActionInvocation invocation) throws Exception { |
而在doenterpagevariables 对应的action中,input对应的是渲染vm模板?
1 | <action name="doenterpagevariables" class="com.atlassian.confluence.pages.actions.PageVariablesAction" method="doEnter"> |
疑问:为什么error 和input也是渲染模板,success也是
疑问,这里问什么这了多resultconfig,xml中只配置了3个。
提示:有些interceptor会返回input的result,也会触发对应的result处理,不一定全在action 逻辑中。
3.1.3 createpage
同样的,先看result映射
3.2 认证相关 Interceptors
- ConfluenceAccessInterceptor
- PageAwareInterceptor
- PermissionCheckInterceptor
- UserAwareInterceptor
3.2.1 ConfluenceAccessInterceptor
总结:
ConfluenceAccessInterceptor 是用来实现以下四个注解的,
- PublicAccess: 所有都可访问
- RequiresAnyConfluenceAccess: 对应LICENSED_ACCESS/UNLICENSED_AUTHENTICATED_ACCESS/ANONYMOUS_ACCESS
- RequiresLicensedConfluenceAccess:对应LICENSED_ACCESS
- RequiresLicensedOrAnonymousConfluenceAccess
逻辑1: 如果目标 package / class / method 没有以上的注解,那么返回ABSTIN,也可以认证通过
逻辑2: 如果目标 package / class / method 有以上注解,那么和当前用户对应的权限进行匹配,返回 GRANTED / DENIED。
具体逻辑如下:
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
Access 判断逻辑
ActionAccessChecker
1 | public boolean isAccessPermitted(Object action, { String methodName) |
1 | private AccessDecision checkUserAccessFromAnnotations(Class<?> actionClass, { String methodName, ConfluenceUser currentUser) |
AccessStatusImpl结构
- LICENSED_ACCESS
- UNLICENSED_AUTHENTICATED_ACCESS
- ANONYMOUS_ACCESS
- NOT_PERMITTED
1 | static AccessDecision checkAccessAnnotations(AnnotatedElement annotatedElement, Supplier<AccessStatus> accessStatusSupplier) { |
四个权限相关的注解
- PublicAccess: 所有都可访问
- RequiresAnyConfluenceAccess: 对应LICENSED_ACCESS/UNLICENSED_AUTHENTICATED_ACCESS/ANONYMOUS_ACCESS
- RequiresLicensedConfluenceAccess:对应LICENSED_ACCESS
- RequiresLicensedOrAnonymousConfluenceAccess:对应LICENSED_ACCESS / UNLICENSED_AUTHENTICATED_ACCESS
其对应的验证逻辑如下:
1 | private static final Map<Class<? extends Annotation>, Predicate<AccessStatus>> POSITIVE_ACCESS_CHECKS = ImmutableMap.builder().put(PublicAccess.class, (accessStatus) -> { |
举个注解的例子
1 |
|
3.2.2 PageAwareInterceptor
PageAwareInterceptor 也是权限相关,。总结:
PageAwareInterceptor 是理解是负责confluence page权限的。
看看具体的逻辑:
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
具体逻辑如下:
1 | public Result configure(PageAware pageAware, HttpServletRequest servletRequest, ParameterSource parameterSource) { |
具体不跟踪了,应该是confulence 文章/空间的权限问题。
3.2.3 PermissionCheckInterceptor
也是个权限相关的Interceptor,总结:
PermissionCheckInterceptor 是检测读写行为相关的
逻辑1: 未安装完成时,都是可写的(未安装完成也无法开启只读模式)
逻辑2: 如果开启了只读模式(默认不启用,可以参考https://blog.csdn.net/u013587602/article/details/84926864)
a. 如果action 的package / class / method 有ReadOnlyAccessBlocked 注解,那么返回readonly
b. 默认返回readonly
c. 逻辑3.a 中存在一种特殊情况
逻辑3: 如果action isPermitted 为false,默认返回”notpermitted”,除非一下情况
a. 如果开启readonly只读模式,且是POST/PUT/DELETE 操作,而且namespace是/admin开头、或者package / class / method 有ReadOnlyAccessAllowed 注解,那么允许访问,否则返回readonly
b. 如果是space 相关action,并且当前space 是私有的,那么返回”notpermittedpersonal”
逻辑4: 其他情况,返回ok
看看具体逻辑
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
- ContainerManager.isContainerSetup() 判断是否安装
1 | private boolean isReadOnlyAccessAllowed(ActionInvocation actionInvocation, Package actionClassPackage, Class<? extends Action> actionClass) { |
功能:
- 如果namespace 以/admin开始
- 或者class / package/ method 有ReadOnlyAccessAllowed 注解,那么返回true
这里存在问题
isPermitted
1 | public boolean isPermitted() { |
- 个人理解:除开admin,这里都会false,未验证
- 实际测试:未登陆用户isPermitted 也为true ??所以未登录客户可以直接bypass这个规则。
重要:
3.2.4 UserAwareInterceptor
总结:
UserAwareInterceptor 是鉴定和用户相关操作的
具体逻辑:
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
3.2.5 SetupCheckInterceptor
前文中,判断是否已经安装有通过
- ContainerManager.isContainerSetup()
- this.getBootstrapManager().isSetupComplete()
在SetupCheckInterceptor 中,同时检验了这两处。
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
3.2.6 SetupIncompleteInterceptor
1 | public String intercept(ActionInvocation invocation) throws Exception { |
1 | private boolean isSetupComplete() { |
逻辑同上,不过是检测没安装完成,如果没安装完成,就返回notsetup,进入安装。
3.3 区别
interceptor | doenterpagevariables | createpage |
---|---|---|
ConfluenceAccessInterceptor | Y | Y |
PageAwareInterceptor | Y | pagenotpermitted |
注意这里的pageAware instanceof CreatePageEntryAction
导致进入了PAGE_NOT_PERMITTED 逻辑。
在doenterpagevariables中,直接进入到最下面的PageAwareHelper.Result.OK。
疑问:可能是健壮性设计要求?这个像个白名单逻辑,很容易出问题。
3.4 isPermitted
isPermitted 在最基础的ConfluenceActionSupport 类中出现,其他的子类中,也有对这一方法的重写。
如果还记得PermissionCheckInterceptor,中间有这么一段逻辑
1 | if (!confluenceAction.isPermitted()) { |
这里才是整个action权限验证的关键:重写isPermitted 实现权限约束。
3.4.1 admin权限
看看admin 相关action是如何通过isPermitted 实现权限约束的。
举个例子:ConfigureLog4jAction,管理员配置日志功能
1 | public boolean isPermitted() { |
0x04 漏洞挖掘
从漏洞关键点出发:
利用namespace 继承父namespace 的actions,但是default interceptor 用的却不是父namespace的,会造成interceptors的不一致
4.1 Actions with default Interceptor
思路如下:
setup namespace 默认setupStack,有SetupCheckInterceptor
admin/json namespace 默认validatingStack,没有setupStack
看看有哪些setup namespace 中没有SetupCheckInterceptor 的action。
筛选下来,有以下action:
- getbundles
- dosetuplicense
- setupcluster
- setupdatasourcedb
- setupadministrator
- setup-restore
- setup-restore-local
- finishsetup
- finishclustersetup
试试/json/getbundles.action,发现过不了,原因为:
GetBundlesAction extends AbstractSetupAction,其isPermitted 接口如下:
1 | public boolean isPermitted() { |
剩下几个类似,也都是AbstractSetupAction子类,也没有重写isPermitted
4.2 setup-restore & setup-restore-local 未授权访问
直到setup-restore,重写了isPermitted,always true,导致可以为授权访问。
SetupRestoreAction
1 | public boolean isPermitted() { |
但是发现过不了XsrfTokenInterceptor 的alt_token
1 | public boolean validateToken(HttpServletRequest request, String token) { |
SSRF Token,在实现上,Confluence 并没有一个From & Session 一个Token,而是一个Session 一个Token,所以可以找到其他的未授权的From 表单,找到对应的atl_token
找到了login.action 登陆接口。
至此实现了未授权访问
4.3 setup-restore 网站备份恢复
恰巧的是setup-restore.action 是网站备份恢复接口,post 个zip 过去可以直接覆盖网站。
所以这是个“坐牢洞”,会严重影响系统数据,不可以在线上尝试。
搭配参考文章2中的Confluence 后台RCE,应该可以实现漏洞公告的效果,这里未实际验证,暂不深究了。
0x05 总结与思考
- 这是Struts xwrok.xml 配置实现中存在的通用问题:子namespace 会继承父namespace 的actions,但是却不会继承其default interceptors
- 算是个逻辑漏洞,作者很细心,也一定是对Struts很了解,由一点小bug引发的RCE
如何才能发现该漏洞:
- 深入分析struts 原理,了解namespace 和action、interceptor 机制才能发现
- 自动化不能解决此类问题