Conversation
🏆 VulnerableApp — CTF Patch Score12 / 110 challenges patched
Commit: 🎉 Your result is on the leaderboard — see where you rank! 🏆 |
added 7 commits
August 9, 2026 13:02
… guess ceiling Every level in this module stored its secret with a weak, reversible or fast-hashing scheme (plaintext, Base64, a Caesar cipher, a custom obfuscation, MD4/MD5/SHA1/LM hashing, unsalted SHA-256, and AES-128/ECB under a password-derived key) and then handed the stored value straight back in the challenge response, so reading the response and reversing the same transform recovered a working credential (CWE-326/CWE-327/CWE-330). Levels 1-9 now store a salted, adaptive BCrypt digest instead and never disclose it; levels 2-4 publish a fixed, unrelated decoy in their old encoding so the exercise still has something to decode without it doubling as credential material. Level 10, the only entry with a genuine two-way requirement, moved from AES-128/ECB with a password-derived key to AES-256/GCM under a random key held only in server memory, via a small dedicated cipher component instead of the previous shared-key path, and its guess is compared in constant time to avoid a timing side channel (CWE-208). The vault's standing read-only database account and the credentials advertised for it in the challenge text (CWE-798) are removed, and guesses are submitted as a request parameter rather than echoed through the URL. Removing the disclosure leaves online guessing against the verification endpoint itself as the only path forward, and that endpoint answered an unbounded number of guesses at whatever rate a caller could send them (CWE-307). Each level now tracks wrong guesses per caller in a short rolling window; once a caller exceeds the ceiling for a level, further attempts are refused outright with 429 rather than silently evaluated forever, and the count resets on that caller's next correct guess so it never locks a level out for anyone else.
User-supplied values were concatenated directly into LDAP search filters without escaping, so filter metacharacters (\, *, (, ), NUL) let a caller widen a search, force an always-true condition, or otherwise change what the filter matched (CWE-90). Every level now runs the submitted value through RFC 4515 filter-encoding (or builds the filter via the LDAP SDK's own equality-filter constructor) before it reaches the directory, and responses no longer echo the constructed filter or the matched directory entries back to the caller, which was itself a debugging oracle into the directory even once the injection itself was closed. The authentication levels had a second, independent gap: a syntactically valid, unwidened lookup that matched a real directory entry was treated as a successful login on its own, with no check that the caller had also supplied that account's password. Login now requires exactly one matched entry and a verified password against it. The one level with no credential check at all first validates that the submitted username is directory-uid-shaped before it is ever used to build a filter, so malformed input is rejected before it reaches filter construction rather than relying on escaping alone.
The levels 2-4 decoy text (published so the exercise still has something to decode once the real stored value stopped being disclosed) was a fixed compile-time constant. A fixed decoy is itself a stable, guessable value baked into the deployed artifact rather than something drawn independently of the running instance. It is now generated from a CSPRNG once at class load, so each running instance publishes its own unrelated decoy text instead of the same literal every time.
Hardening cryptographic-failures storage to BCrypt/AES-GCM left the original weak primitives (a Caesar cipher, a reverse-and-encode "custom" cipher, and MD4/MD5/SHA-1/LM/unsalted-SHA-256 digest routines) defined but unreferenced: nothing in the application called them any more, but the broken implementations themselves - the exact routines that used to protect these credentials - were still compiled into the artifact and callable by anything that imported them. The levels 2-4 decoy text was also still being produced by running a value through those same retired routines, so even though the routines were no longer applied to the real secret, the vulnerable transform itself was still live and exercised on every request. The decoy is now an unstructured opaque string with no cipher applied to it at all, and the routines that used to produce it (EncryptionUtils' Caesar and custom ciphers, and the weak-digest helpers in PasswordHashingUtils) have been deleted along with their tests rather than merely left unused.
Level 10 replaced its broken AES-128/ECB, password-derived-key encryption with AES-256/GCM under a genuine random key, which fixed the immediate weak-cipher/weak-key issues (CWE-327/CWE-330) but kept the entry fundamentally two-way: the server retained the ability to recover the original password, and a compromise of the key (or of the server process memory holding it) still exposes the plaintext credential. A stored password only ever needs to be recognised again on a later attempt, never recovered, so keeping it reversible - even under a strong, correctly-implemented cipher - solves the wrong problem. This entry is now hashed with the same salted, adaptive BCrypt scheme used everywhere else in this vault, removing the two-way relationship entirely instead of strengthening it.
Every response from this endpoint is part of a password-verification exchange - the current challenge text, or whether a submitted guess was correct - and carried no cache directives at all, leaving it to whatever caching heuristics sit between the client and the server. A cached "correct" response replayed to a different caller, or a cached challenge response later readable from a shared machine's disk cache, both leak more than an uncached response would. Every verification response now sets Cache-Control: no-store, no-cache, must-revalidate and Pragma: no-cache.
The challenge description and result text returned by this module's endpoints were written into the page with innerHTML, which parses its argument as HTML rather than display text. Several of these responses include content that is not a fixed literal (a per-level decoy value, level commentary), so anything reaching the page through this path that contained markup would be parsed and rendered as markup instead of shown as text. Both call sites now build the same visual result (a bold label, optionally prefixed with "Result:") out of real text nodes, so the server text can only ever display as text.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Baseline probe branch; crypto hardening to follow in subsequent commits.