Skip to content

cPanel CalDAV 路径穿越与权限降级加固(CVE-2026-29205) ​

1. 威胁模型与三层缺陷拆解 ​

CVE-2026-29205 由三条防御边界同时失效叠加触发:

  1. 认证边界缺失:cpdavd 的 /calendars/<principal>/<collection>/<attachment-path> 托管附件处理逻辑在特定条件下允许预认证请求进入文件读取流程。
  2. 解码与校验顺序倒置:先用正则校验未解码的原始 URI(%2F 被当成普通字符通过),随后调用 URI::Escape::uri_unescape 解码为 /,拼出 ../../../etc/shadow 实现目录穿越。
  3. RAII 权限降级对象生命周期错误:Perl 代码中 'Cpanel::AccessIds::ReducedPrivileges'->new($system_owner); 未赋值给词法变量,语句结束立即触发 DESTROY,权限提前恢复为 root,导致后续文件读取以高权限执行。

攻击者只需配合 SMTP plus-addressing(user+x-attachment-1-y@domain)触发 Dovecot 创建对应 Maildir 目录,即可利用 2079/2080 端口无认证读取 /etc/shadow 等系统敏感文件。

2. 分层防御与加固矩阵 ​

防线层级控制措施对应缺陷层实施标准
补丁升级升级 cPanel & WHM 至 11.134.0.26 或对应 LTS 修复版全部三层执行 /usr/local/cpanel/scripts/upcp 并确认版本
认证与访问控制强制 CalDAV/CardDAV 接口认证;限制 2079/2080 端口来源 IP第一层(预认证可达)禁止 2079 明文暴露公网,仅对授权客户网段或 VPN 放行
路径规范化解码 + realpath 规范化后再验证前缀边界,拒绝任何 %2f/%2e/..第二层(先校验后解码)在反向代理 WAF 与应用路由同时拦截编码穿越
权限生命周期将 ReducedPrivileges 绑定到作用域词法变量 my $privs,校验操作完成后再释放第三层(过早恢复 root)代码静态规则禁止在 void 上下文调用降权构造函数
日志与邮件侧监测监控 cpdavd 访问日志中的 %2f / attachment- 异常请求与批量 plus-address 投递利用链前置条件SIEM 对 /calendars/.*%2[fF] 与 /etc/shadow 探测规则告警

安全路径校验与 Perl RAII 加固模式 ​

perl
# 1. 必须先彻底 URL 解码并规范化真实路径,再校验目录归属
my $decoded = URI::Escape::uri_unescape($attachment_full_path);
die "Forbidden traversal" if $decoded =~ m{(?:^|/)\.\.(?:/|$)|%2[ef]|\\}i;

# 2. 必须将权限降级对象绑定到词法变量,覆盖整个文件读取作用域
{
    my $privs = Cpanel::AccessIds::ReducedPrivileges->new($system_owner);
    my $real  = Cwd::abs_path($candidate_path) // die "Invalid path";
    die "Out of sandbox" unless index($real, $expected_maildir_root . "/") == 0;
    # 在降权作用域内完成安全打开与读取
}

网络边界与 Nginx/CSF 临时阻断规则 ​

在无法立即停机升级的窗口期,可通过前置代理拦截针对 cpdavd 的异常编码路径并收口端口:

nginx
# 前置反向代理拦截未规范化穿越序列(覆盖 2080 HTTPS 入口)
location ~* ^/calendars/ {
    if ($request_uri ~* "(%2e|%2f|\.\./|\.\.\\)") {
        return 403;
    }
    proxy_pass https://127.0.0.1:2080;
}

防御工作流 ​

  1. 版本与端口盘点:检查所有 cPanel/WHM 主机版本(/usr/local/cpanel/version)及 2079/2080 公网暴露情况。
  2. 紧急边界止血:在边界防火墙或前置代理拦截 /calendars/ 下包含 %2e/%2f/.. 的 URI,必要时临时关闭非业务所需的 CalDAV 外部直连。
  3. 版本升级与凭证轮换:升级至 11.134.0.26+;若历史日志存在可疑命中,立即轮换 /etc/shadow 相关系统密码、邮件账号密码及 API Token。
  4. 日志取证与回归验证:检索 cpdavd 与 exim/dovecot 日志中的 +x-attachment- 异常目录创建记录,并通过路径规范化检查脚本验证拦截逻辑生效。

3. Python URL 规范化边界与日志异常检测脚本 ​

python
#!/usr/bin/env python3
"""CVE-2026-29205 路径规范化边界校验与 cpdavd 访问日志异常检测脚本。"""

from pathlib import Path
from urllib.parse import unquote
import posixpath
import re

SUSPICIOUS_LOG_RE = re.compile(
    r"(?:GET|PROPFIND)\s+/calendars/[^\s]*(?:%2f|%2e|\.\.)[^\s]*",
    re.IGNORECASE,
)


def validate_caldav_attachment_path(raw_uri_path: str, allowed_root: str) -> tuple[bool, str]:
    """先循环解码直至稳定,再做规范化与根目录前缀 containment 校验。"""
    current = raw_uri_path
    for _ in range(3):
        decoded = unquote(current)
        if decoded == current:
            break
        current = decoded
    if "%" in current or "\\" in current or "\x00" in current:
        return False, "rejected: residual encoding, backslash, or null byte"

    normalized = posixpath.normpath(current)
    root = posixpath.normpath(allowed_root).rstrip("/") + "/"
    if not normalized.startswith(root):
        return False, f"rejected: escaped root ({normalized})"
    if "/.." in normalized or normalized.endswith("/.."):
        return False, "rejected: traversal segment"
    return True, normalized


def scan_access_log_lines(lines: list[str]) -> list[str]:
    return [line.strip() for line in lines if SUSPICIOUS_LOG_RE.search(line)]


if __name__ == "__main__":
    base = "/var/cpanel/calendars/user1/calendar"
    attack_uri = "/var/cpanel/calendars/user1/calendar/..%2F..%2F..%2Fetc%2Fshadow"
    valid_uri = "/var/cpanel/calendars/user1/calendar/attachments/event-1.ics"

    ok1, res1 = validate_caldav_attachment_path(attack_uri, base)
    ok2, res2 = validate_caldav_attachment_path(valid_uri, base)
    print("attack_uri:", ok1, res1)
    print("valid_uri:", ok2, res2)
    assert not ok1 and "escaped root" in res1
    assert ok2

    hits = scan_access_log_lines([
        '203.0.113.9 - - "GET /calendars/admin/calendar/..%2F..%2Fetc%2Fshadow HTTP/1.1" 403 0',
        '198.51.100.4 - user1 "GET /calendars/user1/calendar/inbox.ics HTTP/1.1" 200 512',
    ])
    assert len(hits) == 1

验证与判定标准 ​

检查项判定为已加固(PASS)判定为存在风险(FAIL)验证方式
cPanel 版本版本号 >= 11.134.0.26(或官方公告安全版本)低于 11.134.0.26/usr/local/cpanel/version 核查
未认证穿越请求对 /calendars/.../..%2F... 返回 401/403/404 且不读取宿主机文件返回 200 且响应含系统文件内容使用 http_probe 对测试环境发合规负向样例
2079/2080 暴露面禁用 2079 明文端口或仅限白名单网段,强制 TLS 与认证公网任意 IP 可无认证访问 cpdavd防火墙规则与端口扫描复核
邮件异常投递监测存在针对 plus-address 批量投递与 %2F CalDAV 访问的告警规则无日志留存或无 %2F 穿越特征告警核查 SIEM 与 cpdavd 日志规则

MCP 工具映射 ​

防御步骤MCP 工具说明
检索 CalDAV 加固策略kb_router按 CVE-2026-29205 / path traversal 搜索防御条目
读取攻击链详情kb_read_file读取 ctf-website/09-cve/03-cpanel-caldav-traversal 对照三层成因
端点回归探测http_probe验证反向代理与 cpdavd 2080 端口对编码穿越请求已返回 401/403

Apache-2.0 · 仅供授权环境下的学习与防御性研究使用 · 免责声明