FlawOpen

flawopen.com/Teardowns/cve-2026-21589-atlassian-arbitrary-file-access

● CVE-2026-21589 · CVSS v4 9.3 · 严重
安全研究 · FlawOpen

CVE-2026-21589:Atlassian Data Center 文件访问漏洞

Atlassian 公告和 10月12日的 MS-ISAC 报告说明了风险和紧迫性,但不能证明某个客户实例遭到利用或已确认被入侵。[1], [4], [5], [10]

💡 通俗易懂的原理解析 (ELI5)

想象图书馆允许访客报出书架位置。访客不能浏览所有书架,但如果他已知道某本书的确切位置,边界检查缺陷可能让工作人员从阅览区以外取书。修复要确保解析后的路径始终位于允许范围,并安装对应产品的更新。[1], [4]

核心概念与专有名词

任意文件访问
请求可访问超出预期范围的文件。Atlassian 描述的是 Web 应用根目录中的特定文件,而非整个文件系统。[1], [4]
路径穿越
路径组件越过预期目录边界。Atlassian 的缓解规则针对路径分隔符附近的穿越标记及其编码形式。[1], [4]
Data Center
Atlassian 的自托管部署模式。公告列出八款受影响产品;Atlassian 表示 Cloud 产品已修复。[1]
已知路径前提
攻击者必须预先知道目标文件的确切名称和路径;公告指出该漏洞不能枚举或列出目录。[1]
CVSS 4.0
Atlassian 根据自身评估将其定为严重,CVSS 4.0 评分为 9.3。[1]
Router.unescapeSlashes()
共享库在处理资源前会将编码分隔符 :: 还原为 /;watchTowr 的源码分析将此解码边界作为关键点。[1], [4]
ResourceServingHelpers
该辅助函数解析资源名称,并将剩余相对路径传给 createResourceWithRelativePath()。[1], [4]
Crowd 凭据链
有条件的后续阶段:可读取的 Crowd 属性文件可能暴露应用密码;能否使用取决于网络可达性和授权配置。[4], [5]
CWE-22
FlawOpen 对路径遍历弱点的编辑分类:路径未被充分限制在预期目录内。Atlassian 的公告未指定 CWE。[6]

框架映射

  • FlawOpen 对 MITRE CWE 的概念性映射;没有厂商或 CVE 记录将此弱点编号分配给本研究演示。 CWE-22

根本原因剖析 (Root Cause)

watchTowr 对共享库 atlassian-plugins-webresource 的分析指出了路径解码边界。Router.unescapeSlashes() 会将 :: 转换为 /;资源路由在调用资源处理程序前,会对请求可控的路径组件应用该函数。后续查找拆分资源名称,并将剩余路径传给 createResourceWithRelativePath();替代分隔符表示可绕过路径穿越检查的假设。研究人员在 Jira、Confluence 和 Bitbucket 中演示了读取 Tomcat 应用上下文内文件;Atlassian 列出八款受影响产品。报告称其测试路径未越出 Tomcat 上下文。在有条件的攻击链中,可读取的 WEB-INF/classes/crowd.properties 可能暴露明文 Crowd 应用密码;影响取决于具体配置及 Crowd 是否可达。BleepingComputer 报道称,Previdian 传感器在技术分析和 PoC 发布数小时后观察到利用尝试。这是针对蜜罐的尝试证据,并不证明客户系统访问成功。下方脆弱 Java 片段来自 watchTowr 报告。安全示例是通用路径约束模式,并非 Atlassian 未完整公开的补丁源码;请安装各产品官方修复版本。[1], [4], [5]

攻击执行流程分解

步骤 1

共享解码器转换路径

Atlassian Web 资源路由器会在提供资源前将替代分隔符 :: 转为 /。这是正常资源路由的一部分。[1], [4]

步骤 2

资源查找接收解码后的路径

辅助函数拆分路径组件,并将剩余相对路径传给资源工厂。替代分隔符可能绕过路径穿越检查所依赖的假设。[4]

步骤 3

应用上下文内的文件可被读取

watchTowr 在 Jira、Confluence 和 Bitbucket 中演示了这一点。报告称测试路径未越出 Tomcat 上下文;不能据此推断可读取主机上的任意文件。[1], [4]

步骤 4

身份凭据链取决于配置

如果可读取的 Crowd 配置含应用密码,且 Crowd 网络可达并接受该凭据,攻击者可能进一步访问身份管理功能。网络白名单和部署设置会影响结果。[4], [5]

步骤 5

观察到的尝试提高了紧迫性,但不证明入侵成功

10月12日,互联网安全中心(CIS)的 MS-ISAC 公告称,有报告指出 CVE-2026-21589 已在现实环境中遭到利用。公告未指明受害者,也未公布事件证据,因此应将其表述为 MS-ISAC 报告的利用情况,而不是对某个已知客户遭访问的确认。Atlassian 10月5日的公告称,其调查未发现 Cloud 产品遭到利用的证据,并表示这些产品已修复。该结论仅涉及 Cloud,与 MS-ISAC 关于此漏洞遭到利用的报告并不矛盾;两项声明都不能证明某个具体实例发生了什么。[1], [10]

源代码对比:漏洞与安全实现

易受攻击的代码片段展示了 watchTowr 公布的受影响 web-resource 库中的路径转换;它不是利用请求。下方示例展示 Java 中的一般防御方法,并非 Atlassian 补丁:从可信根目录出发,通过 SecureDirectoryStream 句柄逐级遍历,拒绝每个路径组件上的符号链接,并限制读取字节数。调用方必须传入只解码一次的相对路径,以及以可信目录为根打开的 SecureDirectoryStream。部分文件系统提供程序可能不支持 SecureDirectoryStream;此时应安全地拒绝操作。请安装厂商针对具体产品提供的修复版本。此示例要求资源目录树可信,其目录和文件不能被不可信主体修改,并且只包含普通文件;示例本身不会检查文件类型。字节上限和有限的无进展读取处理可以限制读取,但这并不是通用的文件服务实现。调用方必须传入只解码一次的相对路径,并保持可信根目录流处于打开状态。文件系统提供程序可以不支持 SecureDirectoryStream;如果不可用,应安全地拒绝操作。[4], [7]

存在漏洞的实现
public static String unescapeSlashes(String string) {
    return string.replaceAll("::", "/");
}
加固后的安全修复
static byte[] readUnder(SecureDirectoryStream<Path> root, Path relative, int maxBytes) throws IOException {
    if (relative.isAbsolute() || relative.getNameCount() == 0 || maxBytes < 0) {
        throw new SecurityException("Expected a relative path and non-negative limit");
    }
    List<SecureDirectoryStream<Path>> opened = new ArrayList<>();
    SecureDirectoryStream<Path> parent = root;
    try {
        for (int i = 0; i < relative.getNameCount() - 1; i++) {
            Path part = relative.getName(i);
            if (part.toString().isEmpty() || part.toString().equals(".") || part.toString().equals("..")) {
                throw new SecurityException("Invalid path component");
            }
            parent = parent.newDirectoryStream(part, LinkOption.NOFOLLOW_LINKS);
            opened.add(parent);
        }
        Path leaf = relative.getFileName();
        if (leaf.toString().isEmpty() || leaf.toString().equals(".") || leaf.toString().equals("..")) {
            throw new SecurityException("Invalid file name");
        }
        try (SeekableByteChannel channel = parent.newByteChannel(
                leaf, Set.of(StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS))) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            int total = 0;
            int zeroReads = 0;
            while (true) {
                int count = channel.read(buffer);
                if (count == -1) break;
                if (count == 0) {
                    if (++zeroReads >= 3) throw new IOException("Resource read made no progress");
                    continue;
                }
                zeroReads = 0;
                if (count > maxBytes - total) throw new IOException("Resource exceeds size limit");
                out.write(buffer.array(), 0, count);
                total += count;
                buffer.clear();
            }
            return out.toByteArray();
        }
    } finally {
        for (int i = opened.size() - 1; i >= 0; i--) opened.get(i).close();
    }
}

工程与系统安全加固清单

参考来源