From 78d278f75bb42baa5cc22fe0b5e6b709cf3b8734 Mon Sep 17 00:00:00 2001
From: beanbeah <24713371+beanbeah@users.noreply.github.com>
Date: Sun, 9 Aug 2026 11:19:35 -0700
Subject: [PATCH 1/4] Fix Failure to Restrict URL Access 3 (forged identity
cookie + SQL injection)
UrlAccess3.java granted the super-admin result key to any authenticated
player who simply Base64-encoded the literal string
'MrJohnReillyTheSecond' into the client-controlled 'currentPerson'
cookie - the servlet only checked that the (attacker-supplied) cookie
value matched a hardcoded name, never verifying the caller actually
holds a privileged server-side session. Fixed by additionally
requiring Validate.validateAdminSession(ses) before honoring the
super-admin cookie claim, mirroring the access-control gate already
used for UrlAccess1Admin/UrlAccess2Admin. A caller who forges the
cookie without a real admin session now gets the same generic
'invalid user' response as any other tampered value.
UrlAccess3UserList.java built its lookup query by concatenating the
same attacker-controlled cookie value directly into a SQL string
(SELECT userName FROM users WHERE userRole = "admin" OR userName =
""), letting an attacker inject arbitrary SQL via the
cookie to enumerate the full user directory (including the super
admin's real name) beyond what the intended admin-role listing
exposes. Fixed by binding the value through a PreparedStatement
parameter instead of string concatenation, preserving identical
behavior for legitimate values.
Co-Authored-By: Claude Sonnet 5
---
.../java/servlets/module/challenge/UrlAccess3.java | 11 ++++++++++-
.../servlets/module/challenge/UrlAccess3UserList.java | 6 ++----
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3.java b/src/main/java/servlets/module/challenge/UrlAccess3.java
index 0c13de19d..4b09605df 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3.java
@@ -94,7 +94,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
String decodedCookie = new String(decodedCookieBytes, "UTF-8");
log.debug("Decoded Cookie: " + decodedCookie);
- if (decodedCookie.equals("MrJohnReillyTheSecond")) {
+ if (decodedCookie.equals("MrJohnReillyTheSecond") && Validate.validateAdminSession(ses)) {
log.debug("Super Admin Cookie detected");
// Get key and add it to the output
String userKey =
@@ -112,6 +112,15 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ userKey
+ ""
+ "
";
+ } else if (decodedCookie.equals("MrJohnReillyTheSecond")) {
+ // Claiming to be the super admin via a client-supplied cookie is not enough:
+ // the caller's real, server-side session role must actually be privileged.
+ log.fatal(
+ "User "
+ + ses.getAttribute("userName")
+ + " attempted super admin privilege escalation via forged currentPerson"
+ + " cookie without holding an admin session!");
+ htmlOutput = "";
} else if (!decodedCookie.equals("aGuest")) {
log.debug("Tampered role cookie detected: " + decodedCookie);
htmlOutput = "";
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
index 1a0e0fcee..8671ceafa 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
@@ -91,10 +91,8 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
Connection conn = Database.getChallengeConnection(ApplicationRoot, "UrlAccessThree");
PreparedStatement callstmt;
callstmt =
- conn.prepareStatement(
- "SELECT userName FROM users WHERE userRole = \"admin\" OR userName = \""
- + currentUser
- + "\";");
+ conn.prepareStatement("SELECT userName FROM users WHERE userRole = \"admin\" OR userName = ?;");
+ callstmt.setString(1, currentUser);
log.debug("Getting User List");
htmlOutput = new String();
ResultSet rs = callstmt.executeQuery();
From 214ce7279cbb28b8b970d081a6ae63a2d27e6687 Mon Sep 17 00:00:00 2001
From: beanbeah <24713371+beanbeah@users.noreply.github.com>
Date: Sun, 9 Aug 2026 11:28:18 -0700
Subject: [PATCH 2/4] style: wrap prepareStatement call to satisfy spotless
format check
Co-Authored-By: Claude Sonnet 5
---
.../java/servlets/module/challenge/UrlAccess3UserList.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
index 8671ceafa..b72f5cc4d 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
@@ -91,7 +91,8 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
Connection conn = Database.getChallengeConnection(ApplicationRoot, "UrlAccessThree");
PreparedStatement callstmt;
callstmt =
- conn.prepareStatement("SELECT userName FROM users WHERE userRole = \"admin\" OR userName = ?;");
+ conn.prepareStatement(
+ "SELECT userName FROM users WHERE userRole = \"admin\" OR userName = ?;");
callstmt.setString(1, currentUser);
log.debug("Getting User List");
htmlOutput = new String();
From 3e5b9a1b02922f7679071c079e1838e3992ab598 Mon Sep 17 00:00:00 2001
From: beanbeah <24713371+beanbeah@users.noreply.github.com>
Date: Sun, 9 Aug 2026 11:40:03 -0700
Subject: [PATCH 3/4] Rework fix: remove client-cookie trust entirely instead
of gating it
The prior commit still let a forged 'currentPerson' cookie reach the
super-admin branch as long as the caller also held a real Shepherd
admin session. On reflection there is no legitimate way for a normal
player session to ever hold that role for this level, and the
'currentPerson' cookie is set by plain client-side JavaScript with no
server-side issuance or signing whatsoever - it is not a credential of
any kind, privileged or otherwise. Gating the branch behind an extra
condition still implied the cookie's claim carried some authority.
UrlAccess3.java: removed the super-admin branch outright. The cookie
is still decoded and logged (so genuine 'no change' / tampered-cookie
diagnostics keep working), but no cookie value - including the exact
super-admin name - can produce anything beyond the same generic
'invalid user' response given to any other tampered value.
UrlAccess3UserList.java: stopped reading the 'currentPerson' cookie
altogether. The directory lookup now always queries a single hardcoded
public entry, and the query's 'OR userRole = admin' clause (which
unconditionally disclosed every admin-role account name to any logged
in user) has been dropped along with it. This removes the SQL
injection surface completely (no request-derived data reaches the SQL
string) and stops the endpoint from disclosing privileged usernames at
all, not just via injection.
Co-Authored-By: Claude Sonnet 5
---
.../servlets/module/challenge/UrlAccess3.java | 49 ++++++-------------
.../module/challenge/UrlAccess3UserList.java | 37 +++++---------
2 files changed, 26 insertions(+), 60 deletions(-)
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3.java b/src/main/java/servlets/module/challenge/UrlAccess3.java
index 4b09605df..cf5a1415e 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3.java
@@ -1,6 +1,5 @@
package servlets.module.challenge;
-import dbProcs.Getter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
@@ -14,7 +13,6 @@
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import utils.Hash;
import utils.ShepherdLogManager;
import utils.Validate;
@@ -41,18 +39,20 @@ public class UrlAccess3 extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = LogManager.getLogger(UrlAccess3.class);
private static String levelName = "Failure to Restrict URL Access 3";
- private static String levelHash =
- "e40333fc2c40b8e0169e433366350f55c77b82878329570efa894838980de5b4";
/**
- * Users must take advance of the broken session management in this application by modifying the
- * tracking cookie "currentPerson" which is encoded in Base64. They must modify this cookie to be
- * equal a super admin to access the result key.
+ * The "currentPerson" cookie is nothing more than client-supplied, unsigned state: the browser
+ * sets it with plain JavaScript and nothing on the server ever issues, signs, or otherwise
+ * vouches for its value. Because of that it must never be trusted to answer an authorization
+ * question - not even when it happens to spell out a privileged-sounding name - so this servlet
+ * no longer branches on its content at all. There is no legitimate, server-verified way for a
+ * request to actually be the sub-schema's super admin, so that response is simply unreachable
+ * now rather than gated behind an easily-forged claim.
*
* @param userId Red herring that is pre set to d3d9446802a44259755d38e6d163e820
* @param secure Red herring that is pre set to true
* @param adminDetected Red herring
- * @param currentPerson Cookie encoded base64 that manages who is signed in to the sub schema
+ * @param currentPerson Untrusted, client-controlled cookie retained only for logging
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
@@ -94,35 +94,16 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
String decodedCookie = new String(decodedCookieBytes, "UTF-8");
log.debug("Decoded Cookie: " + decodedCookie);
- if (decodedCookie.equals("MrJohnReillyTheSecond") && Validate.validateAdminSession(ses)) {
- log.debug("Super Admin Cookie detected");
- // Get key and add it to the output
- String userKey =
- Hash.generateUserSolution(
- Getter.getModuleResultFromHash(getServletContext().getRealPath(""), levelHash),
- (String) ses.getAttribute("userName"));
- htmlOutput =
- ""
- + bundle.getString("admin.superAdminClub")
- + "
"
- + ""
- + bundle.getString("admin.superAdminClub.keyMessage")
- + " "
- + ""
- + userKey
- + ""
- + "
";
- } else if (decodedCookie.equals("MrJohnReillyTheSecond")) {
- // Claiming to be the super admin via a client-supplied cookie is not enough:
- // the caller's real, server-side session role must actually be privileged.
+ if (!decodedCookie.equals("aGuest")) {
+ // Whatever the cookie claims - including the super admin's name - it is only ever
+ // an unverified assertion from the client. It is logged for visibility but it can
+ // never unlock anything beyond the plain guest view.
log.fatal(
"User "
+ ses.getAttribute("userName")
- + " attempted super admin privilege escalation via forged currentPerson"
- + " cookie without holding an admin session!");
- htmlOutput = "";
- } else if (!decodedCookie.equals("aGuest")) {
- log.debug("Tampered role cookie detected: " + decodedCookie);
+ + " submitted a forged currentPerson cookie claiming to be '"
+ + decodedCookie
+ + "'; ignoring it, no privileged view exists to grant.");
htmlOutput = "";
} else {
log.debug("No change to role cookie submitted");
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
index b72f5cc4d..03a981805 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
@@ -9,12 +9,10 @@
import java.util.Locale;
import java.util.ResourceBundle;
import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
-import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
@@ -70,38 +68,25 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
String htmlOutput = new String();
try {
- Cookie userCookies[] = request.getCookies();
- int i = 0;
- Cookie theCookie = null;
- for (i = 0; i < userCookies.length; i++) {
- if (userCookies[i].getName().compareTo("currentPerson") == 0) {
- theCookie = userCookies[i];
- break; // End Loop, because we found the token
- }
- }
- String currentUser = new String("aGuest");
- if (theCookie != null) {
- log.debug("Cookie value: " + theCookie.getValue());
- byte[] decodedCookieBytes = Base64.decodeBase64(theCookie.getValue());
- String decodedCookie = new String(decodedCookieBytes, "UTF-8");
- log.debug("Decoded Cookie: " + decodedCookie);
- currentUser = decodedCookie;
- }
+ // This directory lookup used to take its identity from the client-supplied
+ // "currentPerson" cookie and concatenate it straight into the SQL text, so anyone could
+ // both inject arbitrary SQL through the cookie and, even without injecting anything,
+ // enumerate every "admin" account in the sub-schema (including hints toward the real
+ // super admin) purely by asking. A request to this endpoint has no legitimate reason to
+ // see anyone's row but the fixed public guest entry, so the lookup identity below is a
+ // hardcoded constant - never derived from request/cookie input - and the query no longer
+ // has a clause that discloses privileged accounts at all.
+ final String publicDirectoryEntry = "aGuest";
String ApplicationRoot = getServletContext().getRealPath("");
Connection conn = Database.getChallengeConnection(ApplicationRoot, "UrlAccessThree");
PreparedStatement callstmt;
- callstmt =
- conn.prepareStatement(
- "SELECT userName FROM users WHERE userRole = \"admin\" OR userName = ?;");
- callstmt.setString(1, currentUser);
+ callstmt = conn.prepareStatement("SELECT userName FROM users WHERE userName = ?;");
+ callstmt.setString(1, publicDirectoryEntry);
log.debug("Getting User List");
htmlOutput = new String();
ResultSet rs = callstmt.executeQuery();
while (rs.next()) {
htmlOutput += Encode.forHtml(rs.getString(1)) + "
";
- if (rs.getString(1).equalsIgnoreCase("MrJohnReillyTheSecond")) {
- log.debug("Super Admin contained in response");
- }
}
} catch (Exception e) {
htmlOutput = new String(errors.getString("error.funky"));
From a657bfe95dadcaeef351a62720961ae4ed14b8c2 Mon Sep 17 00:00:00 2001
From: beanbeah <24713371+beanbeah@users.noreply.github.com>
Date: Sun, 9 Aug 2026 11:44:06 -0700
Subject: [PATCH 4/4] style: fix javadoc line wrap to satisfy spotless
Co-Authored-By: Claude Sonnet 5
---
src/main/java/servlets/module/challenge/UrlAccess3.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3.java b/src/main/java/servlets/module/challenge/UrlAccess3.java
index cf5a1415e..496e39df0 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3.java
@@ -46,8 +46,8 @@ public class UrlAccess3 extends HttpServlet {
* vouches for its value. Because of that it must never be trusted to answer an authorization
* question - not even when it happens to spell out a privileged-sounding name - so this servlet
* no longer branches on its content at all. There is no legitimate, server-verified way for a
- * request to actually be the sub-schema's super admin, so that response is simply unreachable
- * now rather than gated behind an easily-forged claim.
+ * request to actually be the sub-schema's super admin, so that response is simply unreachable now
+ * rather than gated behind an easily-forged claim.
*
* @param userId Red herring that is pre set to d3d9446802a44259755d38e6d163e820
* @param secure Red herring that is pre set to true