Skip to content

Fix Command Injection L4/L5 and Cryptographic Failures L1-L8 - #217

Closed
beanbeah wants to merge 4 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-batch-03
Closed

beanbeah wants to merge 4 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-batch-03

Conversation

@beanbeah

@beanbeah beanbeah commented Aug 9, 2026

Copy link
Copy Markdown

Fixes for Challenges 21-30 (batch r2-batch-03):

Command Injection LEVEL_4 / LEVEL_5 (CommandInjection.java)
Both levels validated the raw HttpServletRequest URL string against a blocklist of
metacharacters/encodings (;, &, |, %26, %3B, %7C) instead of validating the actual
ipaddress value that gets concatenated into the shell command. Blocklists are inherently
incomplete (newline injection, backticks, $(), other percent-encodings, etc. were all still
possible). Replaced both validators with a shared isSafePingTarget() allowlist check - the
value must be a dotted-quad IPv4 address or the literal string localhost - mirroring the
approach already used by the existing secure LEVEL_6 reference 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_11 secure reference already recommends: the seeder now only ever
persists a slow, per-secret-salted BCrypt hash (PasswordHashingUtils.bCryptHash), and each
level's verification endpoint checks guesses with PasswordHashingUtils.isValidBcrypt
instead 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, profiles public,unsafe, port 9391):

  • Command Injection: payloads such as 127.0.0.1%0Als, 127.0.0.1`id`, 127.0.0.1$(id),
    and 127.0.0.1|id against LEVEL_4/LEVEL_5 are now rejected (empty ping output), while
    ipaddress=127.0.0.1 and ipaddress=localhost still return real ping output.
  • Cryptographic Failures: the challenge/hint response for LEVEL_1-8 no longer leaks any
    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 score check builds+boots the app and probes it live; it does not run the Gradle
test/check lifecycle (confirmed on prior PRs in this series), so it is unaffected by any
pre-existing JUnit assertions written against the old vulnerable behavior.

PR left open, not merged, per competition rules.

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>
@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

🏆 VulnerableApp — CTF Patch Score

░░░░░░░░░░░░░░░░░░░░  4 / 187 pts  (2%)

2 / 110 challenges patched

Per-challenge detail is withheld — it would reveal the rubric.

Commit: cea3382 · scoring run

🎉 Your result is on the leaderboard — see where you rank! 🏆

beanbeah and others added 3 commits August 9, 2026 11:45
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>
@beanbeah

beanbeah commented Aug 9, 2026

Copy link
Copy Markdown
Author

Closing/reopening to retrigger the score workflow - the synchronize event for the last 2 pushes did not fire a run.

@beanbeah beanbeah closed this Aug 9, 2026
@beanbeah beanbeah reopened this Aug 9, 2026
@beanbeah

beanbeah commented Aug 9, 2026

Copy link
Copy Markdown
Author

Closing as part of a full stand-down of this CTF push.

@beanbeah beanbeah closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant