Challenge 1: tenable_cli.jar
===
As much as it pains me to admit it, not every tool is written in C or C++.
It's important for a vulnerability hunter to be able to handle other languages:
Python, Lua, Java, Go, js, etc. This challenge is written in Java. For this
task, find as many vulnerabilities in tenable_cli.jar as you can. Do not
overthink this. If you are hunting outside of CLI.class then you've gone too far.
Deliverables:
1. A write up describing how you approached the hunt and found the vulnerabilities.
2. PoC scripts for each vulnerability. Please provide individual scripts for each PoC.
The first step was decompiling the JAR using JD-GUI. Loading tenable_cli.jar reveals the following methods in the CLI class:
main() begins by verifying the host OS is Linux, then creates the directory /tmp/notes_dir. It binds a TCP socket listener on port 1270 (all interfaces), sends a prompt to each incoming connection, reads input, and passes it to command_loop().
command_loop() dispatches input to one of the following handlers based on a prefix match:
ping <ipv4 address>get <URL>lsnotesmknote <content>readnote <name>versionexitNote that dispatch uses a prefix check rather than exact matching. This has implications that become clear when examining the hidden command described later.
do_ping()
do_ping() constructs a shell command from attacker-controlled socket input. The method splits input on a single space and validates that exactly two parts are present, then applies a regex intended to enforce IPv4 address format. The regex is fatally flawed: it only asserts that the input begins with dotted-quad notation, omitting a trailing $ anchor. Any input appended after a valid IP prefix will pass validation.
Supplying 1.1.1.1;id as the address argument trivially bypasses the check and results in shell command injection:
The output of id confirms successful exploitation. Because the socket listens on all interfaces with no authentication, this constitutes an unauthenticated remote command execution (RCE) vulnerability.
do_get()
do_get() accepts a URL from the socket, constructs a URI object, and calls openStream() to issue a GET request. The critical omission is any form of scheme validation — the code enforces no allowlist and accepts arbitrary URI schemes, including file://.
Normal operation for context:
By supplying a file:// URI, an attacker can read arbitrary files from the server's filesystem:
PoC: PoCs/pwn_c1_do_get_arbitrary_file_read.py
Since requests are issued from the server's network context, the method can be directed at internal services not exposed externally. A service listening only on loopback, for example, is fully accessible:
PoC: PoCs/pwn_c1_do_get_ssrf.py (requires a service running on port 9001 on the target host)
Pointing do_get() at a URL serving a large response causes the Java process to attempt loading the entire body into memory. With a sufficiently large payload, the JVM heap is exhausted and the process crashes:
PoC: PoCs/pwn_c1_do_get_DoS.py
do_make_note()
do_list_notes() runs ls on /tmp/notes_dir and writes the result back to the socket. No directly exploitable issue is apparent in isolation.
do_make_note() is intended to strip the mknote command prefix (via substring(7)) and write the remaining content to a timestamped file under /tmp/notes_dir. In practice, the substring operation does not behave as expected: the full input including the command prefix is written to disk rather than just the intended content. This allows an attacker to write files of effectively unbounded size (up to JVM heap capacity), enabling two denial-of-service primitives:
PoC (heap exhaustion): PoCs/pwn_c1_do_make_note_OOM_DoS.py
PoC (disk exhaustion): PoCs/pwn_c1_do_make_note_exhaust_disk_space_DoS.py — exercise caution before running.
do_read_note()
do_read_note() constructs a file path from the caller-supplied note name and reads it back. An attempt is made to prevent directory traversal by checking whether the path starts with .. or /. However, the check is incomplete: a path beginning with ./ passes validation while still enabling traversal. The path ./../../../../../../../etc/passwd reads the system password file:
PoC: PoCs/pwn_c1_do_read_note_directory_traversal.py
secret_shellA full review of command_loop() reveals an undocumented command: sending secret_shell mikejones invokes a hidden shell handler not listed in any help output. This appears to be a deliberate backdoor, granting unauthenticated shell access to any caller who knows the trigger string.
PoC: PoCs/pwn_c1_mikejoooooonnnnneeesss.py
do_ping(): unanchored regex allows shell metacharacter injectiondo_get(): no URI scheme allowlist permits file:// readsdo_get(): server-issued requests expose internal servicesdo_get(): unbounded response loading exhausts JVM heapdo_make_note(): unbounded note content exhausts JVM heapdo_make_note(): repeated large writes exhaust filesystem capacitydo_read_note(): incomplete prefix check bypassed with ./secret_shell: hidden command grants unauthenticated interactive shellChallenge file: tenable_cli.jar