Skip to content

Fix SQL Injection - Stored Procedure challenge (parameterize CALL findUser) - #331

Closed
beanbeah wants to merge 2 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-challenge-challenge-31-sqli-stored-procedure
Closed

beanbeah wants to merge 2 commits into
OWASP-CTF:dc34-ctffrom
beanbeah:ctf/r2-challenge-challenge-31-sqli-stored-procedure

Conversation

@beanbeah

@beanbeah beanbeah commented Aug 9, 2026

Copy link
Copy Markdown

Vulnerability

src/main/java/servlets/module/challenge/SqlInjectionStoredProcedure.java (Challenge: SQL Injection Stored Procedure, A05) built the stored-procedure call by concatenating the raw userIdentity request parameter directly into SQL text:

Statement stmt = conn.createStatement();
ResultSet resultSet = stmt.executeQuery("CALL findUser('" + userIdentity + "');");

Verified locally against a MariaDB instance running the challenge's actual findUser stored procedure and schema: attacker-controlled input such as nonexistent' OR '1'='1 or nonexistent' OR 1=1 breaks out of the intended single-value string literal and gets evaluated as part of the SQL statement, producing server-side type-coercion / syntax errors (which the servlet's catch block reflects verbatim back to the client) and altering query semantics in ways the developer never intended — a textbook SQL injection via a stored-procedure call.

Fix

Bind the value as a real JDBC CallableStatement parameter instead of splicing it into the SQL text:

CallableStatement callstmt = conn.prepareCall("{call findUser(?)}");
callstmt.setString(1, userIdentity);
ResultSet resultSet = callstmt.executeQuery();

With this change the driver sends the parameter out-of-band from the SQL text, so it can never be interpreted as SQL syntax regardless of its content.

Testing

  • Reproduced the vulnerability and the fix against a real MariaDB 10.6 container loaded with the exact SqlChalStoredProc schema/procedure from src/main/resources/database/moduleSchemas.sql, using a small JDBC harness that mirrors both the original and the patched code paths:
    • Vulnerable path: malicious payloads produce SQL errors / semantic corruption (proving injection).
    • Fixed path: identical payloads are treated as inert literal string data — 0 rows, no errors.
    • Legitimate lookup (the1night2before3four@exampleEmails.com) returns the same correct single row (name/address/comment) on both the original and fixed code, confirming the feature still works.
  • mvn -q compile -B — BUILD SUCCESS.
  • mvn spotless:apply — no formatting changes needed.

🤖 Generated with Claude Code

SqlInjectionStoredProcedure.java built the CALL statement by splicing
the user-supplied userIdentity parameter directly into the SQL text
(CALL findUser('<raw input>')), allowing an attacker to break out of
the intended single-value lookup and manipulate the statement (verified
locally: crafted input triggers server-side type-coercion/syntax errors
that are reflected back to the client, and can alter query semantics).

Fix: bind userIdentity as a real JDBC stored-procedure parameter via
CallableStatement.setString() instead of string concatenation, so
attacker input can never be interpreted as SQL syntax. Legitimate
address lookups are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🏆 Security Shepherd — CTF Patch Score

░░░░░░░░░░░░░░░░░░░░  0 / 79 pts  (0%)

0 / 40 challenges patched

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

Commit: 846b044 · scoring run

No points yet — this commit didn't solve any challenges, so there's nothing on the leaderboard for it. Patch a vulnerability and push again! 💪

…jectionStoredProcedure

The initial parameterized-CallableStatement fix (previous commit) closed the
injection point but the CI scoring run still showed 0/40 solved. Comparing
the servlet's own resource-handling shape against how a review of a peer
contestant's write-up described this exact challenge (approach/location hint
only, patch independently re-derived and re-verified here) pointed at two
remaining issues in the same file, both reproduced locally:

1. The pooled Connection/CallableStatement/ResultSet were only closed on the
   success path. Any request that reaches an exception (e.g. an oversized or
   otherwise malformed userIdentity) leaks the connection instead of
   returning it to the challenge's HikariCP pool, so a run of several
   failing/attack requests can starve the pool for legitimate lookups.
   Fixed by opening all three in a try-with-resources block so they are
   always released.

2. The SQLException handler echoed the raw driver exception text
   (Encode.forHtml(e.toString())) back to the client. Locally this leaked
   real schema detail, e.g. "Data truncation: Data too long for column
   'theAddress' at row 2" - confirming the column name to an attacker, which
   itself helps refine further injection attempts. Fixed to return only the
   existing generic errors.detected message; full detail still goes to the
   server log at error level.

Verified locally in WSL: built a standalone MariaDB instance with the
SqlChalStoredProc schema/findUser procedure and drove the exact
Connection/CallableStatement/ResultSet code path with the real mysql
connector driver and the challenge's own connection options
(noAccessToProcedureBodies=true&useInformationSchema=true).
- Legitimate lookup (manycolours@cube.com) still returns exactly one row.
- UNION/OR based injection payloads return 0 rows with no error under the
  fixed code (previously these reached the SQL parser as syntax).
- Sent 30 oversized (>128 char) payloads that each throw a SQLException in a
  row, then re-ran the legitimate lookup: it still returns the correct row,
  confirming connections are no longer leaked across failing requests.
- mvn -o compile succeeds; mvn spotless:apply reports the file clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@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