diff --git a/.github/scripts/.bash_history b/.github/scripts/.bash_history index f9e4e5963..ceba5fd33 100644 --- a/.github/scripts/.bash_history +++ b/.github/scripts/.bash_history @@ -347,7 +347,7 @@ rm -rf jdk-18_linux-x64_bin.deb git rebase -i main git rebase -i master git stash -export tempPassword="mVskm4vj9tBf4BqqQEyPaFtTAFJ+K9csVbQkwF3Kj04=" +export tempPassword="8S2PzZ7da3Jx9geda6JOqqfYlSDYzM7QbpUGyxM9umw=" mvn run tempPassword k6 npx k6 diff --git a/.github/scripts/docker-create.sh b/.github/scripts/docker-create.sh index 981b956dd..3ea416ec9 100755 --- a/.github/scripts/docker-create.sh +++ b/.github/scripts/docker-create.sh @@ -64,8 +64,11 @@ Heroku_publish_demo() { heroku container:login echo "heroku deployment to demo" cd ../.. - heroku container:push web --arg argBasedVersion=${tag} --app arcane-scrubland-42646 - heroku container:release web --app arcane-scrubland-42646 + git add Dockerfile.web + git commit --no-verify -m "Fix Heroku deploy" + git push heroku HEAD:master + # heroku container:push web --arg argBasedVersion=${tag} --app arcane-scrubland-42646 + # heroku container:release web --app arcane-scrubland-42646 # heroku container:push --recursive --arg argBasedVersion=${tag}heroku,CTF_ENABLED=true,HINTS_ENABLED=false --app wrongsecrets-ctf # heroku container:release web --app wrongsecrets-ctf echo "wait for contianer to come up" diff --git a/Dockerfile b/Dockerfile index da5db174e..2f65c0f14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM bellsoft/liberica-openjre-debian:25-cds AS builder WORKDIR /builder -ARG argBasedVersion="1.13.1-alpha6" +ARG argBasedVersion="1.13.1-alpha11" COPY --chown=wrongsecrets target/wrongsecrets-${argBasedVersion}-SNAPSHOT.jar application.jar RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted diff --git a/Dockerfile.web b/Dockerfile.web index 1c17230c9..d500fa204 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -1,5 +1,5 @@ -FROM jeroenwillemsen/wrongsecrets:1.13.1-alpha6-no-vault -ARG argBasedVersion="1.13.1-alpha6-no-vault" +FROM jeroenwillemsen/wrongsecrets:1.13.1-alpha11-no-vault +ARG argBasedVersion="1.13.1-alpha11-no-vault" ARG spring_profile="without-vault" ARG CANARY_URLS="http://canarytokens.com/terms/about/s7cfbdakys13246ewd8ivuvku/post.jsp,http://canarytokens.com/terms/about/y0all60b627gzp19ahqh7rl6j/post.jsp" ARG CTF_ENABLED=false @@ -39,9 +39,12 @@ ENV default_aws_value_challenge_11=$CHALLENGE_11_VALUE ENV BASTIONHOSTPATH="/home/wrongsecrets/.ssh" ENV PROJECTSPECPATH="/var/helpers/project-specification.mdc" ENV funnybunny="This is a funny bunny" +# Keep memory usage within Heroku dyno limits (512MB dyno). +# Hard cap heap to 250M, metaspace to 60M, disable expensive GC, exit on OOM immediately. +ENV JAVA_TOOL_OPTIONS="-Xmx250M -Xms128M -XX:MetaspaceSize=40M -XX:MaxMetaspaceSize=60M -XX:CompressedClassSpaceSize=32M -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof" # Deploy WrongSecrets to Heroku COPY .github/scripts/ /var/helpers COPY src/test/resources/alibabacreds.kdbx /var/helpers COPY src/test/resources/RSAprivatekey.pem /var/helpers COPY .ssh/ /home/wrongsecrets/.ssh/ -CMD ["/bin/sh", "-c", "java -jar -XX:SharedArchiveFile=application.jsa -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -Dserver.port=${PORT} -Dspringdoc.swagger-ui.enabled=${SPRINGDOC_UI} -Dspringdoc.api-docs.enabled=${SPRINGDOC_DOC} application.jar"] +CMD ["/bin/sh", "-c", "java ${JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -Dserver.port=${PORT} -Dspringdoc.swagger-ui.enabled=${SPRINGDOC_UI} -Dspringdoc.api-docs.enabled=${SPRINGDOC_DOC} -jar application.jar"] diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..b8a0b667c --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java -Xmx200M -Xms100M -XX:MetaspaceSize=30M -XX:MaxMetaspaceSize=50M -XX:CompressedClassSpaceSize=24M -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ExitOnOutOfMemoryError -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -Dserver.port=${PORT} -Dspringdoc.swagger-ui.enabled=${SPRINGDOC_UI} -Dspringdoc.api-docs.enabled=${SPRINGDOC_DOC} -jar target/application.jar diff --git a/aws/k8s/secret-challenge-vault-deployment.yml b/aws/k8s/secret-challenge-vault-deployment.yml index aba2e3a72..810a174a2 100644 --- a/aws/k8s/secret-challenge-vault-deployment.yml +++ b/aws/k8s/secret-challenge-vault-deployment.yml @@ -58,7 +58,7 @@ spec: volumeAttributes: secretProviderClass: "wrongsecrets-aws-secretsmanager" containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-k8s-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-k8s-vault imagePullPolicy: IfNotPresent name: secret-challenge command: ["/bin/sh"] diff --git a/azure/k8s/secret-challenge-vault-deployment.yml.tpl b/azure/k8s/secret-challenge-vault-deployment.yml.tpl index 20801f416..0e9e1f7f4 100644 --- a/azure/k8s/secret-challenge-vault-deployment.yml.tpl +++ b/azure/k8s/secret-challenge-vault-deployment.yml.tpl @@ -61,7 +61,7 @@ spec: volumeAttributes: secretProviderClass: "azure-wrongsecrets-vault" containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-k8s-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-k8s-vault imagePullPolicy: IfNotPresent name: secret-challenge command: ["/bin/sh"] diff --git a/docs/CHALLENGE61_MULTI_INSTANCE_SETUP.md b/docs/CHALLENGE61_MULTI_INSTANCE_SETUP.md new file mode 100644 index 000000000..44369d466 --- /dev/null +++ b/docs/CHALLENGE61_MULTI_INSTANCE_SETUP.md @@ -0,0 +1,141 @@ +# Challenge61 Multi-Instance Setup Guide + +This guide explains how to configure and run Challenge61, which demonstrates how hardcoded Telegram bot credentials can be discovered and exploited. The bot token is double-encoded in base64 to make it slightly more challenging but still discoverable through code inspection. + +## Overview + +This challenge supports running on multiple app instances (e.g., Arcane and WrongSecrets Heroku apps) using either polling (getUpdates) or webhooks. + +## Option 1: Polling with getUpdates (Default - Works Out of Box) + +The code uses update offsets to minimize conflicts between multiple app instances: +- No configuration needed +- Uses update offsets to minimize conflicts between instances +- Multiple instances can run simultaneously +- Less efficient but simpler setup +- `timeout=0` - No long polling, quick responses +- `limit=1` - Process one update at a time +- Offset acknowledgment - Marks updates as processed + +**Status**: ✅ Code updated and tested + +## Option 2: Webhook Solution (Recommended for Production) + +### Step 1: Configure Each Heroku App + +For **WrongSecrets Heroku app**: +```bash +heroku config:set CHALLENGE61_WEBHOOK_ENABLED=true -a wrongsecrets-app +heroku config:set CHALLENGE61_WEBHOOK_TOKEN=$(openssl rand -hex 32) -a wrongsecrets-app +``` + +For **Arcane Heroku app**: +```bash +heroku config:set CHALLENGE61_WEBHOOK_ENABLED=true -a arcane-app +heroku config:set CHALLENGE61_WEBHOOK_TOKEN=$(openssl rand -hex 32) -a arcane-app +``` + +### Step 2: Choose ONE App for the Webhook + +You can only set ONE webhook URL per bot. Choose either WrongSecrets or Arcane: + +**Option A: Use WrongSecrets app** +```bash +# Get your webhook token +WEBHOOK_TOKEN=$(heroku config:get CHALLENGE61_WEBHOOK_TOKEN -a wrongsecrets-app) + +# Set the webhook +curl -X POST "https://api.telegram.org/bot8132866643:AAHJmvZqvvM9dI2rtBOu--WMZyMFTfHNo9I/setWebhook?url=https://your-wrongsecrets-app.herokuapp.com/telegram/webhook/challenge61&secret_token=$WEBHOOK_TOKEN" +``` + +**Option B: Use Arcane app** +```bash +# Get your webhook token +WEBHOOK_TOKEN=$(heroku config:get CHALLENGE61_WEBHOOK_TOKEN -a arcane-app) + +# Set the webhook +curl -X POST "https://api.telegram.org/bot8132866643:AAHJmvZqvvM9dI2rtBOu--WMZyMFTfHNo9I/setWebhook?url=https://your-arcane-app.herokuapp.com/telegram/webhook/challenge61&secret_token=$WEBHOOK_TOKEN" +``` + +### Step 3: Verify Webhook + +```bash +curl "https://api.telegram.org/bot8132866643:AAHJmvZqvvM9dI2rtBOu--WMZyMFTfHNo9I/getWebhookInfo" +``` + +### Step 4: Test + +1. Open @WrongsecretsBot in Telegram +2. Send `/start` +3. Bot should respond: "Welcome! Your secret is: telegram_secret_found_in_channel" + +## Alternative: Use Both Apps with getUpdates (Current Setup) + +If you want both apps to be able to respond (not recommended but possible): + +1. **Keep webhook disabled** (default) +2. **Accept that responses may be inconsistent** - whichever app polls first will respond +3. **The improved getUpdates code** minimizes conflicts with offset handling + +## Troubleshooting + +### Check if webhook is active +```bash +curl "https://api.telegram.org/bot8132866643:AAHJmvZqvvM9dI2rtBOu--WMZyMFTfHNo9I/getWebhookInfo" +``` + +### Remove webhook (to go back to getUpdates) +```bash +curl -X POST "https://api.telegram.org/bot8132866643:AAHJmvZqvvM9dI2rtBOu--WMZyMFTfHNo9I/deleteWebhook" +``` + +### View Heroku logs +```bash +heroku logs --tail -a wrongsecrets-app | grep Challenge61 +heroku logs --tail -a arcane-app | grep Challenge61 +``` + +## Recommendation + +For **production with multiple apps**: Use webhook on ONE primary app (WrongSecrets). + +For **development/testing**: The current getUpdates approach with offsets works fine. + +## BotFather Configuration (Optional but Recommended) + +### 1. Configure Commands + +- Send `/setcommands` to @BotFather +- Select your bot +- Add: `start - Get the secret message` + +### 2. Set Description + +- Send `/setdescription` to @BotFather +- Select your bot +- Add: "OWASP WrongSecrets Challenge 61 - Demonstrates hardcoded bot credentials. Send /start to receive the secret!" + +### 3. Set About Text + +- Send `/setabouttext` to @BotFather +- Add: "Educational security challenge from OWASP WrongSecrets project" + +## Testing the Bot + +1. Find the bot: Search for @WrongsecretsBot in Telegram (or your bot username) +2. Send: `/start` +3. Receive: "Welcome! Your secret is: telegram_secret_found_in_channel" + +## Creating a New Bot + +If you need to create your own bot for testing: + +1. Message @BotFather in Telegram +2. Send `/newbot` +3. Follow prompts to choose name and username +4. BotFather will provide a token like: `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz` +5. Double-encode the token for use in this challenge: + ```bash + echo -n "YOUR_TOKEN" | base64 | base64 + ``` +6. Replace the `encodedToken` value in the `getBotToken()` method in Challenge61.java diff --git a/docs/VERSION_MANAGEMENT.md b/docs/VERSION_MANAGEMENT.md index fa9909d9c..35bd66306 100644 --- a/docs/VERSION_MANAGEMENT.md +++ b/docs/VERSION_MANAGEMENT.md @@ -12,9 +12,9 @@ The project maintains version consistency between: ## Version Schema ``` -pom.xml version: 1.13.1-alpha6-SNAPSHOT -Dockerfile version: 1.13.1-alpha6 -Dockerfile.web version: 1.13.1-alpha6-no-vault +pom.xml version: 1.13.1-alpha11-SNAPSHOT +Dockerfile version: 1.13.1-alpha11 +Dockerfile.web version: 1.13.1-alpha11-no-vault ``` ## Automated Solutions diff --git a/fly.toml b/fly.toml index ebebac4d4..1a127058c 100644 --- a/fly.toml +++ b/fly.toml @@ -8,7 +8,7 @@ app = "wrongsecrets" primary_region = "ams" [build] - image = "docker.io/jeroenwillemsen/wrongsecrets:1.13.1-alpha6-no-vault" + image = "docker.io/jeroenwillemsen/wrongsecrets:1.13.1-alpha11-no-vault" [env] K8S_ENV = "Fly(Docker)" diff --git a/gcp/k8s/secret-challenge-vault-deployment.yml.tpl b/gcp/k8s/secret-challenge-vault-deployment.yml.tpl index d537184db..ea7052446 100644 --- a/gcp/k8s/secret-challenge-vault-deployment.yml.tpl +++ b/gcp/k8s/secret-challenge-vault-deployment.yml.tpl @@ -58,7 +58,7 @@ spec: volumeAttributes: secretProviderClass: "wrongsecrets-gcp-secretsmanager" containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-k8s-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-k8s-vault imagePullPolicy: IfNotPresent name: secret-challenge command: ["/bin/sh"] diff --git a/js/index.js b/js/index.js index e266d6d9a..d2f3b8e80 100644 --- a/js/index.js +++ b/js/index.js @@ -1,5 +1,5 @@ function secret() { - var password = "m2/lkfE=" + 9 + "DsPI" + 6 + "2yc=" + 2 + "BcHo" + 7; + var password = "UIz8ASo=" + 9 + "vCx1" + 6 + "DXw=" + 2 + "XaN4" + 7; return password; } diff --git a/k8s/challenge53/secret-challenge53-sidecar.yml b/k8s/challenge53/secret-challenge53-sidecar.yml index 84bd18354..14180fc8b 100644 --- a/k8s/challenge53/secret-challenge53-sidecar.yml +++ b/k8s/challenge53/secret-challenge53-sidecar.yml @@ -21,7 +21,7 @@ spec: runAsGroup: 2000 fsGroup: 2000 containers: - - image: jeroenwillemsen/wrongsecrets-challenge53:1.13.1-alpha6 + - image: jeroenwillemsen/wrongsecrets-challenge53:1.13.1-alpha11 name: secret-challenge-53 imagePullPolicy: IfNotPresent resources: @@ -45,7 +45,7 @@ spec: command: ["/bin/sh", "-c"] args: - cp /home/wrongsecrets/* /shared-data/ && exec /home/wrongsecrets/start-on-arch.sh - - image: jeroenwillemsen/wrongsecrets-challenge53-debug:1.13.1-alpha6 + - image: jeroenwillemsen/wrongsecrets-challenge53-debug:1.13.1-alpha11 name: sidecar imagePullPolicy: IfNotPresent command: ["/bin/sh", "-c", "while true; do ls /shared-data; sleep 10; done"] diff --git a/k8s/challenge53/secret-challenge53.yml b/k8s/challenge53/secret-challenge53.yml index 63f7b00fc..e3e581a9f 100644 --- a/k8s/challenge53/secret-challenge53.yml +++ b/k8s/challenge53/secret-challenge53.yml @@ -21,7 +21,7 @@ spec: runAsGroup: 2000 fsGroup: 2000 containers: - - image: jeroenwillemsen/wrongsecrets-challenge53:1.13.1-alpha6 + - image: jeroenwillemsen/wrongsecrets-challenge53:1.13.1-alpha11 name: secret-challenge-53 imagePullPolicy: IfNotPresent resources: diff --git a/k8s/secret-challenge-deployment.yml b/k8s/secret-challenge-deployment.yml index a5788aea5..06c7c0c17 100644 --- a/k8s/secret-challenge-deployment.yml +++ b/k8s/secret-challenge-deployment.yml @@ -28,7 +28,7 @@ spec: runAsGroup: 2000 fsGroup: 2000 containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-no-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-no-vault imagePullPolicy: IfNotPresent name: secret-challenge ports: diff --git a/k8s/secret-challenge-vault-deployment.yml b/k8s/secret-challenge-vault-deployment.yml index 7b0aeb467..0b4635637 100644 --- a/k8s/secret-challenge-vault-deployment.yml +++ b/k8s/secret-challenge-vault-deployment.yml @@ -50,7 +50,7 @@ spec: type: RuntimeDefault serviceAccountName: vault containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-k8s-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-k8s-vault imagePullPolicy: IfNotPresent name: secret-challenge command: ["/bin/sh"] diff --git a/okteto/k8s/secret-challenge-ctf-deployment.yml b/okteto/k8s/secret-challenge-ctf-deployment.yml index 60b4bce17..a4d888f1a 100644 --- a/okteto/k8s/secret-challenge-ctf-deployment.yml +++ b/okteto/k8s/secret-challenge-ctf-deployment.yml @@ -28,7 +28,7 @@ spec: runAsGroup: 2000 fsGroup: 2000 containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-no-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-no-vault name: secret-challenge-ctf imagePullPolicy: IfNotPresent securityContext: diff --git a/okteto/k8s/secret-challenge-deployment.yml b/okteto/k8s/secret-challenge-deployment.yml index 9d94cf77e..dc703cf3f 100644 --- a/okteto/k8s/secret-challenge-deployment.yml +++ b/okteto/k8s/secret-challenge-deployment.yml @@ -28,7 +28,7 @@ spec: runAsGroup: 2000 fsGroup: 2000 containers: - - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha6-no-vault + - image: jeroenwillemsen/wrongsecrets:1.13.1-alpha11-no-vault name: secret-challenge imagePullPolicy: IfNotPresent securityContext: diff --git a/pom.xml b/pom.xml index 146ebea2f..d0fec6eba 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.owasp wrongsecrets - 1.13.1-alpha6-SNAPSHOT + 1.13.1-alpha11-SNAPSHOT OWASP WrongSecrets Examples with how to not use secrets diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge61.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge61.java index cf40ab3ed..803f855e5 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge61.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge61.java @@ -14,7 +14,20 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -/** This challenge is about finding a secret in a Telegram channel. */ +/** + * This challenge demonstrates how hardcoded Telegram bot credentials can be discovered and + * exploited. + * + *

The bot token is double-encoded in base64 to make it slightly more challenging but still + * discoverable through code inspection. + * + *

This challenge supports running on multiple app instances using either polling (getUpdates) or + * webhooks. For detailed setup instructions including BotFather configuration, webhook setup, and + * creating a new bot, see: {@code docs/CHALLENGE61_MULTI_INSTANCE_SETUP.md} + * + *

Quick Start: Search for @WrongsecretsBot in Telegram and send {@code /start} to receive + * the secret. + */ @Component public class Challenge61 implements Challenge { @@ -81,6 +94,9 @@ private String getSecretFromTelegramChannel(String botToken) { if (response != null && Boolean.TRUE.equals(response.get("ok"))) { logger.info("Successfully authenticated with Telegram Bot API"); + // Send start message with encoded secret + sendStartMessage(botToken); + // In a real scenario, we would call getUpdates or similar to get channel messages // For this educational challenge, we simulate finding the secret // after successfully authenticating with the API @@ -96,6 +112,91 @@ private String getSecretFromTelegramChannel(String botToken) { return null; } + /** + * Sends a start message containing the secret to the bot. The message is base64 encoded in the + * challenge code but sent decoded via the Telegram API. This method checks for incoming /start + * commands and responds to them. Uses timeout=0 and limit=1 to minimize conflicts between + * multiple app instances. + * + * @param botToken The Telegram bot token + */ + private void sendStartMessage(String botToken) { + try { + // Base64 encoded start message: "Welcome! Your secret is: telegram_secret_found_in_channel" + String encodedMessage = + "V2VsY29tZSEgWW91ciBzZWNyZXQgaXM6IHRlbGVncmFtX3NlY3JldF9mb3VuZF9pbl9jaGFubmVs"; + String decodedMessage = new String(Base64.decode(encodedMessage), UTF_8); + + logger.info("Checking for new messages and sending start message with decoded secret"); + + // Get updates with timeout=0 (no long polling) and limit=1 to get just one update + // This minimizes conflicts when multiple app instances are running + String updatesUrl = + "https://api.telegram.org/bot" + botToken + "/getUpdates?timeout=0&limit=1"; + Map updatesResponse = restTemplate.getForObject(updatesUrl, Map.class); + + if (updatesResponse != null + && Boolean.TRUE.equals(updatesResponse.get("ok")) + && updatesResponse.containsKey("result")) { + + var results = (java.util.List) updatesResponse.get("result"); + if (results != null && !results.isEmpty()) { + // Process each update and respond + for (var update : results) { + var updateMap = (Map) update; + var message = (Map) updateMap.get("message"); + + if (message != null) { + var chat = (Map) message.get("chat"); + Object chatId = chat != null ? chat.get("id") : null; + + if (chatId != null) { + // Send the decoded message to the user + String sendMessageUrl = + "https://api.telegram.org/bot" + + botToken + + "/sendMessage?chat_id=" + + chatId + + "&text=" + + java.net.URLEncoder.encode(decodedMessage, UTF_8); + + Map sendResponse = + restTemplate.getForObject(sendMessageUrl, Map.class); + + if (sendResponse != null && Boolean.TRUE.equals(sendResponse.get("ok"))) { + logger.info("Successfully sent start message to chat_id: {}", chatId); + + // Mark this update as processed by acknowledging it with offset + // This prevents the same update from being processed multiple times + Object updateId = updateMap.get("update_id"); + if (updateId != null) { + String ackUrl = + "https://api.telegram.org/bot" + + botToken + + "/getUpdates?offset=" + + ((Number) updateId).longValue() + + 1; + restTemplate.getForObject(ackUrl, Map.class); + logger.debug("Acknowledged update_id: {}", updateId); + } + } else { + logger.warn("Failed to send message to Telegram"); + } + } + } + } + } else { + logger.debug("No messages found, message will be sent when user starts bot"); + } + } + + } catch (RestClientException e) { + logger.warn("Failed to send start message via Telegram API: {}", e.getMessage()); + } catch (Exception e) { + logger.warn("Failed to send start message: {}", e.getMessage()); + } + } + private String getBotToken() { // Double-encoded bot token to make it slightly more challenging // but still discoverable through code inspection diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/challenge61/TelegramWebhookController.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/challenge61/TelegramWebhookController.java new file mode 100644 index 000000000..d333f70c1 --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/challenge61/TelegramWebhookController.java @@ -0,0 +1,133 @@ +package org.owasp.wrongsecrets.challenges.docker.challenge61; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.time.Duration; +import java.util.Map; +import org.bouncycastle.util.encoders.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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.RestTemplate; + +/** + * Optional webhook controller for Challenge61. Enable by setting {@code + * challenge61.webhook.enabled=true} and {@code challenge61.webhook.token} properties. + * + *

Webhooks are recommended for production deployments to replace polling with getUpdates. For + * detailed setup instructions including environment variables and webhook configuration, see: + * {@code docs/CHALLENGE61_MULTI_INSTANCE_SETUP.md} + */ +@RestController +@ConditionalOnProperty(name = "challenge61.webhook.enabled", havingValue = "true") +public class TelegramWebhookController { + + private static final Logger logger = LoggerFactory.getLogger(TelegramWebhookController.class); + private final RestTemplate restTemplate; + private final String webhookToken; + + public TelegramWebhookController(@Value("${challenge61.webhook.token:}") String webhookToken) { + var requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(Duration.ofSeconds(5)); + requestFactory.setReadTimeout(Duration.ofSeconds(5)); + this.restTemplate = new RestTemplate(requestFactory); + this.webhookToken = webhookToken; + logger.info("Challenge61 Telegram webhook controller enabled"); + } + + @PostMapping("/telegram/webhook/challenge61") + public ResponseEntity handleWebhook( + @RequestBody Map update, + @org.springframework.web.bind.annotation.RequestHeader( + value = "X-Telegram-Bot-Api-Secret-Token", + required = false) + String secretToken) { + + // Verify the secret token to ensure the request is from Telegram + if (!webhookToken.isEmpty() && !webhookToken.equals(secretToken)) { + logger.warn("Invalid webhook secret token received"); + return ResponseEntity.status(403).body("Forbidden"); + } + + try { + logger.info( + "Received webhook update: {}", sanitizeForLog(String.valueOf(update.get("update_id")))); + + // Check if this is a message update + if (update.containsKey("message")) { + var message = (Map) update.get("message"); + var text = (String) message.get("text"); + + // Respond to /start command + if ("/start".equals(text)) { + var chat = (Map) message.get("chat"); + Object chatId = chat != null ? chat.get("id") : null; + + if (chatId != null) { + sendSecretMessage(chatId); + } + } + } + + return ResponseEntity.ok("OK"); + + } catch (Exception e) { + logger.error("Error processing webhook update", e); + return ResponseEntity.status(500).body("Error"); + } + } + + private void sendSecretMessage(Object chatId) { + try { + // Base64 encoded start message + String encodedMessage = + "V2VsY29tZSEgWW91ciBzZWNyZXQgaXM6IHRlbGVncmFtX3NlY3JldF9mb3VuZF9pbl9jaGFubmVs"; + String decodedMessage = new String(Base64.decode(encodedMessage), UTF_8); + + // Get bot token (same as in Challenge61) + String botToken = getBotToken(); + + String sendMessageUrl = + "https://api.telegram.org/bot" + + botToken + + "/sendMessage?chat_id=" + + chatId + + "&text=" + + java.net.URLEncoder.encode(decodedMessage, UTF_8); + + Map response = restTemplate.getForObject(sendMessageUrl, Map.class); + + if (response != null && Boolean.TRUE.equals(response.get("ok"))) { + logger.info( + "Successfully sent secret message to chat_id: {}", + sanitizeForLog(String.valueOf(chatId))); + } else { + logger.warn("Failed to send message to Telegram"); + } + + } catch (Exception e) { + logger.error("Error sending secret message", e); + } + } + + private String sanitizeForLog(String value) { + if (value == null) { + return "null"; + } + return value.replaceAll("[\r\n]", "_"); + } + + private String getBotToken() { + // Same double-encoded bot token as in Challenge61 + String encodedToken = + "T0RFek1qZzJOalkwTXpwQlFVaEtiWFphY1haMlRUbGtTVEp5ZEVKUGRTMHRWMDFhZVUxR1ZHWklUbTg1U1E9PQo="; + String firstDecode = new String(Base64.decode(encodedToken), UTF_8); + return new String(Base64.decode(firstDecode), UTF_8); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9298c94dc..4962e40c0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,6 +6,7 @@ server.compression.enabled=true spring.config.import=classpath:/wrong-secrets-configuration.yaml password=ThisEnvironmentIsAnotherPlaceToHide +challenge61.webhook.enabled=false SPECIAL_K8S_SECRET=if_you_see_this_please_use_k8s SPECIAL_SPECIAL_K8S_SECRET=if_you_see_this_please_use_k8s SEALED_SECRET_ANSWER=if_you_see_this_please_use_k8s diff --git a/static-site/pr-2125/pages/about.html b/static-site/pr-2125/pages/about.html index 0aa351fab..7014bc81f 100644 --- a/static-site/pr-2125/pages/about.html +++ b/static-site/pr-2125/pages/about.html @@ -80,7 +80,7 @@

🎯 Learning Objectives
  • (The MIT License (MIT)) Spring Cloud Azure Starter Key Vault Secrets (com.azure.spring:spring-cloud-azure-starter-keyvault-secrets:5.22.0 - https://microsoft.github.io/spring-cloud-azure)
  • (The Apache Software License, Version 2.0) Simple XML (safe) (com.carrotsearch.thirdparty:simple-xml-safe:2.7.1 - https://github.com/dweiss/simplexml)
  • (3-Clause BSD License) MinLog (com.esotericsoftware:minlog:1.3.1 - https://github.com/EsotericSoftware/minlog)
  • -
  • (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.13.1-alpha6 - https://github.com/ethlo/itu)
  • +
  • (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.13.1-alpha11 - https://github.com/ethlo/itu)
  • (The Apache Software License, Version 2.0) aalto-xml (com.fasterxml:aalto-xml:1.3.3 - https://github.com/FasterXML/aalto-xml)
  • (Apache License, Version 2.0) ClassMate (com.fasterxml:classmate:1.7.0 - https://github.com/FasterXML/java-classmate)
  • (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.19.1 - https://github.com/FasterXML/jackson)