diff --git a/.gitignore b/.gitignore index b964337051..ed502d9ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,12 @@ src/main/resources/executables/decrypt/decrypt src/main/resources/executables/wrongsecrets-dotnet src/main/resources/executables/wrongsecrets-dotnet* +# Challenge 65/66 +!src/main/resources/executables/wrongsecrets-java.jar +!src/main/resources/executables/wrongsecrets-java-ctf.jar +!src/main/resources/executables/wrongsecrets-java-obfuscated.jar +!src/main/resources/executables/wrongsecrets-java-obfuscated-ctf.jar + # Challenge 59 k8s/challenge53/executables/wrongsecrets-challenge53-c k8s/challenge53/executables/wrongsecrets-challenge53-c* diff --git a/Dockerfile_webdesktop b/Dockerfile_webdesktop index 70e4e632f0..bfd4cd9f78 100644 --- a/Dockerfile_webdesktop +++ b/Dockerfile_webdesktop @@ -24,6 +24,7 @@ RUN --mount=type=secret,id=mysecret \ WORKDIR /config/Desktop COPY src/main/resources/executables/*linux-mus* /var/tmp/wrongsecrets/ +COPY src/main/resources/executables/wrongsecrets-java*.jar /var/tmp/wrongsecrets/ COPY src/main/resources/executables/decrypt/ /var/tmp/wrongsecrets/decrypt/ COPY src/main/resources/executables/wrongsecrets-advanced-c-windows.exe /var/tmp/wrongsecrets/ COPY src/main/resources/executables/secrchallenge.md /var/tmp/wrongsecrets/ diff --git a/Dockerfile_webdesktopk8s b/Dockerfile_webdesktopk8s index 70e4e632f0..bfd4cd9f78 100644 --- a/Dockerfile_webdesktopk8s +++ b/Dockerfile_webdesktopk8s @@ -24,6 +24,7 @@ RUN --mount=type=secret,id=mysecret \ WORKDIR /config/Desktop COPY src/main/resources/executables/*linux-mus* /var/tmp/wrongsecrets/ +COPY src/main/resources/executables/wrongsecrets-java*.jar /var/tmp/wrongsecrets/ COPY src/main/resources/executables/decrypt/ /var/tmp/wrongsecrets/decrypt/ COPY src/main/resources/executables/wrongsecrets-advanced-c-windows.exe /var/tmp/wrongsecrets/ COPY src/main/resources/executables/secrchallenge.md /var/tmp/wrongsecrets/ diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge65.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge65.java new file mode 100644 index 0000000000..00de5e2f0b --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge65.java @@ -0,0 +1,18 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge; +import org.owasp.wrongsecrets.challenges.docker.binaryexecution.BinaryExecutionHelper; +import org.owasp.wrongsecrets.challenges.docker.binaryexecution.MuslDetectorImpl; +import org.springframework.stereotype.Component; + +/** This challenge is about finding a secret hardcoded in a plain Java CLI JAR. */ +@Component +public class Challenge65 extends FixedAnswerChallenge { + + @Override + public String getAnswer() { + BinaryExecutionHelper binaryExecutionHelper = + new BinaryExecutionHelper(65, new MuslDetectorImpl()); + return binaryExecutionHelper.executeJavaJar("", "wrongsecrets-java.jar"); + } +} diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge66.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge66.java new file mode 100644 index 0000000000..70f85564fc --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge66.java @@ -0,0 +1,18 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge; +import org.owasp.wrongsecrets.challenges.docker.binaryexecution.BinaryExecutionHelper; +import org.owasp.wrongsecrets.challenges.docker.binaryexecution.MuslDetectorImpl; +import org.springframework.stereotype.Component; + +/** This challenge is about finding a secret hidden in an obfuscated Java CLI JAR. */ +@Component +public class Challenge66 extends FixedAnswerChallenge { + + @Override + public String getAnswer() { + BinaryExecutionHelper binaryExecutionHelper = + new BinaryExecutionHelper(66, new MuslDetectorImpl()); + return binaryExecutionHelper.executeJavaJar("", "wrongsecrets-java-obfuscated.jar"); + } +} diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/binaryexecution/BinaryExecutionHelper.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/binaryexecution/BinaryExecutionHelper.java index 9a6b802944..74af6bae40 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/binaryexecution/BinaryExecutionHelper.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/binaryexecution/BinaryExecutionHelper.java @@ -6,13 +6,18 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; +import org.springframework.core.io.ClassPathResource; import org.springframework.util.ResourceUtils; /** Helper for classes to execute binaries as part of the Binary challenges. */ @@ -111,6 +116,37 @@ public String executeCommand(String guess, String fileName) { } } + /** + * Execute a Java CLI packaged as a JAR for either secret retrieval or guess validation. + * + * @param guess containing the guess + * @param fileName of the JAR to be used (pre-defined, make sure it is never user input + * controlled) + * @return the actual answer + */ + public String executeJavaJar(String guess, String fileName) { + BinaryInstructionForFile binaryInstructionForFile; + if (Strings.isNullOrEmpty(guess)) { + binaryInstructionForFile = BinaryInstructionForFile.Spoil; + } else { + binaryInstructionForFile = BinaryInstructionForFile.Guess; + } + try { + File jarFile = createTempJar(fileName); + String result = executeJavaJar(jarFile, binaryInstructionForFile, guess); + deleteFile(jarFile); + log.info( + "stdout challenge {}: {}", + challengeNumber, + result.lines().collect(Collectors.joining(""))); + return result; + } catch (Exception e) { + log.warn("Error executing Java JAR:", e); + executionException = e; + return ERROR_EXECUTION; + } + } + @SuppressFBWarnings( value = "COMMAND_INJECTION", justification = "We check for various injection methods and counter those") @@ -146,6 +182,34 @@ private String executeCommand( } } + @SuppressFBWarnings( + value = "COMMAND_INJECTION", + justification = "We check for various injection methods and counter those") + private String executeJavaJar( + File jarFile, BinaryInstructionForFile binaryInstructionForFile, String guess) + throws IOException, InterruptedException { + if (!jarFile.getPath().contains("wrongsecrets") + || stringContainsCommandChainToken(jarFile.getPath()) + || stringContainsCommandChainToken(guess)) { + return BinaryExecutionHelper.ERROR_EXECUTION; + } + + ProcessBuilder ps; + if (binaryInstructionForFile.equals(BinaryInstructionForFile.Spoil)) { + ps = new ProcessBuilder("java", "-jar", jarFile.getPath(), "spoil"); + } else { + ps = new ProcessBuilder("java", "-jar", jarFile.getPath(), guess); + } + ps.redirectErrorStream(true); + Process pr = ps.start(); + try (BufferedReader in = + new BufferedReader(new InputStreamReader(pr.getInputStream(), StandardCharsets.UTF_8))) { + String result = in.readLine(); + pr.waitFor(); + return result; + } + } + private boolean stringContainsCommandChainToken(String testString) { String[] tokens = {"!", "&", "|", "<", ">", ";"}; boolean found = false; @@ -248,6 +312,20 @@ private File createTempExecutable(String fileName) throws IOException { return execFile; } + @SuppressFBWarnings( + value = "PATH_TRAVERSAL_IN", + justification = "The jar file name is hardcoded at the caller level") + private File createTempJar(String fileName) throws IOException { + File execFile = File.createTempFile("java-jar-" + fileName.replace('.', '-'), ".jar"); + try { + FileUtils.copyInputStreamToFile( + new ClassPathResource("executables/" + fileName).getInputStream(), execFile); + } catch (IOException e) { + FileUtils.copyFile(retrieveFile(fileName), execFile); + } + return execFile; + } + @SuppressFBWarnings( value = "COMMAND_INJECTION", justification = "We check for various injection methods and counter those") diff --git a/src/main/resources/executables/wrongsecrets-java-ctf.jar b/src/main/resources/executables/wrongsecrets-java-ctf.jar new file mode 100644 index 0000000000..03e4294cec Binary files /dev/null and b/src/main/resources/executables/wrongsecrets-java-ctf.jar differ diff --git a/src/main/resources/executables/wrongsecrets-java-obfuscated-ctf.jar b/src/main/resources/executables/wrongsecrets-java-obfuscated-ctf.jar new file mode 100644 index 0000000000..0adfa825ea Binary files /dev/null and b/src/main/resources/executables/wrongsecrets-java-obfuscated-ctf.jar differ diff --git a/src/main/resources/executables/wrongsecrets-java-obfuscated.jar b/src/main/resources/executables/wrongsecrets-java-obfuscated.jar new file mode 100644 index 0000000000..3eaaf7b13d Binary files /dev/null and b/src/main/resources/executables/wrongsecrets-java-obfuscated.jar differ diff --git a/src/main/resources/executables/wrongsecrets-java.jar b/src/main/resources/executables/wrongsecrets-java.jar new file mode 100644 index 0000000000..fae1006397 Binary files /dev/null and b/src/main/resources/executables/wrongsecrets-java.jar differ diff --git a/src/main/resources/explanations/challenge65.adoc b/src/main/resources/explanations/challenge65.adoc new file mode 100644 index 0000000000..25c250771b --- /dev/null +++ b/src/main/resources/explanations/challenge65.adoc @@ -0,0 +1,10 @@ +=== Hiding in binaries part 6: the plain Java CLI + +Plain strings inside a Java CLI are easy to recover once the JAR is downloaded. Can you find the secret hidden in our plain Java CLI? + +To solve it: + +. Download and inspect https://github.com/OWASP/wrongsecrets/raw/master/src/main/resources/executables/wrongsecrets-java.jar[wrongsecrets-java.jar]. +. Decompile the JAR or inspect the main class with a Java decompiler such as CFR, JADX, IntelliJ IDEA, or `javap`. +. Look for the class that returns the secret and identify the plain string embedded in the CLI. +. Once you recover the exact secret value, you can submit it in the box below or validate it with `java -jar wrongsecrets-java.jar `. diff --git a/src/main/resources/explanations/challenge65_hint.adoc b/src/main/resources/explanations/challenge65_hint.adoc new file mode 100644 index 0000000000..e3d8a5a65f --- /dev/null +++ b/src/main/resources/explanations/challenge65_hint.adoc @@ -0,0 +1,18 @@ +This challenge uses a plain Java CLI JAR. + +You can solve it by: + +1. Find the compiled class that holds the secret: +- Download `wrongsecrets-java.jar`. +- Run `jar tf wrongsecrets-java.jar` and locate `io/github/owasp/wrongsecrets/WrongSecretsPlain.class`. +- Open that class in CFR, JADX, IntelliJ IDEA, or another decompiler. + +2. Inspect how the secret is stored: +- Run `javap -c -p -classpath wrongsecrets-java.jar io.github.owasp.wrongsecrets.WrongSecretsPlain`. +- Find the `getSecret()` method and look at the `ldc` instruction it uses. +- Notice that the secret is stored as a plain string constant instead of being obfuscated. + +3. Recover the value and submit it: +- Copy the string returned by `getSecret()` from the decompiler or bytecode output. +- If you want another quick check, tools like `strings wrongsecrets-java.jar` can also help expose readable constants. +- Submit the recovered string as the answer. diff --git a/src/main/resources/explanations/challenge65_reason.adoc b/src/main/resources/explanations/challenge65_reason.adoc new file mode 100644 index 0000000000..04997f4192 --- /dev/null +++ b/src/main/resources/explanations/challenge65_reason.adoc @@ -0,0 +1,7 @@ +*Why runnable JARs and Android APKs should not be used to hide secrets.* + +Runnable JARs are not a safe place to hide secrets. Just like Android APKs, they are archives that ship bytecode and resources directly to the attacker, which makes embedded strings, constants, and helper methods straightforward to inspect with common reverse-engineering tools. + +If a client-side Java artifact needs a secret to work, assume that secret can be extracted once the file is downloaded. Keep real secrets on a trusted backend and only release them after proper authentication and authorization. + +If you want more Java and Android reverse-engineering practice, explore the https://github.com/OWASP/MASTG-Hacking-Playground[OWASP MASTG Hacking Playground]. diff --git a/src/main/resources/explanations/challenge66.adoc b/src/main/resources/explanations/challenge66.adoc new file mode 100644 index 0000000000..d97f91cee8 --- /dev/null +++ b/src/main/resources/explanations/challenge66.adoc @@ -0,0 +1,10 @@ +=== Hiding in binaries part 7: the obfuscated Java CLI + +Obfuscation might slow someone down, but it does not stop them from recovering embedded secrets. Can you find the harder secret in our obfuscated Java CLI? + +To solve it: + +. Download and inspect https://github.com/OWASP/wrongsecrets/tree/master/src/main/resources/executables/wrongsecrets-java-obfuscated.jar[wrongsecrets-java-obfuscated.jar]. +. Decompile the JAR with a Java decompiler such as CFR, JADX, or IntelliJ IDEA and trace the main class. +. Look for the encoded byte array, XOR key, and helper methods that reconstruct the secret at runtime. +. Once you recover the secret, submit it with `java -jar wrongsecrets-java-obfuscated.jar `. diff --git a/src/main/resources/explanations/challenge66_hint.adoc b/src/main/resources/explanations/challenge66_hint.adoc new file mode 100644 index 0000000000..1f9977b842 --- /dev/null +++ b/src/main/resources/explanations/challenge66_hint.adoc @@ -0,0 +1,20 @@ +This challenge uses an obfuscated Java CLI JAR. + +You can solve it by: + +1. Find where the obfuscated data lives: +- Download `wrongsecrets-java-obfuscated.jar`. +- Run `jar tf wrongsecrets-java-obfuscated.jar` and locate `io/github/owasp/wrongsecrets/WrongSecretsObfuscated.class`. +- Open that class in CFR, JADX, IntelliJ IDEA, or another decompiler. +- Look for the static fields that hold the XOR key and the encoded secret bytes. + +2. Inspect the exact decoding logic: +- Run `javap -c -p -classpath wrongsecrets-java-obfuscated.jar io.github.owasp.wrongsecrets.WrongSecretsObfuscated`. +- In the output, find the `static { ... }` block that fills `XOR_KEY_CHARS` and `ENCODED_SECRET`. +- Then find `decodeSecret()` and note that each encoded byte is XORed with one byte from the key, repeating the key with modulo arithmetic. + +3. Rebuild the secret yourself: +- Convert the `XOR_KEY_CHARS` values into bytes. +- Copy the `ENCODED_SECRET` byte values from the bytecode or decompiled source. +- XOR each encoded byte with the matching key byte, wrapping around when you reach the end of the key. +- Decode the resulting byte array as UTF-8 to recover the secret, then submit that value as the answer. diff --git a/src/main/resources/explanations/challenge66_reason.adoc b/src/main/resources/explanations/challenge66_reason.adoc new file mode 100644 index 0000000000..c2596e56e3 --- /dev/null +++ b/src/main/resources/explanations/challenge66_reason.adoc @@ -0,0 +1,5 @@ +*Why obfuscation is only a speed bump.* + +Encoding, reflection, and light obfuscation can make reverse engineering less convenient, but they do not create real secrecy. The executable still contains everything it needs to recover the secret. + +If the application can derive the secret locally, a determined attacker can do the same. Protect secrets by moving trust decisions and secret material to controlled server-side systems. diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index 00993f2350..6871933e11 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -987,3 +987,29 @@ configurations: category: *bin ctf: enabled: true + + - name: Challenge 65 + short-name: "challenge-65" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge65" + explanation: "explanations/challenge65.adoc" + hint: "explanations/challenge65_hint.adoc" + reason: "explanations/challenge65_reason.adoc" + environments: *all_envs + difficulty: *normal + category: *bin + ctf: + enabled: true + + - name: Challenge 66 + short-name: "challenge-66" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge66" + explanation: "explanations/challenge66.adoc" + hint: "explanations/challenge66_hint.adoc" + reason: "explanations/challenge66_reason.adoc" + environments: *all_envs + difficulty: *master + category: *bin + ctf: + enabled: true diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge65Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge65Test.java new file mode 100644 index 0000000000..2ea564f989 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge65Test.java @@ -0,0 +1,25 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.owasp.wrongsecrets.Challenges.ErrorResponses.EXECUTION_ERROR; + +import org.junit.jupiter.api.Test; +import org.owasp.wrongsecrets.challenges.Spoiler; + +class Challenge65Test { + + @Test + void spoilerShouldNotCrash() { + var challenge = new Challenge65(); + + assertThat(challenge.spoiler()).isNotEqualTo(new Spoiler(EXECUTION_ERROR)); + assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); + } + + @Test + void incorrectAnswerShouldNotSolveChallenge() { + var challenge = new Challenge65(); + + assertThat(challenge.answerCorrect("wrong answer")).isFalse(); + } +} diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge66Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge66Test.java new file mode 100644 index 0000000000..839214dc92 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge66Test.java @@ -0,0 +1,25 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.owasp.wrongsecrets.Challenges.ErrorResponses.EXECUTION_ERROR; + +import org.junit.jupiter.api.Test; +import org.owasp.wrongsecrets.challenges.Spoiler; + +class Challenge66Test { + + @Test + void spoilerShouldNotCrash() { + var challenge = new Challenge66(); + + assertThat(challenge.spoiler()).isNotEqualTo(new Spoiler(EXECUTION_ERROR)); + assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); + } + + @Test + void incorrectAnswerShouldNotSolveChallenge() { + var challenge = new Challenge66(); + + assertThat(challenge.answerCorrect("wrong answer")).isFalse(); + } +}