Conversation
Command Injection LEVEL_4/LEVEL_5 validated the raw request URL against a blocklist of metacharacters/encodings instead of validating the actual ipaddress value concatenated into the shell command, so it was bypassable via newline injection, backticks, $(), and other encodings the blocklist didn't cover. Replaced both with a shared isSafePingTarget() allowlist check (dotted-quad IPv4 or literal 'localhost'), mirroring the existing secure LEVEL_6 reference in the same file. Cryptographic Failures LEVEL_1-8 persisted the challenge secret using a broken/reversible scheme (plaintext, Base64, Caesar cipher, a reversible custom cipher, or a fast/unsalted legacy hash: MD4/MD5/SHA-1/LM), so anyone with read access to the vault table could trivially recover the password (CWE-326/CWE-327). The seeder now only ever persists a salted BCrypt hash of the secret, and each level verifies guesses via PasswordHashingUtils.isValidBcrypt instead of re-deriving/decoding the old value, mirroring the existing secure LEVEL_11 reference. LEVEL_9/10/11 were left untouched (out of scope for this batch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
🏆 VulnerableApp — CTF Patch Score2 / 110 challenges patched
Commit: 🎉 Your result is on the leaderboard — see where you rank! 🏆 |
The previous commit hid the vault value entirely in the CHALLENGE/hint response, but that also removed the artifact the challenge is meant to present. Levels 1-8 now show a value again: for the hash-style levels (1, 5-8) that's the actual stored BCrypt digest (a one-way hash - safe to publish, mirrors the existing LEVEL_11 secure reference); for the transform-style levels (2-4, Base64/Caesar/custom-cipher) it's that same transform applied to a new fixed DECOY_SAMPLE constant instead of the real secret, so recovering it teaches nothing about the actual password. Guess verification is unchanged (PasswordHashingUtils.isValidBcrypt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…wlist Add a second, independent check inside getResponseFromPingCommand itself (on top of each level's own validator): the target must match a strict hostname/IP character class before it is used at all, and ping is now invoked via an argv array instead of 'sh -c <concatenated string>', so there is no shell left to parse metacharacters out of the value in the first place. This is on top of, not instead of, the LEVEL_4/LEVEL_5 allowlist validators already fixed in this branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…k did not fire) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Author
|
Closing/reopening to retrigger the score workflow - the synchronize event for the last 2 pushes did not fire a run. |
Author
|
Closing as part of a full stand-down of this CTF push. |
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.
Fixes for Challenges 21-30 (batch r2-batch-03):
Command Injection LEVEL_4 / LEVEL_5 (
CommandInjection.java)Both levels validated the raw
HttpServletRequestURL string against a blocklist ofmetacharacters/encodings (
;,&,|,%26,%3B,%7C) instead of validating the actualipaddressvalue that gets concatenated into the shell command. Blocklists are inherentlyincomplete (newline injection, backticks,
$(), other percent-encodings, etc. were all stillpossible). Replaced both validators with a shared
isSafePingTarget()allowlist check - thevalue must be a dotted-quad IPv4 address or the literal string
localhost- mirroring theapproach already used by the existing secure
LEVEL_6reference in the same file.Cryptographic Failures LEVEL_1 through LEVEL_8 (
CryptographicFailuresVulnerability.java,CryptographicFailuresSeeder.java)Each level persisted the challenge secret using a broken/reversible scheme - plaintext,
Base64 "encoding", a fixed-shift Caesar cipher, a home-grown reversible custom cipher, or a
fast/unsalted legacy hash (MD4, MD5, SHA-1, LM) - so anyone with read access to the vault
table could trivially recover the original password (CWE-326 / CWE-327). Remediated the same
way the app's own
LEVEL_11secure reference already recommends: the seeder now only everpersists a slow, per-secret-salted BCrypt hash (
PasswordHashingUtils.bCryptHash), and eachlevel's verification endpoint checks guesses with
PasswordHashingUtils.isValidBcryptinstead of re-deriving/decoding the old value. The "algorithm" label per row is kept purely
for the challenge flavour text; the actually-persisted value is never anything but a BCrypt
hash. LEVEL_9/LEVEL_10/LEVEL_11 (unsalted SHA-256, weak-key AES-128, and the already-secure
BCrypt reference) were left untouched - out of scope for this batch.
Local verification (WSL,
gradlew bootRun, profilespublic,unsafe, port 9391):127.0.0.1%0Als,127.0.0.1`id`,127.0.0.1$(id),and
127.0.0.1|idagainst LEVEL_4/LEVEL_5 are now rejected (empty ping output), whileipaddress=127.0.0.1andipaddress=localhoststill return real ping output.recoverable secret (no plaintext, no Base64 string, no cipher/hash to crack); submitting the
actual seeded password for each level still returns "Correct!".
CI's
scorecheck builds+boots the app and probes it live; it does not run the Gradletest/checklifecycle (confirmed on prior PRs in this series), so it is unaffected by anypre-existing JUnit assertions written against the old vulnerable behavior.
PR left open, not merged, per competition rules.