Confluence CVE-2021-26084 是前台能够触发的RCE,CVSS9.8,危害不小。漏洞原因是因为Velocity 写得有问题导致可以Ognl注入,这种模板问题导致的漏洞挺少见,但是很适用,值得分析。
0x01 环境搭建
P🐮的Vulhub 有镜像,参考【2】,同样调试接口没有开出来,可以参考之前的《Confluence CVE-2022-26134 OGNL 分析》一文搭建环境。
新建Dockerfile
1
2FROM vulhub/confluence:7.4.10
RUN sed "67 iCATALINA_OPTS=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 \$\{CATALINA_OPTS\}\"" -i /opt/atlassian/confluence/bin/setenv.sh更新docker-compose.yml
1
2
3
4build: .
ports:
- "8090:8090"
- "5005:5005"
0x02 漏洞复现
参考【1】中Lotso师傅给的几个patch
confluence/users/user-dark-features.vm
value='$!action.featureKey' => value=featureKey
7.12.4版本已经对该文件进行了更改
confluence/login.vm
value='$!action.token' => value=token
7.12.4版本已经对该文件进行了更改
confluence/pages/createpage-entervariables.vm
1
2value='$!querystring' => value=querystring
value='$linkCreation' => value=linkCreationconfluence/template/custom/content-editor.vm
对多个input标签进行了value的替换,主要是将value中的$去掉
"Hidden" "name='linkCreation'" "value='$isLinkCreation'" => "Hidden" "name='linkCreation'" "value=isLinkCreation"
JAR文件的中templates/editor-preload-container.vm
value='$!{action.syncRev}' => value=syncRev
以doenterpagevariables.action 为例
0x03 原理解析
3.1 doenterpagevariables.action 调用链跟踪
顺着执行跟踪
1. doenterpagevariables.action
根据补丁消息,可以定位到createpage-entervariables.vm,对应action为doenterpagevariables
1 | <action name="doenterpagevariables" class="com.atlassian.confluence.pages.actions.PageVariablesAction" method="doEnter"> |
2. PageVariablesAction#doEnter
在PageVariablesAction#doEnter,debug打点发现未进入
此小结与整体跟踪无关,仅探讨为何未进入debug
struts2 执行流程如下,interceptors 一个个执行,如果哪一个返回了有效的result,则不会进入后续的流程了。
执行到ConfluenceXsrfTokenInterceptor,返回了”input” result,直接进行了executeResult,因此避开了doEnter 方法。
3. 返回 input,进行Velocity渲染 createpage-entervariables.vm
1 | <form name="filltemplateform" method="POST" action="doenterpagevariables.action"> |
4. AbstractTagDirective tag子语句
渲染Tag #tag (“Hidden” “name=’queryString’” “value=’$!queryString’”)
此刻Tag 有3个子节点
5. 第三个子节点ASTStringLiteral
第三个子节点ASTStringLiteral#value
Velocity 一文中有单独介绍ASTStringLiteral 节点,不过这里稍为复杂点,interpolate为true,需要this.nodeTree.render(context, writer);
其nodeTree 为ASTReference
6. 子节点ASTReference#getVariableValue
进过一些列的调用,最终到执行到getVariableValue 方法,功能为从context中获取value值
6.1 InternalContextAdapterImpl & WebWorkVelocityContext
1 | public Object get(String key) { |
this.context 类为OutputAwareWebWorkVelocityContext
OutputAwareWebWorkVelocityContext extends WebWorkVelocityContext
WebWorkVelocityContext 在Velocity那一便文章中有讲到
会尝试从一下context中获取
- context
- stack.findValue
- chainedContexts
1 | public Object internalGet(String key) { |
6.2 queryString 如何赋值的
root 为PageVariablesAction,有个queryString 属性,此刻就是post参数。
那么是如何赋值的呢?
结论是在SafeParametersInterceptor 中赋值的
最终时通过Ognl.setValue 进行赋值的
ASTProperty.setValueBody 经过一些列的调用,最终调用AbstractCreatePageAction.setQueryString 实现赋值,具体调用栈如下
完整的调用栈如下
1 | setQueryString:381, AbstractCreatePageAction (com.atlassian.confluence.pages.actions) |
7. findValue
getVariableValue 会通过OgnlValueStack.findValue 实现从stack root中获取queryString
1 | findValue:137, OgnlValueStack (com.opensymphony.xwork.util) |
8. setValue & setName
AbstractUITag
9. 二次findValue
但是是如何解析的呢?答案在AbstractUITag#evaluateParams
1 | evaluateParams:378, AbstractUITag (com.opensymphony.webwork.views.jsp.ui) |
3.2 createpage.action 利用链
createpage 与以上类似,只不过需要登录
- content-editor.vm
1 | #tag ("Hidden" "name='queryString'" "value='$!queryString'") |
7.4.10版本,可以看到有几处可以触发
- CreatePageAction
createpage.vm – createpage-form.vm – content-editor.vm
1 | <action name="createpage" class="com.atlassian.confluence.pages.actions.CreatePageEntryAction" method="doDefault"> |
其他与以上都类似,poc如下
0x04 扩展思考
4.1 Confluence 的账号权限逻辑
4.1.1 登录
上文中的doenterpagevariables.action 无需登录,createpage.action需要。Confluence 是如何实现的呢?
这些其实是struts2 的基础架构知识,之前没理过,现在理一遍。
以createpage.action为例
defaultStack
在xwork.xml struts2配置文件中,可以找到createpage action 对应的interceptors 为defaultStack
1 | <action name="createpage" class="com.atlassian.confluence.pages.actions.CreatePageEntryAction" method="doDefault"> |
interceptors
defaultStack 具体包含一下interceptors
1 | 0 = {XWorkProfilingInterceptor@44739} |
- DefaultActionInvocation#invoke
DefaultActionInvocation#invoke 会按顺序调用以上interceptor#invoke,直至有有效的result
- PageAwareInterceptor
当执行至PageAwareInterceptor,会判断是否登录、检验权限
1 | public String intercept(ActionInvocation actionInvocation) throws Exception { |
- pagenotpermitted result
pagenotpermitted result 对应ActionChainResult
1 | <result name="pagenotpermitted" type="chain"> |
- PageNotPermittedAction
ActionChainResult 会执行 pagenotpermitted的action,也即PageNotPermittedAction
1 | <action name="pagenotpermitted" class="com.atlassian.confluence.pages.actions.PageNotPermittedAction"> |
之后就进入了pagenotpermitted action 的流程
- validatingStack
1 | <package name="pages" extends="default" namespace="/pages"> |
此时对应的interceptors 为validatingStack
之后的流程与之前类似,这里不赘述了
- PageNotPermittedAction
执行完interceptors,返回result login
1 |
|
进行redirect
- RedirectResult
RedirectResult 进行跳转响应
4.1.2 权限验证
与登录相似,同样是在PageAwareInterceptor 中进行权限验证。
1 | isPermitted:17, CreatePageEntryAction (com.atlassian.confluence.pages.actions) |
调用CreatePageEntryAction#isPermitted 进行权限验证
后续还涉及到
- DefaultPermissionManager
- DefaultSpacePermissionManager
不做过多赘述
4.2 如何写出有问题的Velocity
其实归根结底,是Struct2 velocity模板二次注入的问题,在Struct2 的历史漏洞中也有出现过,留待以后Struct2 系列再总结吧。
0x05 小结
本文跟踪复现了Confluence CVE-2021-26084 这个Velocity模板二次注入漏洞,发现这是Struct2 的通用问题。包括之前分析过的Confluence CVE-2022-26134 OGNL 这一漏洞,其实也是和Struct2 特性有关。
Struct2 框架虽然不再流行,但其一系列的漏洞及修复、绕过还是很经典,一些历史的产品也可能还存在类似的问题,虽然是个难啃的知识点,但是借Confluence 这两个漏洞的契机,也准备复现分析Struct2 的历史漏洞一番。
其他的思考点
- Confluence的登录/角色 鉴权怎么做的,这块还没分析,有没有可操作空间
- poc有unicode编码,原因在ognl一节已经提到过