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/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..8229add588 --- /dev/null +++ b/src/main/resources/explanations/challenge65.adoc @@ -0,0 +1,5 @@ +=== Hiding in binaries part 6: the plain Java CLI + +Compiled Java applications can hide secrets too, even when they are only distributed as runnable JARs. Can you find the secret in our plain Java CLI? + +Try downloading and inspecting https://github.com/OWASP/wrongsecrets/tree/master/src/main/resources/executables/wrongsecrets-java.jar[wrongsecrets-java.jar]. Run it locally with `java -jar wrongsecrets-java.jar spoil` or `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..0e5ef7eb0b --- /dev/null +++ b/src/main/resources/explanations/challenge65_hint.adoc @@ -0,0 +1,7 @@ +This challenge uses a plain Java CLI JAR. + +You can solve it by: + +1. Extracting the JAR and looking at the compiled classes or bundled metadata. +2. Using tools like `strings`, CFR, `javap`, JADX, or IntelliJ's decompiler. +3. Running the JAR with `spoil` or trying to understand how it compares your input. diff --git a/src/main/resources/explanations/challenge65_reason.adoc b/src/main/resources/explanations/challenge65_reason.adoc new file mode 100644 index 0000000000..c935ff2ed5 --- /dev/null +++ b/src/main/resources/explanations/challenge65_reason.adoc @@ -0,0 +1,5 @@ +*Why shipping a secret inside a plain JAR is still shipping the secret to the attacker.* + +Java bytecode is straightforward to decompile. If a secret is embedded in a class, an attacker can recover it from the JAR with static analysis or by observing the program at runtime. + +If a client-side executable needs a secret to do its job, assume the secret can be extracted. Prefer retrieving secrets from a trusted backend after proper authentication and authorization. diff --git a/src/main/resources/explanations/challenge66.adoc b/src/main/resources/explanations/challenge66.adoc new file mode 100644 index 0000000000..dc8378816a --- /dev/null +++ b/src/main/resources/explanations/challenge66.adoc @@ -0,0 +1,5 @@ +=== 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? + +Try downloading and inspecting https://github.com/OWASP/wrongsecrets/tree/master/src/main/resources/executables/wrongsecrets-java-obfuscated.jar[wrongsecrets-java-obfuscated.jar]. Run it locally with `java -jar wrongsecrets-java-obfuscated.jar spoil` or `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..24476669cd --- /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. Decompile the JAR with a Java decompiler such as CFR, JADX, or IntelliJ IDEA: +- Download `wrongsecrets-java-obfuscated.jar`. +- Open it in your decompiler of choice. +- Find the main class and trace what happens when the program receives the `spoil` argument. +- Look for helper methods that rebuild the secret at runtime. + +2. Inspect the bytecode from the command line: +- Run `jar tf wrongsecrets-java-obfuscated.jar` to list the classes in the JAR. +- Extract it with `jar xf wrongsecrets-java-obfuscated.jar`. +- Run `javap -c -p io/github/owasp/wrongsecrets/WrongSecretsObfuscated.class` on the extracted class. +- Look for the encoded byte array, the XOR key, and the method that decodes the secret. + +3. Run the JAR locally and observe its behavior: +- Execute `java -jar wrongsecrets-java-obfuscated.jar spoil`. +- If you want to understand the guessing flow, also run `java -jar wrongsecrets-java-obfuscated.jar wronganswer`. +- Compare the runtime behavior with what you found in the decompiled code to recover the secret. 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(); + } +}