cPanel CalDAV 路径穿越与权限降级加固(CVE-2026-29205)
1. 威胁模型与三层缺陷拆解
CVE-2026-29205 由三条防御边界同时失效叠加触发:
- 认证边界缺失:
cpdavd的/calendars/<principal>/<collection>/<attachment-path>托管附件处理逻辑在特定条件下允许预认证请求进入文件读取流程。 - 解码与校验顺序倒置:先用正则校验未解码的原始 URI(
%2F被当成普通字符通过),随后调用URI::Escape::uri_unescape解码为/,拼出../../../etc/shadow实现目录穿越。 - 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;
}防御工作流
- 版本与端口盘点:检查所有 cPanel/WHM 主机版本(
/usr/local/cpanel/version)及2079/2080公网暴露情况。 - 紧急边界止血:在边界防火墙或前置代理拦截
/calendars/下包含%2e/%2f/..的 URI,必要时临时关闭非业务所需的 CalDAV 外部直连。 - 版本升级与凭证轮换:升级至
11.134.0.26+;若历史日志存在可疑命中,立即轮换/etc/shadow相关系统密码、邮件账号密码及 API Token。 - 日志取证与回归验证:检索
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 |