From 006f2e981ece2dc1afac59d32f65ea99288117cf Mon Sep 17 00:00:00 2001 From: Adarsh A <104344230+adarsh-a-tw@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:37:13 +0530 Subject: [PATCH 1/3] feat: add challenge 49 --- .../challenges/docker/Challenge49.java | 103 ++++++++++++++++++ src/main/resources/application.properties | 2 + .../resources/explanations/challenge49.adoc | 9 ++ .../explanations/challenge49_hint.adoc | 5 + .../explanations/challenge49_reason.adoc | 9 ++ .../wrong-secrets-configuration.yaml | 13 +++ .../challenges/docker/Challenge49Test.java | 46 ++++++++ 7 files changed, 187 insertions(+) create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java create mode 100644 src/main/resources/explanations/challenge49.adoc create mode 100644 src/main/resources/explanations/challenge49_hint.adoc create mode 100644 src/main/resources/explanations/challenge49_reason.adoc create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge49Test.java 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..31ba064fe --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java @@ -0,0 +1,103 @@ +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.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** This is a challenge based on using weak KDF to protect secrets. */ +@Slf4j +@Component +@Order(49) +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..cb6b00b79 --- /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. + +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(); + } +} From d2beffa6b7a30a89ae6d0fcecc3669e4a84411b1 Mon Sep 17 00:00:00 2001 From: Adarsh A <104344230+adarsh-a-tw@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:38:40 +0530 Subject: [PATCH 2/3] refactor: remove order annotation in challenge49 --- .../org/owasp/wrongsecrets/challenges/docker/Challenge49.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java index 31ba064fe..54184106b 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge49.java @@ -14,13 +14,11 @@ import org.owasp.wrongsecrets.challenges.Challenge; import org.owasp.wrongsecrets.challenges.Spoiler; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** This is a challenge based on using weak KDF to protect secrets. */ @Slf4j @Component -@Order(49) public class Challenge49 implements Challenge { private final String cipherText; From 867d836aa5d9d479396ecf7912be3be62df7a199 Mon Sep 17 00:00:00 2001 From: Jeroen Willemsen Date: Thu, 3 Oct 2024 14:13:27 +0200 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Ben de Haan <53901866+bendehaan@users.noreply.github.com> --- src/main/resources/explanations/challenge49_reason.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/explanations/challenge49_reason.adoc b/src/main/resources/explanations/challenge49_reason.adoc index cb6b00b79..037bf7435 100644 --- a/src/main/resources/explanations/challenge49_reason.adoc +++ b/src/main/resources/explanations/challenge49_reason.adoc @@ -1,8 +1,8 @@ -*Why Using MD5 as a KDF is Bad ?* +*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. +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.