From ed4e2dde98e8ec766f82fd74cd47e9f47319ad91 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Mar 2026 06:37:04 +0000
Subject: [PATCH 01/10] Initial plan
From adf3e7e6f16f04acdb7c1815c36cc1a7de023aa0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Mar 2026 06:50:00 +0000
Subject: [PATCH 02/10] Add Challenge 62: MCP server with Google Service
Account for Google Drive privilege escalation
Co-authored-by: commjoen <1457214+commjoen@users.noreply.github.com>
Agent-Logs-Url: https://github.com/OWASP/wrongsecrets/sessions/aea8cbf2-d8ec-451d-a4e1-60d1f9d7bd0c
---
README.md | 10 +-
docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md | 169 ++++++++++
.../challenges/docker/Challenge62.java | 35 ++
.../docker/Challenge62McpController.java | 299 ++++++++++++++++++
src/main/resources/application.properties | 3 +
.../challenge-62/challenge-62.snippet | 56 ++++
.../resources/explanations/challenge62.adoc | 38 +++
.../explanations/challenge62_hint.adoc | 37 +++
.../explanations/challenge62_reason.adoc | 35 ++
.../wrong-secrets-configuration.yaml | 14 +
.../docker/Challenge62McpControllerTest.java | 165 ++++++++++
.../challenges/docker/Challenge62Test.java | 43 +++
12 files changed, 901 insertions(+), 3 deletions(-)
create mode 100644 docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md
create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62.java
create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpController.java
create mode 100644 src/main/resources/challenges/challenge-62/challenge-62.snippet
create mode 100644 src/main/resources/explanations/challenge62.adoc
create mode 100644 src/main/resources/explanations/challenge62_hint.adoc
create mode 100644 src/main/resources/explanations/challenge62_reason.adoc
create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpControllerTest.java
create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge62Test.java
diff --git a/README.md b/README.md
index 64d01ea01..6129f4d1f 100644
--- a/README.md
+++ b/README.md
@@ -161,9 +161,12 @@ docker run -p 8080:8080 -p 8090:8090 ghcr.io/owasp/wrongsecrets/wrongsecrets-mas
β οΈ **Warning**: This is a development version built from the latest master branch and may contain experimental features or instabilities.
**π Note on Ports:**
-- Port **8080**: Main application (challenges 0-61)
+- Port **8080**: Main application (challenges 0-62)
- Port **8090**: MCP server (required for Challenge 60)
+**π Note on Challenge 62 (Google Drive MCP):**
+Challenge 62 requires a Google Service Account to be configured for full functionality. See [docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md](docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md) for setup instructions. Without configuration, the challenge will show a placeholder message.
+
Now you can try to find the secrets by means of solving the challenge offered at the links below
all the links for docker challenges (click triangle to open the block).
@@ -218,6 +221,7 @@ Now you can try to find the secrets by means of solving the challenge offered at
- [localhost:8080/challenge/challenge-59](http://localhost:8080/challenge/challenge-59)
- [localhost:8080/challenge/challenge-60](http://localhost:8080/challenge/challenge-60)
- [localhost:8080/challenge/challenge-61](http://localhost:8080/challenge/challenge-61)
+- [localhost:8080/challenge/challenge-62](http://localhost:8080/challenge/challenge-62)
Note that these challenges are still very basic, and so are their explanations. Feel free to file a PR to make them look
@@ -246,7 +250,7 @@ If you want to host WrongSecrets on Railway, you can do so by deploying [this on
## Basic K8s exercise
-_Can be used for challenges 0-6, 8, 12-43, 48-61_
+_Can be used for challenges 0-6, 8, 12-43, 48-62_
### Minikube based
@@ -341,7 +345,7 @@ This is because if you run the start script again it will replace the secret in
## Cloud Challenges
-_Can be used for challenges 0-61_
+_Can be used for challenges 0-62_
**READ THIS**: Given that the exercises below contain IAM privilege escalation exercises,
never run this on an account which is related to your production environment or can influence your account-over-arching
diff --git a/docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md b/docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md
new file mode 100644
index 000000000..14dc07a62
--- /dev/null
+++ b/docs/CHALLENGE62_GOOGLE_DRIVE_SETUP.md
@@ -0,0 +1,169 @@
+# Challenge 62: Google Service Account Setup Guide
+
+This guide explains how to configure Challenge 62, which demonstrates privilege escalation via an MCP (Model Context Protocol) server using a Google Service Account to access restricted Google Drive documents.
+
+## Overview
+
+Challenge 62 shows how an MCP server configured with an overly-privileged Google Service Account allows callers to read Google Drive documents they are not directly authorized to access. The service account acts as a privilege escalation proxy.
+
+## Prerequisites
+
+- A Google Cloud project
+- Owner or Editor role on the Google Cloud project (to create service accounts)
+- A Google Drive document containing a secret
+
+## Step 1: Create a Google Cloud Project (if needed)
+
+If you don't have a Google Cloud project:
+
+```bash
+gcloud projects create YOUR_PROJECT_ID --name="WrongSecrets Challenge 62"
+gcloud config set project YOUR_PROJECT_ID
+```
+
+## Step 2: Enable the Google Drive API
+
+```bash
+gcloud services enable drive.googleapis.com
+```
+
+## Step 3: Create a Service Account
+
+```bash
+gcloud iam service-accounts create wrongsecrets-challenge62 \
+ --display-name="WrongSecrets Challenge 62 Drive Reader" \
+ --description="Service account for WrongSecrets Challenge 62 - demonstrates MCP privilege escalation"
+```
+
+## Step 4: Create and Download a Service Account Key
+
+```bash
+gcloud iam service-accounts keys create challenge62-key.json \
+ --iam-account=wrongsecrets-challenge62@YOUR_PROJECT_ID.iam.gserviceaccount.com
+```
+
+**β οΈ Security Warning**: Service account key files are sensitive credentials. Handle them carefully:
+- Do not commit key files to version control
+- Delete the key file after encoding it
+- Rotate keys regularly
+
+## Step 5: Create a Google Drive Document with the Secret
+
+1. Go to [Google Drive](https://drive.google.com) and create a new Google Doc
+2. Add your challenge secret as the document content (e.g., `my_wrongsecrets_challenge62_answer`)
+3. Note the document ID from the URL:
+ - URL format: `https://docs.google.com/document/d/DOCUMENT_ID/edit`
+ - Copy the `DOCUMENT_ID` part
+
+## Step 6: Share the Document with the Service Account
+
+Share the Google Drive document with the service account's email address:
+
+1. Open the document in Google Drive
+2. Click **Share**
+3. Add the service account email: `wrongsecrets-challenge62@YOUR_PROJECT_ID.iam.gserviceaccount.com`
+4. Set the permission to **Viewer**
+5. Click **Send**
+
+Alternatively, use the Drive API via the CLI:
+```bash
+# Get the document ID from the URL
+DOCUMENT_ID="your_document_id_here"
+SA_EMAIL="wrongsecrets-challenge62@YOUR_PROJECT_ID.iam.gserviceaccount.com"
+
+# Share using the Drive API (requires OAuth2 token)
+curl -X POST "https://www.googleapis.com/drive/v3/files/${DOCUMENT_ID}/permissions" \
+ -H "Authorization: Bearer $(gcloud auth print-access-token)" \
+ -H "Content-Type: application/json" \
+ -d "{\"role\": \"reader\", \"type\": \"user\", \"emailAddress\": \"${SA_EMAIL}\"}"
+```
+
+## Step 7: Encode the Service Account Key
+
+Base64-encode the service account key file:
+
+```bash
+# On Linux/macOS:
+SERVICE_ACCOUNT_KEY_B64=$(base64 -w 0 challenge62-key.json)
+
+# On macOS (if the above doesn't work):
+SERVICE_ACCOUNT_KEY_B64=$(base64 -i challenge62-key.json | tr -d '\n')
+
+echo "Your base64-encoded key (use this as GOOGLE_SERVICE_ACCOUNT_KEY):"
+echo "${SERVICE_ACCOUNT_KEY_B64}"
+```
+
+## Step 8: Configure WrongSecrets
+
+Set the following environment variables when running WrongSecrets:
+
+| Variable | Description | Example |
+|----------|-------------|---------|
+| `GOOGLE_SERVICE_ACCOUNT_KEY` | Base64-encoded service account JSON key | `eyJ0eXBlIjoic2VydmljZV9hY2...` |
+| `GOOGLE_DRIVE_DOCUMENT_ID` | Google Drive document ID | `1vfHmi5lGoHogcjD0wxClZAjDy_qml_i2BtVrjVaklHc` |
+| `WRONGSECRETS_MCP_GOOGLEDRIVE_SECRET` | The secret stored in the document | `my_wrongsecrets_challenge62_answer` |
+
+### Running with Docker
+
+```bash
+docker run -p 8080:8080 -p 8090:8090 \
+ -e GOOGLE_SERVICE_ACCOUNT_KEY="${SERVICE_ACCOUNT_KEY_B64}" \
+ -e GOOGLE_DRIVE_DOCUMENT_ID="your_document_id" \
+ -e WRONGSECRETS_MCP_GOOGLEDRIVE_SECRET="your_secret_here" \
+ ghcr.io/owasp/wrongsecrets/wrongsecrets:latest-no-vault
+```
+
+### Running with Spring Boot (local development)
+
+Add the following to your `application-local.properties` or set environment variables:
+
+```properties
+GOOGLE_SERVICE_ACCOUNT_KEY=
+GOOGLE_DRIVE_DOCUMENT_ID=
+WRONGSECRETS_MCP_GOOGLEDRIVE_SECRET=
+```
+
+Or set environment variables directly:
+```bash
+export GOOGLE_SERVICE_ACCOUNT_KEY="${SERVICE_ACCOUNT_KEY_B64}"
+export GOOGLE_DRIVE_DOCUMENT_ID="your_document_id"
+export WRONGSECRETS_MCP_GOOGLEDRIVE_SECRET="your_secret_here"
+./mvnw spring-boot:run
+```
+
+## Step 9: Clean Up the Key File
+
+After encoding the key, delete the local key file:
+
+```bash
+rm challenge62-key.json
+```
+
+## Using the Default OWASP Document (for testing)
+
+The default document ID configured in the application is the OWASP WrongSecrets Google Drive document:
+- Document: https://docs.google.com/document/d/1vfHmi5lGoHogcjD0wxClZAjDy_qml_i2BtVrjVaklHc/edit
+
+To use this document, your service account must have been granted read access to it by the OWASP WrongSecrets maintainers. For your own deployment, we recommend creating your own document as described above.
+
+## Security Notes
+
+1. **This is intentionally insecure for educational purposes**: In a real system, you should always authenticate and authorize MCP callers before granting access to external resources.
+
+2. **Least Privilege**: The service account used in this challenge demonstrates what happens when you violate least privilege. In production, ensure service accounts only have the minimum permissions necessary.
+
+3. **Never use production credentials**: Do not use service accounts that have access to production data for this challenge.
+
+4. **Key rotation**: Regularly rotate service account keys to limit the window of exposure if a key is compromised.
+
+## Verification
+
+After configuration, verify the challenge works by calling the MCP endpoint:
+
+```bash
+curl -s -X POST http://localhost:8080/mcp62 \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_google_drive_document","arguments":{}}}'
+```
+
+The response should contain the document content with your secret.
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62.java
new file mode 100644
index 000000000..08c77d2ab
--- /dev/null
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62.java
@@ -0,0 +1,35 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import com.google.common.base.Strings;
+import org.owasp.wrongsecrets.challenges.Challenge;
+import org.owasp.wrongsecrets.challenges.Spoiler;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * Challenge demonstrating how an MCP server with an overly-privileged Google Service Account can be
+ * used to escalate privileges and access Google Drive documents that the caller is not authorized
+ * to read directly.
+ */
+@Component
+public class Challenge62 implements Challenge {
+
+ private final String googleDriveSecret;
+
+ public Challenge62(
+ @Value(
+ "${WRONGSECRETS_MCP_GOOGLEDRIVE_SECRET:if_you_see_this_configure_the_google_service_account_properly}")
+ String googleDriveSecret) {
+ this.googleDriveSecret = googleDriveSecret;
+ }
+
+ @Override
+ public Spoiler spoiler() {
+ return new Spoiler(googleDriveSecret);
+ }
+
+ @Override
+ public boolean answerCorrect(String answer) {
+ return !Strings.isNullOrEmpty(answer) && googleDriveSecret.equals(answer.trim());
+ }
+}
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpController.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpController.java
new file mode 100644
index 000000000..f31e73792
--- /dev/null
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpController.java
@@ -0,0 +1,299 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.auth.oauth2.ServiceAccountCredentials;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * MCP (Model Context Protocol) server endpoint for Challenge 62. Demonstrates how an MCP server
+ * configured with an overly-privileged Google Service Account allows privilege escalation: a caller
+ * without direct access to a Google Drive document can use the MCP tool to read it through the
+ * service account's elevated permissions.
+ *
+ *
The service account credentials are provided via the {@code GOOGLE_SERVICE_ACCOUNT_KEY}
+ * environment variable (base64-encoded JSON key file content). The Google Drive document ID to read
+ * is configured via {@code GOOGLE_DRIVE_DOCUMENT_ID}.
+ */
+@Slf4j
+@RestController
+public class Challenge62McpController {
+
+ private static final String JSONRPC_VERSION = "2.0";
+ private static final String DEFAULT_KEY_PLACEHOLDER =
+ "if_you_see_this_configure_the_google_service_account_properly";
+ private static final String DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.readonly";
+ private static final String DRIVE_EXPORT_URL =
+ "https://www.googleapis.com/drive/v3/files/%s/export?mimeType=text/plain";
+
+ private final String serviceAccountKeyBase64;
+ private final String documentId;
+ private final RestTemplate restTemplate;
+ private final ObjectMapper objectMapper;
+
+ @Autowired
+ public Challenge62McpController(
+ @Value(
+ "${GOOGLE_SERVICE_ACCOUNT_KEY:if_you_see_this_configure_the_google_service_account_properly}")
+ String serviceAccountKeyBase64,
+ @Value("${GOOGLE_DRIVE_DOCUMENT_ID:1LPCHxPbRpRBrM1GaGLg-Ax_0gRCuDLiR}")
+ String documentId) {
+ this(
+ serviceAccountKeyBase64,
+ documentId,
+ createDefaultRestTemplate(),
+ new ObjectMapper());
+ }
+
+ Challenge62McpController(
+ String serviceAccountKeyBase64,
+ String documentId,
+ RestTemplate restTemplate,
+ ObjectMapper objectMapper) {
+ this.serviceAccountKeyBase64 = serviceAccountKeyBase64;
+ this.documentId = documentId;
+ this.restTemplate = restTemplate;
+ this.objectMapper = objectMapper;
+ }
+
+ private static RestTemplate createDefaultRestTemplate() {
+ var factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(Duration.ofSeconds(10));
+ factory.setReadTimeout(Duration.ofSeconds(10));
+ return new RestTemplate(factory);
+ }
+
+ @PostMapping(
+ value = "/mcp62",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public Map handleMcpRequest(@RequestBody Map request) {
+ String method = (String) request.get("method");
+ Object id = request.get("id");
+ log.info("Challenge62 MCP request received for method: {}", sanitizeForLog(method));
+
+ return switch (method) {
+ case "initialize" -> buildInitializeResponse(id);
+ case "tools/list" -> buildToolsListResponse(id);
+ case "tools/call" -> handleToolCall(id, request);
+ default -> buildErrorResponse(id, -32601, "Method not found: " + method);
+ };
+ }
+
+ private Map buildInitializeResponse(Object id) {
+ return buildResponse(
+ id,
+ Map.of(
+ "protocolVersion",
+ "2024-11-05",
+ "serverInfo",
+ Map.of("name", "wrongsecrets-googledrive-mcp-server", "version", "1.0.0"),
+ "capabilities",
+ Map.of("tools", Map.of()),
+ "instructions",
+ "This MCP server provides access to Google Drive documents using a configured service"
+ + " account. Use the read_google_drive_document tool to fetch document contents."));
+ }
+
+ private Map buildToolsListResponse(Object id) {
+ return buildResponse(
+ id,
+ Map.of(
+ "tools",
+ List.of(
+ Map.of(
+ "name",
+ "read_google_drive_document",
+ "description",
+ "Read the contents of a Google Drive document using the configured service"
+ + " account credentials. The service account has read access to documents"
+ + " that may not be accessible to the caller directly.",
+ "inputSchema",
+ Map.of(
+ "type",
+ "object",
+ "properties",
+ Map.of(
+ "document_id",
+ Map.of(
+ "type",
+ "string",
+ "description",
+ "The Google Drive document ID to read (optional, uses configured"
+ + " default if not provided)")),
+ "required",
+ List.of())))));
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map handleToolCall(Object id, Map request) {
+ Map params = (Map) request.get("params");
+ if (params == null) {
+ return buildErrorResponse(id, -32602, "Missing params");
+ }
+ String toolName = (String) params.get("name");
+ Map arguments = (Map) params.get("arguments");
+ return switch (toolName) {
+ case "read_google_drive_document" -> handleReadGoogleDriveDocument(id, arguments);
+ default -> buildErrorResponse(id, -32602, "Unknown tool: " + toolName);
+ };
+ }
+
+ private Map handleReadGoogleDriveDocument(
+ Object id, Map arguments) {
+ String docId =
+ (arguments != null && arguments.get("document_id") != null)
+ ? (String) arguments.get("document_id")
+ : documentId;
+
+ log.info(
+ "Challenge62 MCP read_google_drive_document called for document: {}",
+ sanitizeForLog(docId));
+
+ if (DEFAULT_KEY_PLACEHOLDER.equals(serviceAccountKeyBase64)) {
+ log.warn(
+ "Challenge62: GOOGLE_SERVICE_ACCOUNT_KEY is not configured. "
+ + "Set this environment variable to a base64-encoded service account JSON key.");
+ return buildResponse(
+ id,
+ Map.of(
+ "content",
+ List.of(
+ Map.of(
+ "type",
+ "text",
+ "text",
+ "Google Service Account is not configured. "
+ + "Set the GOOGLE_SERVICE_ACCOUNT_KEY environment variable to a "
+ + "base64-encoded service account JSON key file."))));
+ }
+
+ try {
+ String documentContent = readGoogleDriveDocument(docId);
+ return buildResponse(
+ id, Map.of("content", List.of(Map.of("type", "text", "text", documentContent))));
+ } catch (Exception e) {
+ log.error("Challenge62: Failed to read Google Drive document: {}", e.getMessage());
+ return buildErrorResponse(id, -32603, "Failed to read document: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Reads a Google Drive document using the configured service account credentials.
+ *
+ * @param docId the Google Drive document ID
+ * @return the plain text content of the document
+ * @throws Exception if the document cannot be read
+ */
+ String readGoogleDriveDocument(String docId) throws Exception {
+ String accessToken = getServiceAccountAccessToken();
+ String exportUrl = String.format(DRIVE_EXPORT_URL, docId);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(accessToken);
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ try {
+ ResponseEntity response =
+ restTemplate.exchange(exportUrl, HttpMethod.GET, entity, String.class);
+ String body = response.getBody();
+ return body != null ? body.trim() : "";
+ } catch (RestClientException e) {
+ log.error(
+ "Challenge62: Failed to export Google Drive document {}: {}", docId, e.getMessage());
+ throw new Exception("Unable to read document from Google Drive: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Obtains an OAuth2 access token for the configured Google Service Account.
+ *
+ * @return the OAuth2 access token string
+ * @throws Exception if the token cannot be obtained
+ */
+ private String getServiceAccountAccessToken() throws Exception {
+ try {
+ byte[] keyBytes = Base64.getDecoder().decode(serviceAccountKeyBase64);
+ validateServiceAccountJson(keyBytes);
+
+ ServiceAccountCredentials credentials =
+ ServiceAccountCredentials.fromStream(new ByteArrayInputStream(keyBytes));
+ ServiceAccountCredentials scopedCredentials =
+ (ServiceAccountCredentials)
+ credentials.createScoped(Collections.singletonList(DRIVE_SCOPE));
+ scopedCredentials.refreshIfExpired();
+ return scopedCredentials.getAccessToken().getTokenValue();
+ } catch (IllegalArgumentException e) {
+ throw new Exception("Invalid base64 encoding for GOOGLE_SERVICE_ACCOUNT_KEY", e);
+ }
+ }
+
+ /**
+ * Validates that the decoded bytes represent a valid JSON object with the expected service
+ * account fields. This guards against injection of arbitrary content.
+ *
+ * @param keyBytes the decoded service account JSON bytes
+ * @throws Exception if the JSON is invalid or missing required fields
+ */
+ private void validateServiceAccountJson(byte[] keyBytes) throws Exception {
+ try {
+ JsonNode root = objectMapper.readTree(new String(keyBytes, StandardCharsets.UTF_8));
+ if (!root.isObject()) {
+ throw new Exception("Service account key must be a JSON object");
+ }
+ if (!root.has("type") || !"service_account".equals(root.get("type").asText())) {
+ throw new Exception("Invalid service account key: missing or incorrect 'type' field");
+ }
+ if (!root.has("client_email") || !root.has("private_key")) {
+ throw new Exception(
+ "Invalid service account key: missing required fields (client_email, private_key)");
+ }
+ } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
+ throw new Exception("Service account key is not valid JSON", e);
+ }
+ }
+
+ private String sanitizeForLog(String input) {
+ if (input == null) {
+ return null;
+ }
+ return input.replaceAll("[\r\n\u0085\u2028\u2029]", "_");
+ }
+
+ private Map buildResponse(Object id, Object result) {
+ Map response = new LinkedHashMap<>();
+ response.put("jsonrpc", JSONRPC_VERSION);
+ response.put("id", id);
+ response.put("result", result);
+ return response;
+ }
+
+ private Map buildErrorResponse(Object id, int code, String message) {
+ Map response = new LinkedHashMap<>();
+ response.put("jsonrpc", JSONRPC_VERSION);
+ response.put("id", id);
+ response.put("error", Map.of("code", code, "message", message));
+ return response;
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 4962e40c0..b573b811c 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -77,6 +77,9 @@ challenge49pin=NDQ0NDQ=
challenge49ciphertext=k800mdwu8vlQoqeAgRMHDQ==
CHALLENGE59_SLACK_WEBHOOK_URL=YUhSMGNITTZMeTlvYjI5cmN5NXpiR0ZqYXk1amIyMHZjMlZ5ZG1salpYTXZWREV5TXpRMU5qYzRPUzlDTVRJek5EVTJOemc1THpGaE1tSXpZelJrTldVMlpqZG5PR2c1YVRCcU1Xc3liRE50Tkc0MWJ6WndDZz09
WRONGSECRETS_MCP_SECRET=if_you_see_this_please_call_the_mcp_endpoint
+WRONGSECRETS_MCP_GOOGLEDRIVE_SECRET=if_you_see_this_configure_the_google_service_account_properly
+GOOGLE_SERVICE_ACCOUNT_KEY=if_you_see_this_configure_the_google_service_account_properly
+GOOGLE_DRIVE_DOCUMENT_ID=1LPCHxPbRpRBrM1GaGLg-Ax_0gRCuDLiR
mcp.server.port=8090
DOCKER_SECRET_CHALLENGE51=Fald';alksAjhdna'/
management.endpoint.health.probes.enabled=true
diff --git a/src/main/resources/challenges/challenge-62/challenge-62.snippet b/src/main/resources/challenges/challenge-62/challenge-62.snippet
new file mode 100644
index 000000000..758b01b06
--- /dev/null
+++ b/src/main/resources/challenges/challenge-62/challenge-62.snippet
@@ -0,0 +1,56 @@
+
+
π MCP Server with Google Service Account
+
An MCP (Model Context Protocol) server is running on this application. It exposes a read_google_drive_document tool that uses a Google Service Account to read a restricted document β even if you are not directly authorized to access it.
+
+
+
β οΈ This MCP server demonstrates privilege escalation. The service account has more permissions than you (the caller). By calling the tool, you gain access to a document you are not directly authorized to read.
+
+
+
+
Step 1 β Discover what tools the MCP server exposes:
π‘ The document's content contains the secret. Submit the secret content as your answer.
+
+
+
+
+
+
diff --git a/src/main/resources/explanations/challenge62.adoc b/src/main/resources/explanations/challenge62.adoc
new file mode 100644
index 000000000..abc974df7
--- /dev/null
+++ b/src/main/resources/explanations/challenge62.adoc
@@ -0,0 +1,38 @@
+=== MCP Server Privilege Escalation via Google Service Account
+
+The Model Context Protocol (MCP) allows AI assistants and agents to use tools and access external services. When an MCP server is configured with a Google Service Account that has broader permissions than the calling user, it creates a privilege escalation vulnerability: anyone who can call the MCP tool gains access to resources they are not directly authorized to access.
+
+This challenge demonstrates a realistic scenario where a developer has built an MCP server to help an AI assistant access internal Google Drive documents. The service account used by the MCP server has read access to a restricted document β but the MCP server does not verify that the caller is authorized to access it.
+
+**Your goal:**
+
+1. **An MCP server is running on this application** accessible via the `/mcp62` endpoint
+2. **The server exposes a `read_google_drive_document` tool** that uses a Google Service Account
+3. **The service account has read access to a Google Drive document** containing the secret
+4. **The tool does not check whether the caller is authorized** to access that document
+
+**How to interact with the MCP server:**
+
+First, discover the available tools:
+
+[source,bash]
+----
+curl -s -X POST http://localhost:8080/mcp62 \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
+----
+
+Then, call the `read_google_drive_document` tool to retrieve the document contents:
+
+[source,bash]
+----
+curl -s -X POST http://localhost:8080/mcp62 \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_google_drive_document","arguments":{}}}'
+----
+
+****
+π‘ *Note:*
+
+The server reads a Google Drive document using a configured service account. The document's content contains the secret you need to submit as your answer. The key security insight is that the MCP server's service account has privileged access that neither you nor your AI assistant would normally have β but the MCP server grants it implicitly to anyone who calls its tools.
+****
diff --git a/src/main/resources/explanations/challenge62_hint.adoc b/src/main/resources/explanations/challenge62_hint.adoc
new file mode 100644
index 000000000..925944144
--- /dev/null
+++ b/src/main/resources/explanations/challenge62_hint.adoc
@@ -0,0 +1,37 @@
+=== Hint for Challenge 62
+
+This challenge demonstrates how an MCP server with an overly-privileged Google Service Account enables privilege escalation.
+
+**Where to look:**
+
+1. **An MCP server is running at `/mcp62`** on the main application port
+2. **The MCP server exposes a `read_google_drive_document` tool** that uses a Google Service Account to read a document
+3. **You do not need valid Google credentials** β the service account is already configured in the application
+
+**Step-by-step approach:**
+
+First, list the tools the MCP server offers:
+
+[source,bash]
+----
+curl -s -X POST http://localhost:8080/mcp62 \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
+----
+
+Then call `read_google_drive_document` to read the document:
+
+[source,bash]
+----
+curl -s -X POST http://localhost:8080/mcp62 \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_google_drive_document","arguments":{}}}'
+----
+
+**What to look for:**
+
+- The response contains the contents of a Google Drive document
+- The document contains the secret answer for this challenge
+- Notice that you accessed a document you are not directly authorized to read β the MCP server's service account provided that access
+
+**Remember:** This is an example of privilege escalation through an MCP server. The service account has more permissions than you (the caller) would have, and the MCP server does not enforce caller-level access controls.
diff --git a/src/main/resources/explanations/challenge62_reason.adoc b/src/main/resources/explanations/challenge62_reason.adoc
new file mode 100644
index 000000000..1be9d9b92
--- /dev/null
+++ b/src/main/resources/explanations/challenge62_reason.adoc
@@ -0,0 +1,35 @@
+=== Why Challenge 62 Matters: MCP Privilege Escalation via Google Service Account
+
+**The Problem:**
+
+MCP (Model Context Protocol) servers are increasingly used to give AI assistants access to external services and data sources. When these servers are configured with service accounts or API keys that have broader permissions than the calling user, they create a dangerous privilege escalation vector.
+
+This challenge demonstrates the **Principle of Least Privilege** violation: the MCP server's Google Service Account has access to a restricted Google Drive document, but the server grants that access to *any caller* without verifying whether the caller is authorized to see the document.
+
+**The Real-World Scenario:**
+
+1. A developer builds an MCP server to help an internal AI assistant access team documents in Google Drive
+2. The service account is granted read access to a sensitive document
+3. The developer does not implement any caller authentication or authorization checks on the MCP tools
+4. Anyone who discovers the MCP endpoint can now read documents they are not supposed to access β effectively using the service account as a privilege escalation proxy
+
+**Why This Is Dangerous:**
+
+- **Privilege escalation**: Callers gain the service account's permissions, not their own
+- **No audit trail**: Access via the MCP tool may not be logged with the caller's identity
+- **Broad attack surface**: Any MCP client (legitimate or malicious) can invoke the tool
+- **AI-assisted exploitation**: AI agents connected to this MCP server will execute the tool automatically when instructed, amplifying the risk
+
+**How to Fix:**
+
+1. **Authenticate MCP callers**: Require an authentication token on the MCP endpoint and verify caller identity before executing tools
+2. **Authorize at the tool level**: Check whether the authenticated caller is permitted to access the requested resource before calling the service account
+3. **Scope service account permissions narrowly**: The service account should have the minimum permissions necessary for legitimate use cases only
+4. **Audit all tool invocations**: Log the caller identity along with every tool call so access can be reviewed
+5. **Prefer impersonation over shared credentials**: Use caller-specific credentials or token exchange instead of a single shared service account
+
+**References:**
+
+- https://modelcontextprotocol.io/specification/2024-11-05/[MCP Specification]
+- https://owasp.org/www-project-top-ten/[OWASP Top 10]
+- https://cloud.google.com/iam/docs/best-practices-service-accounts[Google Service Account Best Practices]
diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml
index 7529c4fed..976e6f087 100644
--- a/src/main/resources/wrong-secrets-configuration.yaml
+++ b/src/main/resources/wrong-secrets-configuration.yaml
@@ -947,3 +947,17 @@ configurations:
category: *secrets
ctf:
enabled: true
+
+ - name: Challenge 62
+ short-name: "challenge-62"
+ sources:
+ - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge62"
+ explanation: "explanations/challenge62.adoc"
+ hint: "explanations/challenge62_hint.adoc"
+ reason: "explanations/challenge62_reason.adoc"
+ ui-snippet: "challenges/challenge-62/challenge-62.snippet"
+ environments: *all_envs
+ difficulty: *normal
+ category: *ai
+ ctf:
+ enabled: true
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpControllerTest.java
new file mode 100644
index 000000000..669ab5163
--- /dev/null
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge62McpControllerTest.java
@@ -0,0 +1,165 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+class Challenge62McpControllerTest {
+
+ private static final String DEFAULT_KEY =
+ "if_you_see_this_configure_the_google_service_account_properly";
+ private static final String DEFAULT_DOC_ID = "1LPCHxPbRpRBrM1GaGLg-Ax_0gRCuDLiR";
+
+ @Test
+ void initializeShouldReturnServerInfo() {
+ var controller =
+ new Challenge62McpController(DEFAULT_KEY, DEFAULT_DOC_ID, mock(RestTemplate.class), new ObjectMapper());
+ Map request = Map.of("jsonrpc", "2.0", "id", 1, "method", "initialize");
+
+ Map response = controller.handleMcpRequest(request);
+
+ assertThat(response).containsKey("result");
+ @SuppressWarnings("unchecked")
+ Map result = (Map) response.get("result");
+ assertThat(result).containsKey("serverInfo");
+ @SuppressWarnings("unchecked")
+ Map serverInfo = (Map) result.get("serverInfo");
+ assertThat(serverInfo.get("name")).isEqualTo("wrongsecrets-googledrive-mcp-server");
+ }
+
+ @Test
+ void toolsListShouldExposeReadGoogleDriveDocumentTool() {
+ var controller =
+ new Challenge62McpController(DEFAULT_KEY, DEFAULT_DOC_ID, mock(RestTemplate.class), new ObjectMapper());
+ Map request = Map.of("jsonrpc", "2.0", "id", 1, "method", "tools/list");
+
+ Map response = controller.handleMcpRequest(request);
+
+ @SuppressWarnings("unchecked")
+ Map result = (Map) response.get("result");
+ @SuppressWarnings("unchecked")
+ List