Decompiling Java Applications with CFR
Starting a new series on Java application security. I do a lot of web app assessments and many of them involve Java backends — Spring apps, Tomcat deployments, fat JARs, the works. Being able to decompile and read the source is invaluable for finding vulnerabilities that black-box testing misses.
My go-to decompiler is CFR. It handles modern Java well, produces readable output, and runs from the command line which makes it easy to script. Let’s get into it.
Why Decompile?
When you’re pentesting a Java web app you often have access to:
- WAR/JAR files from the deployment
- Class files extracted from a running server
- Client-side Java applets (increasingly rare but still around)
- Android APKs (Dalvik bytecode converts back to Java)
Decompilation lets you see the actual business logic, find hardcoded secrets, trace authentication flows, and identify vulnerable code patterns. It’s the difference between guessing and knowing.
Getting CFR
CFR is a single JAR file with no dependencies. Grab it from the official site or use the Maven artifact:
wget https://www.benf.org/other/cfr/cfr-0.152.jar
That’s it. No installation, no setup.
Basic Usage
Decompile a single class file:
java -jar cfr-0.152.jar MyClass.class
Output goes to stdout by default. For a whole JAR:
java -jar cfr-0.152.jar application.jar --outputdir ./decompiled
This extracts and decompiles everything into a nice directory structure matching the package hierarchy.
Dealing with WAR Files
Web applications come packaged as WAR files. These are just ZIP archives with a specific structure:
myapp.war
├── META-INF/
├── WEB-INF/
│ ├── classes/ <- Compiled application code
│ ├── lib/ <- Dependency JARs
│ └── web.xml <- Deployment descriptor
└── (static files)
The interesting stuff is in WEB-INF/classes and sometimes WEB-INF/lib if the app bundles vulnerable dependencies.
Here’s my workflow:
# Extract the WAR
unzip -q myapp.war -d myapp_extracted
# Decompile the application classes
java -jar cfr-0.152.jar myapp_extracted/WEB-INF/classes --outputdir ./decompiled
# If you need to dig into a dependency
java -jar cfr-0.152.jar myapp_extracted/WEB-INF/lib/some-lib.jar --outputdir ./decompiled_lib
Useful CFR Options
CFR has a ton of options for handling edge cases. These are the ones I use most:
--outputdir <path>— Write output to files instead of stdout--caseinsensitivefs true— Helps on Windows where filenames are case-insensitive--silent true— Suppress progress messages--comments false— Skip the CFR comment header in output files--decodestringswitch false— Sometimes helps with obfuscated code--sugarasserts false— Can help with weird assertion patterns
For obfuscated code:
java -jar cfr-0.152.jar obfuscated.jar \
--outputdir ./decompiled \
--renameillegalidents true \
--renamesmallmembers 4
The renameillegalidents flag handles classes with names that aren’t valid Java identifiers (common in obfuscated code).
Reading the Output
CFR produces pretty clean Java. It handles:
- Lambda expressions
- Try-with-resources
- Switch expressions (newer Java)
- Generics (mostly)
- Inner classes
Sometimes you’ll see comments like /* synthetic */ or /* bridge */ — these are compiler-generated methods you can usually ignore.
If you see variable names like var1, var2, the original names were stripped. You’ll need to figure out what they represent from context.
A Quick Automation Script
I wrote a simple wrapper for bulk decompilation:
#!/bin/bash
# decompile.sh - Bulk decompile Java artifacts
CFR_JAR="$HOME/tools/cfr-0.152.jar"
OUTPUT_BASE="./decompiled"
for artifact in "$@"; do
name=$(basename "$artifact" | sed 's/\.[^.]*$//')
outdir="$OUTPUT_BASE/$name"
echo "[*] Decompiling $artifact -> $outdir"
if [[ "$artifact" == *.war ]]; then
tmpdir=$(mktemp -d)
unzip -q "$artifact" -d "$tmpdir"
java -jar "$CFR_JAR" "$tmpdir/WEB-INF/classes" \
--outputdir "$outdir" --silent true
rm -rf "$tmpdir"
else
java -jar "$CFR_JAR" "$artifact" \
--outputdir "$outdir" --silent true
fi
done
echo "[+] Done"
Usage:
./decompile.sh app1.war app2.jar lib.jar
What to Look For
Once you have decompiled source, you’re looking for security issues. In future posts I’ll cover specific vulnerability patterns, but here’s a quick hit list:
- Hardcoded credentials — Database passwords, API keys, encryption keys
- SQL queries — String concatenation instead of prepared statements
- Deserialization — ObjectInputStream.readObject() calls
- Path traversal — File operations with user input
- Command injection — Runtime.exec() or ProcessBuilder with user data
- XXE — XML parsers without secure configuration
- SSRF — HTTP clients making requests to user-controlled URLs
- Weak crypto — MD5, SHA1 for passwords, ECB mode, hardcoded IVs
Grep is your friend:
# Find potential SQL injection
grep -rn "createStatement\|executeQuery\|executeUpdate" ./decompiled
# Find hardcoded passwords
grep -rn -i "password\s*=\s*\"" ./decompiled
# Find deserialization
grep -rn "ObjectInputStream\|readObject" ./decompiled
# Find command execution
grep -rn "Runtime.getRuntime().exec\|ProcessBuilder" ./decompiled
CFR vs Other Decompilers
I’ve tried most of the popular Java decompilers:
- JD-GUI — GUI-based, good for quick looks but the decompiler is dated
- Procyon — Solid alternative to CFR, sometimes handles edge cases differently
- Fernflower — Built into IntelliJ, good quality but harder to use standalone
- JADX — My go-to for Android APKs, includes dex2jar conversion
- Krakatau — Handles weird bytecode that breaks other decompilers
I keep coming back to CFR because it’s reliable, actively maintained, and the CLI makes it easy to integrate into workflows. When CFR fails on something I’ll try Procyon or Krakatau.
Tips I’ve Learned
A few things that save time:
- Keep the original structure — Don’t flatten the package hierarchy. You’ll need it to understand the architecture.
- Decompile dependencies selectively — Don’t decompile all of
WEB-INF/libunless you need to. Focus on app code first. - Compare versions — If you can get multiple versions of an app, diff them to find recent changes (often security fixes).
- Check web.xml — The deployment descriptor shows URL mappings, filters, and servlets. Good roadmap for the codebase.
- Look at pom.xml or build.gradle — If included, these show dependencies and versions. Check for known CVEs.
What’s Next
This post covered the basics of getting Java source from compiled artifacts. In the next post we’ll dig into finding SQL injection vulnerabilities through code analysis — building patterns to identify injectable queries even in large codebases.
More to come!
If you enjoyed this post please consider subscribing to the feed!