Safe Opener 2 picoCTF 2023 Solution

Published: April 26, 2023

Description

A compiled SafeOpener.class supposedly reveals the forgotten safe code. Either strings analysis or Java decompilation uncovers the embedded flag.

Fast path: strings on the .class file. The flag is in the constant pool as plain UTF-8.

Constant-pool peek without a GUI: javap -c -p SafeOpener.

If you want decompiled Java, jd-gui or jadx renders SafeOpener.java.

bash
wget https://artifacts.picoctf.net/c/290/SafeOpener.class
bash
strings SafeOpener.class | grep pico
bash
javap -c -p SafeOpener

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Static recovery from a Java .class is mostly about constant-pool inspection. For Ghidra-style disassembly background see the Ghidra reverse engineering guide; for the broader CLI workflow, the Linux CLI for CTF guide covers grep, strings, and the rest.
  1. Step 1
    Strings is the fast path
    Observation
    I noticed the challenge provides a compiled SafeOpener.class binary rather than source code, and Java .class files store all string literals verbatim in the constant pool as plain UTF-8, which suggested that the strings utility combined with grep would expose the flag without any decompilation.
    Java string constants live in the .class constant pool as plain UTF-8, so strings + grep reveals the flag in one command.
    bash
    strings SafeOpener.class | grep -oE 'picoCTF\{[^"}]*\}'

    Expected output

    picoCTF{SAf3_0p3...8a993}
    What didn't work first

    Tried: Run strings SafeOpener.class without grep, then scan the output manually.

    The .class constant pool contains dozens of JVM-internal strings (Ljava/lang/String;, Code, StackMapTable, etc.) before the user-defined literals appear. Grepping for the picoCTF prefix narrows the match to one line instantly; scrolling raw strings output by eye wastes time and risks missing the flag if it is preceded by an adjacent pool entry.

    Tried: Run file SafeOpener.class or hexdump -C SafeOpener.class to inspect the flag.

    file only identifies the format (Java class data, version N) and hexdump shows raw bytes without the UTF-8 decoding pass that strings performs. The flag bytes are present in the hex dump but interleaved with pool metadata, making visual recovery unreliable. The strings tool's minimum-run-of-printable-chars filter is exactly what makes the extraction clean.

    Learn more

    Java .class files are JVM bytecode. Unlike stripped native binaries, .class files retain a great deal of structure: class names, method names, field names, and string constants all live in the constant pool, a UTF-8 table at the top of the file. strings SafeOpener.class | grep pico hits that table directly.

    String constants survive obfuscation because the runtime needs them. ProGuard and similar tools rename classes to a.class and methods to a(), but a literal like "picoCTF{...}" must remain intact for String.equals to work, so it stays in the constant pool. That is why grepping for the flag prefix beats most light obfuscation.

  2. Step 2
    Peek the constant pool with javap
    Observation
    I noticed that strings extraction gives just the flag value without context, so I wanted to confirm which method uses it; javap ships with the JDK and disassembles the bytecode while annotating each ldc instruction with the constant-pool literal it loads, making it the natural next step.
    javap -c -p prints disassembled bytecode plus the constant pool. No GUI needed and it ships with the JDK.
    bash
    javap -c -p SafeOpener
    What didn't work first

    Tried: Run javap SafeOpener with no flags to look for the flag.

    Without -c, javap prints only method signatures (public void checkPassword(String)) and no bytecode. Without -p, private members and their string constants are hidden. The flag is loaded inside a private method, so both -c and -p are required to see the ldc instruction that references the constant-pool entry holding the flag.

    Tried: Run javap -verbose SafeOpener expecting cleaner flag output than -c -p.

    -verbose dumps the full constant pool table in raw index form (e.g., #7 = Utf8 picoCTF{...}) which does contain the flag, but the output is many screens long. -c -p gives the same value through the annotated ldc lines without the noise of class file metadata. Either works, but -verbose is slower to scan and can mislead you into editing pool indices instead of reading the string literal.

    Learn more

    javap is the standard JDK disassembler. -c shows method bytecode and -p includes private members. The output is the closest thing to running "objdump on Java": every ldc instruction loads a string from the constant pool, and each load is annotated with the literal it resolves to. So scanning the disassembly for ldc "picoCTF gives you both the value and the method that uses it.

  3. Step 3
    Optional: decompile to Java source
    Observation
    I noticed that the strings and javap approaches show raw bytecode without the surrounding control flow; jd-gui reconstructs near-original Java source from SafeOpener.class so the password comparison logic becomes immediately readable, and the embedded flag literal can be extracted with a simple grep.
    jd-gui is not in the standard Ubuntu repos, so grab the self-contained jar from its GitHub releases. Once you have the .java file, grep -oE 'picoCTF[^"]*' extracts the literal cleanly.
    bash
    wget https://github.com/java-decompiler/jd-gui/releases/download/v1.6.6/jd-gui-1.6.6.jar
    bash
    java -jar jd-gui-1.6.6.jar SafeOpener.class
    bash
    grep -oE 'picoCTF[^"]*' SafeOpener.java
    What didn't work first

    Tried: Open jd-gui and try to save the decompiled source using File > Save All Sources before the GUI fully loads.

    jd-gui decompiles lazily - classes are only processed when their tab is opened or when Save All Sources is triggered. If you trigger the save before the class has been rendered, the output .java file can be empty or contain only a skeleton. Click the class in the tree first so the decompiled source appears in the pane, then save.

    Tried: Run grep 'picoCTF' SafeOpener.java without the -oE flag and the character-class stop pattern.

    Without -o, grep prints the full line containing the flag literal, which also includes the surrounding Java statement (e.g., if (password.equals("picoCTF{...}"))). That is readable, but the closing brace of the flag and the closing quote of the string are on the same line, so copy-pasting introduces the trailing quote as a typo. The -oE 'picoCTF[^"]*' pattern extracts just the flag token with no surrounding syntax.

    Learn more

    jd-gui is a graphical Java decompiler that reconstructs near-original Java source from bytecode. Variable names are usually replaced with synthetic identifiers like paramString1, but string literals stay intact, which is all this challenge needs.

    Once you have the decompiled SafeOpener.java, the surgical grep is grep -oE 'picoCTF[^"]*' SafeOpener.java: -o prints only the match (not the whole line) and the character class [^"]* stops at the closing quote of the literal. That is more robust than splitting on backslashes (the old cut -d "\"" recipe), which breaks the moment the source contains escape sequences.

    For heavier lifting, Ghidra handles both .class files and native binaries with one decompiler view, and bytecode-viewer bundles multiple Java decompilers (jd, jadx, CFR, Procyon) so you can compare their output side-by-side when one of them produces garbled control flow.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{SAf3_0p3...8a993}

No dynamic execution is required; the challenge is purely static analysis.

Key takeaway

Java .class files retain string literals verbatim in the constant pool because the runtime must resolve them at execution time, making grepping the binary a reliable first-pass attack regardless of class or method name obfuscation. This same principle extends to Android APKs (which are zip archives of .dex bytecode), compiled Python .pyc files, and many other managed runtimes where the source language needs string constants accessible at runtime. Static analysis tools like javap, jadx, and strings exploit this structural exposure to recover secrets without ever running the program.

Related reading

Want more picoCTF 2023 writeups?

Useful tools for Reverse Engineering

What to try next