Skip to main content

Safe Opener 2 picoCTF 2023 Solution

Analyze a compiled Java class file to recover a flag that was left in the code.

Published: April 26, 2023Updated: August 25, 2026

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 1Strings is the fast path
    Observation
    The download is a compiled SafeOpener.class, not source. Java class files keep every string literal verbatim in the constant pool as plain UTF-8, so strings plus grep should expose the flag without decompiling anything.
    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 constant pool holds dozens of JVM-internal strings such as Ljava/lang/String;, Code, and StackMapTable before any user literal shows up. Grepping the picoCTF prefix lands on one line; scrolling the raw output by eye is slow and easy to miss.

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

    file only names the format, and hexdump shows raw bytes without the UTF-8 pass that strings does. The flag bytes are in the dump, but interleaved with pool metadata, so reading them off by eye is unreliable. The printable-run filter in strings is 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 2Peek the constant pool with javap
    Observation
    strings gives the flag value with no context about which method uses it. javap ships with the JDK, disassembles the bytecode, and annotates each ldc with the constant it loads, so it answers that.
    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 method signatures and no bytecode. Without -p, private members and their constants stay hidden. The flag is loaded inside a private method, so you need both flags to see the ldc that references it.

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

    -verbose dumps the whole constant pool in raw index form, which does contain the flag, but it runs for screens. -c -p gives the same value through annotated ldc lines without the class metadata. Both work; -verbose is just slower to scan.

    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 3Optional: decompile to Java source
    Observation
    strings and javap both show bytecode without the control flow around it. jd-gui rebuilds near-original Java source, which makes the password comparison readable at a glance, and the flag literal falls out of a 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: a class is only processed when its tab is opened or when Save All Sources runs. Save too early and the .java file comes out empty or a bare skeleton. Click the class in the tree, wait for the source pane, then save.

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

    Without -o, grep prints the whole line, including the Java statement wrapped around the flag. That reads fine, but the closing brace and the closing quote sit together, so a copy-paste drags the quote along as a typo. The -oE pattern extracts just the flag token.

    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 keep string literals verbatim in the constant pool because the runtime has to resolve them at execution time, so grepping the binary works no matter how the class and method names are obfuscated. The same holds for Android APKs, compiled .pyc files, and other managed runtimes. Tools like javap, jadx, and strings exploit that exposure to recover secrets without ever running the program.

Related reading

Useful tools for Reverse Engineering

Where to go next