HTB: Coffee Invocation Challenge

Coffee Invocation - HackTheBox Challenge Writeup

Challenge Information

FieldValue
NameCoffee Invocation
CategoryMisc
DifficultyMedium
Authord3vn0mi

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

Terminal window
# 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/ci
unzip -o -P hackthebox /tmp/dl_<session>.zip
# -> real 32 KiB stripped ELF: coffee_invocation, linked against libjvm.so

2. Static triage

Terminal window
# Confirm it's a JNI Invocation API program, not a plain native binary
readelf -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, halt0

3. Carve and decompile the embedded Java classes

Terminal window
# 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 re
for m in re.finditer(b'\xCA\xFE\xBA\xBE', d):
print(hex(m.start()))
EOF
jadx -d jadx_out Verify1.class Verify2.class

4. Trace the native bootstrap in main

; JNI Invocation API bootstrap
call 1030 <JNI_CreateJavaVM@plt>
...
call 2121 <...> ; DefineClass(Verify1) / DefineClass(Verify2)
...
call 1fb9 <...> ; RegisterNatives on java/lang/Shutdown.halt0

RegisterNatives 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 SetByteField
call SetShortField
call SetCharField

6. 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) & 0xff
s = bytes(((-c - 0x51) & 0xff) for c in t)
print(s) # recovers the first half of the required password

7. Build an oracle to validate the full password

cat > oracle.sh <<'EOF'
#!/bin/bash
L=/usr/lib/jvm/java-17-openjdk-amd64/lib/server
LD_LIBRARY_PATH=$L ./coffee_invocation <<INPUT
3
$1
4
INPUT
EOF
chmod +x oracle.sh
./oracle.sh '<recovered_52_char_password>'
# Confirms the secret-menu option accepts the reconstructed password

8. 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 standard hackthebox-password archive)
  • strings, objdump, readelf — static ELF/string/disassembly triage
  • jadx — decompiling the embedded CAFEBABE Java class blobs
  • python3 — byte carving, offset parsing, and the XOR/subtraction algebra recovery
  • gdb (Python scripting API) — instrumenting DefineClass/RegisterNatives at runtime
  • Bash oracle wrapper + LD_LIBRARY_PATH pointed at the system libjvm.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.
  • RegisterNatives can hijack core JDK internals. Rebinding java/lang/Shutdown.halt0 intercepts every System.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/SetCharField to overwrite the JDK’s Byte/Short/Character.valueOf caches, and swapping Boolean.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.so dependency was satisfied via LD_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.