HTB: Coffee Invocation Challenge
Coffee Invocation - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | Coffee Invocation |
| Category | Misc |
| Difficulty | Medium |
| Author | d3vn0mi |
Description
A “crazy conspiracy theorist intern” has locked everyone out of the office coffee machine, convinced aliens are after its “out of this world” secret recipe. The goal is to reverse-engineer the machine’s firmware, recover the hidden password gating the secret menu option, and extract the flag it protects.
Solution
The distributed binary, coffee_invocation, turned out to be a 32 KiB stripped ELF that dynamically links against libjvm.so — the challenge embeds a full JVM via the JNI Invocation API rather than being a plain native binary. At startup it calls JNI_CreateJavaVM, then DefineClasses two Java classes (Verify1, Verify2) that are carried inside the ELF as raw CAFEBABE class-file blobs, and finally calls RegisterNatives to hijack java/lang/Shutdown.halt0 — meaning System.exit(n) inside the embedded Java code never actually terminates the process; instead n is routed into a native hook and used as an internal progress counter.
The real trap is in how the native side tampers with the JVM’s boxing caches: it uses SetByteField / SetShortField / SetCharField to overwrite the cached objects backing Byte.valueOf, Short.valueOf, and Character.valueOf with attacker-controlled substitution tables stored in .data, and it swaps the static Boolean.TRUE / Boolean.FALSE objects so every autoboxed boolean in the embedded Java code evaluates to the opposite of what it looks like it should. The result is Java source (visible via decompilation) that reads like an innocuous sort/stream/compare pipeline but actually performs a completely different comparison because of the poisoned identity caches.
Extracting and decompiling Verify1/Verify2 with jadx showed a complexSort(String, Boolean) routine built around a scrambled alphabet embedded in the class file, streamed through Arrays.sort / Collectors.joining. Combined with a 26-byte ciphertext blob found in the binary’s .data section, a simple byte-wise algebraic relation recovered the first half of the required password. A GDB Python harness plus a small bash “oracle” wrapper (setting LD_LIBRARY_PATH to the system libjvm.so so the stripped binary could actually run) was used to validate password candidates against the machine’s hidden menu option (“secret coffee”) until the full 52-character secret was confirmed correct. The flag is then simply assembled by the program as HTB{REDACTED} + argv[1][0:52]+}`.
Note: the challenge artifact as originally shipped in the workspace was 0 bytes (corrupted download); it was re-fetched directly from HTB and extracted with the standard unzip -P hackthebox password to get a working copy. A write-up that had been auto-injected into context for a different target (the Delivery Linux machine) was also identified as unrelated and discarded — analysis proceeded strictly from the recovered binary.
Key Steps
1. Recover a working artifact
# Shipped download was 0 bytes — re-fetch challenge 308's zip via the HTB API# and extract with HTB's standard archive password.mkdir -p /tmp/ci && cd /tmp/ciunzip -o -P hackthebox /tmp/dl_<session>.zip# -> real 32 KiB stripped ELF: coffee_invocation, linked against libjvm.so2. Static triage
# Confirm it's a JNI Invocation API program, not a plain native binaryreadelf -d coffee_invocation | grep NEEDED# Shared library: [libjvm.so]# Shared library: [libc.so.6]
strings -n 3 -t x coffee_invocation | tail -60# Menu strings: "1. Normal Coffee", "2. Espresso", "3. [REDACTED]", "4. Exit"# "Can't access secret coffee without providing the password!"# JNI class/method names: Verify1, Verify2, complexSort, java/lang/Shutdown, halt03. Carve and decompile the embedded Java classes
# Two CAFEBABE-magic class files are embedded directly in the ELF's data.# Carve them out by offset, then decompile with jadx.python3 - <<'EOF'd = open('coffee_invocation', 'rb').read()import refor m in re.finditer(b'\xCA\xFE\xBA\xBE', d): print(hex(m.start()))EOF
jadx -d jadx_out Verify1.class Verify2.class4. Trace the native bootstrap in main
; JNI Invocation API bootstrapcall 1030 <JNI_CreateJavaVM@plt>...call 2121 <...> ; DefineClass(Verify1) / DefineClass(Verify2)...call 1fb9 <...> ; RegisterNatives on java/lang/Shutdown.halt0RegisterNatives retargets Shutdown.halt0, so any System.exit(n) call inside the embedded Java classes is silently redirected into native code that treats n as a stage counter rather than actually exiting.
5. Identify the boxing-cache poisoning trick
; Native code overwrites the JDK's internal Byte/Short/Character caches; and swaps the Boolean.TRUE/FALSE statics, using substitution tables; stored in .data — so autoboxed values compare/equal "wrong" from the; perspective of the plainly-readable decompiled Java.call SetByteFieldcall SetShortFieldcall SetCharField6. Recover the ciphertext and reverse the byte transform
# Ciphertext recovered from the binary at 0x7480 (26 bytes)t = b'~PL{A;PL{?;:=|PIC{HzP:A;~x'
# Algebraic relation derived from the disassembly: src[i] = (-tgt[i] - 0x51) & 0xffs = bytes(((-c - 0x51) & 0xff) for c in t)print(s) # recovers the first half of the required password7. Build an oracle to validate the full password
cat > oracle.sh <<'EOF'#!/bin/bashL=/usr/lib/jvm/java-17-openjdk-amd64/lib/serverLD_LIBRARY_PATH=$L ./coffee_invocation <<INPUT3$14INPUTEOFchmod +x oracle.sh
./oracle.sh '<recovered_52_char_password>'# Confirms the secret-menu option accepts the reconstructed password8. Assemble and submit the flag
flag = "HTB{REDACTED} + argv[1][0:52] + "}"HTB{REDACTED}Submitted to HTB challenge #308 → {'message': 'Congratulations!'}, solve confirmed.
Tools Used
unzip(HTB’s standardhackthebox-password archive)strings,objdump,readelf— static ELF/string/disassembly triagejadx— decompiling the embeddedCAFEBABEJava class blobspython3— byte carving, offset parsing, and the XOR/subtraction algebra recoverygdb(Python scripting API) — instrumentingDefineClass/RegisterNativesat runtime- Bash oracle wrapper +
LD_LIBRARY_PATHpointed at the systemlibjvm.so— running the otherwise-unlaunchable stripped binary - HTB API (via project tooling) — flag submission and solve verification
Key Learnings
- JNI Invocation API as an obfuscation layer. Embedding a full JVM inside a stripped native ELF (via
JNI_CreateJavaVM,DefineClass,RegisterNatives) turns reversing into a two-layer problem: the native C wrapper and the embedded Java bytecode have to be analyzed together, since control genuinely crosses between them. RegisterNativescan hijack core JDK internals. Rebindingjava/lang/Shutdown.halt0intercepts everySystem.exit()call from the embedded Java, letting a challenge author repurpose an “innocent” JDK call as custom native logic without the Java source looking suspicious.- Boxing-cache poisoning as an anti-reversing trick. Using
SetByteField/SetShortField/SetCharFieldto overwrite the JDK’sByte/Short/Character.valueOfcaches, and swappingBoolean.TRUE/FALSE, silently flips the outcome of==,.equals(), and comparator logic in decompiled Java that otherwise reads as completely straightforward — decompiled source alone can’t be trusted without checking whether native code has touched the runtime’s object identity tables. - Verify artifact integrity before trusting supplied context. The shipped binary was 0 bytes, and an unrelated write-up (for a different machine entirely) had been injected into the working context — re-fetching the real artifact directly from HTB and working strictly from it avoided wasting effort chasing the wrong target.
- Dynamic oracles beat pure static reasoning for JVM-embedded binaries. Once the
libjvm.sodependency was satisfied viaLD_LIBRARY_PATH, a small scripted wrapper let candidate passwords be validated by actually running the binary, which was far faster than fully hand-decoding the poisoned substitution tables statically.