Skip to content

Commit 0dbccff

Browse files
authored
Merge pull request #2203 from OWASP/copilot/fix-2131
Implement Challenge 59: Slack Webhook URL Vulnerability with Java 23 Support and Docker Build Configuration
2 parents 1afb099 + 5c1919b commit 0dbccff

File tree

16 files changed

+646
-14
lines changed

16 files changed

+646
-14
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ WORKDIR /application
1111

1212
ARG argBasedPassword="default"
1313
ARG spring_profile=""
14+
ARG challenge59_webhook_url="YUhSMGNITTZMeTlvYjI5cmN5NXpiR0ZqYXk1amIyMHZjMlZ5ZG1salpYTXZWREEwVkRRd1RraFlMMEl3T1VSQlRrb3lUamRMTDJNeWFqYzFSVEUzVjFrd2NFeE5SRXRvU0RsbGQzZzBhdz09"
1415
ENV SPRING_PROFILES_ACTIVE=$spring_profile
1516
ENV ARG_BASED_PASSWORD=$argBasedPassword
1617
ENV APP_VERSION=$argBasedVersion
1718
ENV DOCKER_ENV_PASSWORD="This is it"
1819
ENV AZURE_KEY_VAULT_ENABLED=false
20+
ENV CHALLENGE59_SLACK_WEBHOOK_URL=$challenge59_webhook_url
1921
ENV SPRINGDOC_UI=false
2022
ENV SPRINGDOC_DOC=false
2123
ENV BASTIONHOSTPATH="/home/wrongsecrets/.ssh"

Dockerfile.web

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ ARG CTF_ENABLED=false
55
ARG HINTS_ENABLED=true
66
ARG CHALLENGE_ACHT_CTF_HOST_VALUE="not_set"
77
ARG CHALLENGE_THIRTY_HOST_VALUE="not_set"
8+
ARG challenge59_webhook_url="YUhSMGNITTZMeTlvYjI5cmN5NXpiR0ZqYXk1amIyMHZjMlZ5ZG1salpYTXZWREEwVkRRd1RraFlMMEl3T1VSQlRrb3lUamRMTDJNeWFqYzFSVEUzVjFrd2NFeE5SRXRvU0RsbGQzZzBhdz09"
9+
ENV CHALLENGE59_SLACK_WEBHOOK_URL=$challenge59_webhook_url
810
ARG CHALLENGE_RANDO_KEY_CTF_TO_PROVIDE_TO_HOST="not_set"
911
#ONLY OVERRIDE THE ARGS BELOW WHEN YOU ARE SETTING UP A CTF!
1012
ARG CTF_KEY=TRwzkRJnHOTckssAeyJbysWgP!Qc2T
@@ -28,6 +30,7 @@ ENV vaultpassword=$CHALLENGE_7_VALUE
2830
ENV challenge_acht_ctf_host_value=$CHALLENGE_ACHT_CTF_HOST_VALUE
2931
ENV challenge_thirty_ctf_to_provide_to_host_value=$CHALLENGE_THIRTY_HOST_VALUE
3032
ENV challenge_rando_key_ctf_to_provide_to_host_value=$CHALLENGE_RANDO_KEY_CTF_TO_PROVIDE_TO_HOST
33+
ENV CHALLENGE59_SLACK_WEBHOOK_URL=$CHALLENGE59_SLACK_WEBHOOK_URL
3134
ENV default_aws_value_challenge_9=$CHALLENGE_9_VALUE
3235
ENV default_aws_value_challenge_10=$CHALLENGE_10_VALUE
3336
ENV default_aws_value_challenge_11=$CHALLENGE_11_VALUE

scripts/generate-slack-webhook.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/bin/bash
2+
3+
# Script to generate obfuscated Slack webhook URLs for Challenge 59
4+
# Usage: ./generate-slack-webhook.sh [webhook-url]
5+
# If no webhook URL is provided, generates a realistic example
6+
7+
set -e
8+
9+
# Default webhook URL if none provided
10+
DEFAULT_WEBHOOK="https://hooks.slack.com/services/T123456789/B123456789/1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p"
11+
12+
# Function to validate webhook URL format
13+
validate_webhook_url() {
14+
local url="$1"
15+
if [[ ! "$url" =~ ^https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+ ]]; then
16+
echo "Error: Invalid Slack webhook URL format" >&2
17+
echo "Expected format: https://hooks.slack.com/services/T123456789/B123456789/abcdef123..." >&2
18+
exit 1
19+
fi
20+
}
21+
22+
# Function to obfuscate webhook URL with double base64 encoding
23+
obfuscate_webhook() {
24+
local webhook_url="$1"
25+
# First base64 encoding (no line wrapping)
26+
local first_encode=$(echo -n "$webhook_url" | base64 -w 0)
27+
# Second base64 encoding (no line wrapping)
28+
local double_encode=$(echo -n "$first_encode" | base64 -w 0)
29+
echo "$double_encode"
30+
}
31+
32+
# Function to deobfuscate webhook URL (for verification)
33+
deobfuscate_webhook() {
34+
local obfuscated="$1"
35+
# First base64 decode
36+
local first_decode=$(echo -n "$obfuscated" | base64 -d)
37+
# Second base64 decode
38+
local original=$(echo -n "$first_decode" | base64 -d)
39+
echo "$original"
40+
}
41+
42+
# Main script logic
43+
main() {
44+
echo "=== Slack Webhook URL Generator for Challenge 59 ==="
45+
echo
46+
47+
# Use provided webhook URL or default
48+
local webhook_url="${1:-$DEFAULT_WEBHOOK}"
49+
50+
# Validate the webhook URL format
51+
validate_webhook_url "$webhook_url"
52+
53+
echo "Original webhook URL: $webhook_url"
54+
echo
55+
56+
# Obfuscate the webhook URL
57+
local obfuscated=$(obfuscate_webhook "$webhook_url")
58+
echo "Obfuscated webhook URL (double base64 encoded):"
59+
echo "$obfuscated"
60+
echo
61+
62+
# Verification - deobfuscate to ensure it works
63+
local verified=$(deobfuscate_webhook "$obfuscated")
64+
echo "Verification (deobfuscated): $verified"
65+
echo
66+
67+
if [ "$webhook_url" = "$verified" ]; then
68+
echo "✅ Obfuscation/deobfuscation successful!"
69+
echo
70+
echo "To use this in Challenge 59:"
71+
echo "1. Update application.properties:"
72+
echo " CHALLENGE59_SLACK_WEBHOOK_URL=$obfuscated"
73+
echo
74+
echo "2. The challenge answer will be:"
75+
echo " $webhook_url"
76+
else
77+
echo "❌ Error: Obfuscation/deobfuscation failed!"
78+
exit 1
79+
fi
80+
}
81+
82+
# Show usage if help is requested
83+
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
84+
echo "Usage: $0 [webhook-url]"
85+
echo
86+
echo "Generates double base64 encoded Slack webhook URLs for Challenge 59."
87+
echo
88+
echo "Arguments:"
89+
echo " webhook-url Slack webhook URL to obfuscate (optional)"
90+
echo " If not provided, uses a realistic example"
91+
echo
92+
echo "Examples:"
93+
echo " $0"
94+
echo " $0 'https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/abcdef123456'"
95+
echo
96+
echo "The webhook URL should follow Slack's format:"
97+
echo " https://hooks.slack.com/services/T[TEAM_ID]/B[CHANNEL_ID]/[TOKEN]"
98+
exit 0
99+
fi
100+
101+
# Run main function
102+
main "$@"

src/main/java/org/owasp/wrongsecrets/Challenges.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,12 @@ public List<Difficulty> difficulties() {
9696
}
9797

9898
public boolean isFirstChallenge(ChallengeDefinition challengeDefinition) {
99-
return challengeDefinition.equals(definitions.challenges().get(0));
99+
return challengeDefinition.equals(definitions.challenges().getFirst());
100100
}
101101

102102
public boolean isLastChallenge(ChallengeDefinition challengeDefinition) {
103-
return challengeDefinition.equals(definitions.challenges().getLast());
103+
var challenges = definitions.challenges();
104+
return challengeDefinition.equals(challenges.getLast());
104105
}
105106

106107
public List<ChallengeDefinition> getChallengeDefinitions() {

src/main/java/org/owasp/wrongsecrets/WrongSecretsApplication.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.context.annotation.Bean;
1111
import org.springframework.context.annotation.Scope;
1212
import org.springframework.context.annotation.ScopedProxyMode;
13+
import org.springframework.web.client.RestTemplate;
1314

1415
@SpringBootApplication
1516
@EnableConfigurationProperties({Vaultpassword.class, Vaultinjected.class})
@@ -31,4 +32,9 @@ public RuntimeEnvironment runtimeEnvironment(
3132
ChallengeDefinitionsConfiguration challengeDefinitions) {
3233
return RuntimeEnvironment.fromString(currentRuntimeEnvironment, challengeDefinitions);
3334
}
35+
36+
@Bean
37+
public RestTemplate restTemplate() {
38+
return new RestTemplate();
39+
}
3440
}

src/main/java/org/owasp/wrongsecrets/challenges/ChallengesController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import org.owasp.wrongsecrets.RuntimeEnvironment;
1818
import org.owasp.wrongsecrets.ScoreCard;
1919
import org.owasp.wrongsecrets.challenges.docker.Challenge8;
20+
import org.owasp.wrongsecrets.challenges.docker.SlackNotificationService;
2021
import org.owasp.wrongsecrets.challenges.docker.authchallenge.Challenge37;
2122
import org.owasp.wrongsecrets.challenges.docker.challenge30.Challenge30;
2223
import org.owasp.wrongsecrets.definitions.ChallengeDefinition;
24+
import org.springframework.beans.factory.annotation.Autowired;
2325
import org.springframework.beans.factory.annotation.Value;
2426
import org.springframework.http.HttpStatus;
2527
import org.springframework.security.crypto.codec.Hex;
@@ -38,6 +40,7 @@ public class ChallengesController {
3840
private final ScoreCard scoreCard;
3941
private final RuntimeEnvironment runtimeEnvironment;
4042
private final Challenges challenges;
43+
private final SlackNotificationService slackNotificationService;
4144

4245
@Value("${hints_enabled}")
4346
private boolean hintsEnabled;
@@ -69,10 +72,12 @@ public ChallengesController(
6972
ScoreCard scoreCard,
7073
Challenges challenges,
7174
RuntimeEnvironment runtimeEnvironment,
75+
@Autowired(required = false) SlackNotificationService slackNotificationService,
7276
@Value("${spoiling_enabled}") boolean spoilingEnabled) {
7377
this.scoreCard = scoreCard;
7478
this.challenges = challenges;
7579
this.runtimeEnvironment = runtimeEnvironment;
80+
this.slackNotificationService = slackNotificationService;
7681
this.spoilingEnabled = spoilingEnabled;
7782
}
7883

@@ -216,6 +221,11 @@ public String postController(
216221

217222
if (challenge.answerCorrect(challengeForm.solution())) {
218223
scoreCard.completeChallenge(challengeDefinition);
224+
// Send Slack notification for challenge completion
225+
if (slackNotificationService != null) {
226+
slackNotificationService.notifyChallengeCompletion(
227+
challengeDefinition.name().shortName(), null);
228+
}
219229
// TODO extract this to a separate method probably have separate handler classes in the
220230
// configuration otherwise this is not maintainable, probably give the challenge a CTF
221231
// method hook which you can override and do these kind of things in there.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.owasp.wrongsecrets.challenges.docker;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
5+
import java.util.Base64;
6+
import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.stereotype.Component;
9+
10+
/**
11+
* This challenge demonstrates the security risk of hardcoded Slack webhook URLs in environment
12+
* variables. Shows how an ex-employee could misuse the webhook if it's not rotated when they leave.
13+
*/
14+
@Component
15+
public class Challenge59 extends FixedAnswerChallenge {
16+
17+
private final String obfuscatedSlackWebhookUrl;
18+
19+
public Challenge59(@Value("${CHALLENGE59_SLACK_WEBHOOK_URL}") String obfuscatedSlackWebhookUrl) {
20+
this.obfuscatedSlackWebhookUrl = obfuscatedSlackWebhookUrl;
21+
}
22+
23+
@Override
24+
public String getAnswer() {
25+
return deobfuscateSlackWebhookUrl(obfuscatedSlackWebhookUrl);
26+
}
27+
28+
/**
29+
* Deobfuscates the Slack webhook URL. The URL is base64 encoded twice to avoid detection by
30+
* security scanners.
31+
*/
32+
private String deobfuscateSlackWebhookUrl(String obfuscatedUrl) {
33+
try {
34+
// First decode from base64
35+
byte[] firstDecode = Base64.getDecoder().decode(obfuscatedUrl);
36+
// Second decode from base64
37+
byte[] secondDecode = Base64.getDecoder().decode(firstDecode);
38+
return new String(secondDecode, UTF_8);
39+
} catch (Exception e) {
40+
// Return a default value if the environment variable is not properly set
41+
return "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX";
42+
}
43+
}
44+
45+
/**
46+
* Gets the deobfuscated Slack webhook URL for use in Slack notifications. This method is used by
47+
* the Slack integration service.
48+
*/
49+
public String getSlackWebhookUrl() {
50+
return getAnswer();
51+
}
52+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.owasp.wrongsecrets.challenges.docker;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import java.util.Optional;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.http.HttpEntity;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.stereotype.Service;
13+
import org.springframework.web.client.RestTemplate;
14+
15+
/** Service for sending Slack notifications when challenges are completed. */
16+
@Service
17+
public class SlackNotificationService {
18+
19+
private static final Logger logger = LoggerFactory.getLogger(SlackNotificationService.class);
20+
21+
private final RestTemplate restTemplate;
22+
private final ObjectMapper objectMapper;
23+
private final Optional<Challenge59> challenge59;
24+
25+
public SlackNotificationService(
26+
RestTemplate restTemplate,
27+
ObjectMapper objectMapper,
28+
@Autowired(required = false) Challenge59 challenge59) {
29+
this.restTemplate = restTemplate;
30+
this.objectMapper = objectMapper;
31+
this.challenge59 = Optional.ofNullable(challenge59);
32+
}
33+
34+
/**
35+
* Sends a Slack notification when a challenge is completed.
36+
*
37+
* @param challengeName The name of the completed challenge
38+
* @param userName Optional username of the person who completed the challenge
39+
*/
40+
public void notifyChallengeCompletion(String challengeName, String userName) {
41+
if (!isSlackConfigured()) {
42+
logger.debug("Slack not configured, skipping notification for challenge: {}", challengeName);
43+
return;
44+
}
45+
46+
try {
47+
String message = buildCompletionMessage(challengeName, userName);
48+
SlackMessage slackMessage = new SlackMessage(message);
49+
50+
HttpHeaders headers = new HttpHeaders();
51+
headers.setContentType(MediaType.APPLICATION_JSON);
52+
53+
HttpEntity<SlackMessage> request = new HttpEntity<>(slackMessage, headers);
54+
55+
String webhookUrl = challenge59.get().getSlackWebhookUrl();
56+
restTemplate.postForEntity(webhookUrl, request, String.class);
57+
logger.info(
58+
"Successfully sent Slack notification for challenge completion: {}", challengeName);
59+
60+
} catch (Exception e) {
61+
logger.warn("Failed to send Slack notification for challenge: {}", challengeName, e);
62+
}
63+
}
64+
65+
private boolean isSlackConfigured() {
66+
return challenge59.isPresent()
67+
&& challenge59.get().getSlackWebhookUrl() != null
68+
&& !challenge59.get().getSlackWebhookUrl().trim().isEmpty()
69+
&& !challenge59.get().getSlackWebhookUrl().equals("not_set")
70+
&& challenge59.get().getSlackWebhookUrl().startsWith("https://hooks.slack.com");
71+
}
72+
73+
private String buildCompletionMessage(String challengeName, String userName) {
74+
String userPart = (userName != null && !userName.trim().isEmpty()) ? " by " + userName : "";
75+
76+
return String.format(
77+
"🎉 Challenge %s completed%s! Another secret vulnerability discovered in WrongSecrets.",
78+
challengeName, userPart);
79+
}
80+
81+
/** Simple record for Slack message payload. */
82+
public static class SlackMessage {
83+
@JsonProperty("text")
84+
private final String text;
85+
86+
public SlackMessage(String text) {
87+
this.text = text;
88+
}
89+
90+
public String getText() {
91+
return text;
92+
}
93+
}
94+
}

src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ challenge27ciphertext=gYPQPfb0TUgWK630tHCWGwwME6IWtPWA51eU0Qpb9H7/lMlZPdLGZWmYE8
7474
challenge41password=UEBzc3dvcmQxMjM=
7575
challenge49pin=NDQ0NDQ=
7676
challenge49ciphertext=k800mdwu8vlQoqeAgRMHDQ==
77+
CHALLENGE59_SLACK_WEBHOOK_URL=YUhSMGNITTZMeTlvYjI5cmN5NXpiR0ZqYXk1amIyMHZjMlZ5ZG1salpYTXZWREV5TXpRMU5qYzRPUzlDTVRJek5EVTJOemc1THpGaE1tSXpZelJrTldVMlpqZG5PR2c1YVRCcU1Xc3liRE50Tkc0MWJ6WndDZz09
7778
DOCKER_SECRET_CHALLENGE51=Fald';alksAjhdna'/
7879
management.endpoint.health.probes.enabled=true
7980
management.health.livenessState.enabled=true

0 commit comments

Comments
 (0)