diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java new file mode 100644 index 000000000..54184106b --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java @@ -0,0 +1,101 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.owasp.wrongsecrets.Challenges.ErrorResponses.DECRYPTION_ERROR; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import lombok.extern.slf4j.Slf4j; +import org.owasp.wrongsecrets.challenges.Challenge; +import org.owasp.wrongsecrets.challenges.Spoiler; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** This is a challenge based on using weak KDF to protect secrets. */ +@Slf4j +@Component +public class Challenge49 implements Challenge { + + private final String cipherText; + private final String pin; + + public Challenge49( + @Value("${challenge49ciphertext}") String cipherText, + @Value("${challenge49pin}") String pin) { + this.cipherText = cipherText; + this.pin = pin; + } + + @Override + public Spoiler spoiler() { + return new Spoiler(base64Decode(pin)); + } + + @Override + public boolean answerCorrect(String answer) { + String plainText = "the answer"; + + try { + int enteredPin = Integer.parseInt(answer); + if (enteredPin < 0 || enteredPin > 99999) { + return false; + } + } catch (Exception e) { + log.warn("given answer is not an integer", e); + return false; + } + + try { + String md5Hash = hashWithMd5(answer); + return decrypt(cipherText, md5Hash).equals(plainText); + } catch (Exception e) { + log.warn("there was an exception with hashing content in challenge49", e); + return false; + } + } + + @SuppressFBWarnings( + value = "WEAK_MESSAGE_DIGEST_MD5", + justification = "This is to allow md5 hashing") + private String hashWithMd5(String plainText) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + + byte[] result = md.digest(plainText.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : result) { + hexString.append(String.format("%02x", b)); + } + return hexString.toString(); + } + + @SuppressFBWarnings( + value = {"CIPHER_INTEGRITY", "ECB_MODE"}, + justification = "This is to allow ecb encryption") + private String decrypt(String cipherText, String key) { + try { + byte[] decodedEncryptedText = Base64.getDecoder().decode(cipherText); + + SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); + + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + + byte[] decryptedData = cipher.doFinal(decodedEncryptedText); + + return new String(decryptedData, StandardCharsets.UTF_8); + } catch (Exception e) { + log.warn("there was an exception with decrypting content in challenge49", e); + return DECRYPTION_ERROR; + } + } + + private String base64Decode(String base64) { + byte[] decodedBytes = Base64.getDecoder().decode(base64); + return new String(decodedBytes, StandardCharsets.UTF_8); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4043278b1..12905a57c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -72,6 +72,8 @@ challenge26ciphertext=gbU5thfgy8nwzF/qc1Pq59PrJzLB+bfAdTOrx969JZx1CKeG4Sq7v1uUpz DEFAULT37=DEFAULT37 challenge27ciphertext=gYPQPfb0TUgWK630tHCWGwwME6IWtPWA51eU0Qpb9H7/lMlZPdLGZWmYE83YmEDmaEvFr2hX challenge41password=UEBzc3dvcmQxMjM= +challenge49pin=NDQ0NDQ= +challenge49ciphertext=k800mdwu8vlQoqeAgRMHDQ== management.endpoint.health.probes.enabled=true management.health.livenessState.enabled=true management.health.readinessState.enabled=true diff --git a/src/main/resources/explanations/challenge49.adoc b/src/main/resources/explanations/challenge49.adoc new file mode 100644 index 000000000..be7dba7ae --- /dev/null +++ b/src/main/resources/explanations/challenge49.adoc @@ -0,0 +1,9 @@ +=== Cracking AES Encryption with a Weak MD5 Key + +Imagine you're a security analyst investigating a mobile app that handles sensitive information. You discover that the developer is using AES encryption to protect a secret, but instead of using a strong Key Derivation Function (KDF), they rely on the insecure MD5 algorithm to derive encryption keys from a simple numeric PIN. + +You’ve obtained an encrypted string: `k800mdwu8vlQoqeAgRMHDQ==`. You know that this string, when decrypted, reveals the text `the answer`. + +The key used for AES encryption is derived by taking the MD5 hash of a PIN, which is a number between 0 and 99999. Your task is to find the correct PIN that was used to derive the encryption key and decrypt the secret. + +Can you figure out the correct PIN and unlock the secret? diff --git a/src/main/resources/explanations/challenge49_hint.adoc b/src/main/resources/explanations/challenge49_hint.adoc new file mode 100644 index 000000000..0eee60e63 --- /dev/null +++ b/src/main/resources/explanations/challenge49_hint.adoc @@ -0,0 +1,5 @@ +The simplest way to crack the PIN in this scenario is to perform a brute-force attack due to the limited range of possible values (0 to 99,999). + +- Iterate over all possible PINs (from 0 to 99,999). +- For each PIN, compute its MD5 hash to get the decryption key and try decrypting provided ciphertext. +- If decrypted text is equal to `the answer`, you've found the correct PIN. diff --git a/src/main/resources/explanations/challenge49_reason.adoc b/src/main/resources/explanations/challenge49_reason.adoc new file mode 100644 index 000000000..037bf7435 --- /dev/null +++ b/src/main/resources/explanations/challenge49_reason.adoc @@ -0,0 +1,9 @@ +*Why Using MD5 as a KDF is Bad* + +Protecting keys effectively is crucial, and this means using the right Key Derivation Functions (KDFs) with additional entropy and contextual binding, as emphasized in the https://mas.owasp.org/MASTG/0x04g-Testing-Cryptography/#weak-key-generation-functions[Mobile Security Testing Guide (MSTG).] + +MD5 is too fast and easy to compute, enabling attackers to quickly try a vast number of inputs (like PINs) to derive the key. Additionally (although a bit harder to exploit), the collision space is relatively small, meaning multiple different inputs can lead to the same md5 hash. + +Stronger KDFs are crucial when dealing with sensitive data, as they provide much-needed resistance against brute-force attacks and protect secrets even if the attacker gains access to partial information. + +The MSTG recommends using robust KDFs like PBKDF2, bcrypt, or Argon2, which incorporate additional entropy and enforce computational hardness, making brute-force attacks more costly. diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index cf71de4c9..dc1004ead 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -775,3 +775,16 @@ configurations: category: *secrets ctf: enabled: false + + - name: Challenge 49 + short-name: "challenge-49" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge49" + explanation: "explanations/challenge49.adoc" + hint: "explanations/challenge49_hint.adoc" + reason: "explanations/challenge49_reason.adoc" + environments: *all_envs + difficulty: *hard + category: *crypto + ctf: + enabled: true diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge49Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge49Test.java new file mode 100644 index 000000000..c9fbd2048 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge49Test.java @@ -0,0 +1,46 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class Challenge49Test { + + @Test + void spoilerShouldGiveAnswer() { + var challenge = new Challenge49("uz5cIFm0hW3LtWaqEX0S/Q==", "MTIzNDU="); + Assertions.assertThat(challenge.spoiler().solution()).isEqualTo("12345"); + } + + @Test + void correctPinShouldSolveChallenge() { + var challenge = new Challenge49("uz5cIFm0hW3LtWaqEX0S/Q==", "MTIzNDU="); + Assertions.assertThat(challenge.answerCorrect("12345")).isTrue(); + } + + @Test + void nonIntegerPinShouldNotSolveChallenge() { + var challenge = new Challenge49("uz5cIFm0hW3LtWaqEX0S/Q==", "MTIzNDU="); + Assertions.assertThat(challenge.answerCorrect("abcde")).isFalse(); + } + + @Test + void incorrectPinShouldNotSolveChallenge() { + var challenge = new Challenge49("uz5cIFm0hW3LtWaqEX0S/Q==", "MTIzNDU="); + Assertions.assertThat(challenge.answerCorrect("1234")).isFalse(); + } + + @Test + void pinGreaterThan99999ShouldNotSolveChallenge() { + var challenge = new Challenge49("uz5cIFm0hW3LtWaqEX0S/Q==", "MTIzNDU="); + Assertions.assertThat(challenge.answerCorrect("123456")).isFalse(); + } + + @Test + void pinLesserThan0ShouldNotSolveChallenge() { + var challenge = new Challenge49("uz5cIFm0hW3LtWaqEX0S/Q==", "MTIzNDU="); + Assertions.assertThat(challenge.answerCorrect("-123456")).isFalse(); + } +}