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.
wget https://artifacts.picoctf.net/c/290/SafeOpener.classstrings SafeOpener.class | grep picojavap -c -p SafeOpenerSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Strings is the fast pathObservationI 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.bashstrings 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 picohits that table directly.String constants survive obfuscation because the runtime needs them. ProGuard and similar tools rename classes to
a.classand methods toa(), but a literal like"picoCTF{...}"must remain intact forString.equalsto work, so it stays in the constant pool. That is why grepping for the flag prefix beats most light obfuscation.Step 2
Peek the constant pool with javapObservationI 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.bashjavap -c -p SafeOpenerWhat 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
javapis the standard JDK disassembler.-cshows method bytecode and-pincludes private members. The output is the closest thing to running "objdump on Java": everyldcinstruction loads a string from the constant pool, and each load is annotated with the literal it resolves to. So scanning the disassembly forldc "picoCTFgives you both the value and the method that uses it.Step 3
Optional: decompile to Java sourceObservationI 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.bashwget https://github.com/java-decompiler/jd-gui/releases/download/v1.6.6/jd-gui-1.6.6.jarbashjava -jar jd-gui-1.6.6.jar SafeOpener.classbashgrep -oE 'picoCTF[^"]*' SafeOpener.javaWhat 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 isgrep -oE 'picoCTF[^"]*' SafeOpener.java:-oprints 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 oldcut -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.