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