Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add challenge 49 #1651

Merged
merged 4 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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");
Dismissed Show dismissed Hide dismissed

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);
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/explanations/challenge49.adoc
Original file line number Diff line number Diff line change
@@ -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?
5 changes: 5 additions & 0 deletions src/main/resources/explanations/challenge49_hint.adoc
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions src/main/resources/explanations/challenge49_reason.adoc
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions src/main/resources/wrong-secrets-configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading