Series
Tenable Zero Day Assessment — View all write-ups

Challenge

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.

Initial Reconnaissance

The first step was decompiling the JAR using JD-GUI. Loading tenable_cli.jar reveals the following methods in the CLI class:

JD-GUI method list for CLI class

main()

main() method decompiled

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()

command_loop() method decompiled

command_loop() dispatches input to one of the following handlers based on a prefix match:

  • ping <ipv4 address>
  • get <URL>
  • lsnotes
  • mknote <content>
  • readnote <name>
  • version
  • exit

Note that dispatch uses a prefix check rather than exact matching. This has implications that become clear when examining the hidden command described later.

Vulnerability 1 — Remote Command Execution: do_ping()

do_ping() method decompiled

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:

do_ping command injection — output of injected id command

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.

PoC: PoCs/pwn_c1_do_ping.py

Vulnerabilities 2–4 — Arbitrary File Read, SSRF, and Denial of Service: do_get()

do_get() method decompiled

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:

do_get normal HTTP request

Arbitrary File Read

By supplying a file:// URI, an attacker can read arbitrary files from the server's filesystem:

Arbitrary file read — /etc/passwd via file:// URI

PoC: PoCs/pwn_c1_do_get_arbitrary_file_read.py

Server-Side Request Forgery (SSRF)

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:

SSRF — reaching localhost-only service

PoC: PoCs/pwn_c1_do_get_ssrf.py (requires a service running on port 9001 on the target host)

Denial of Service via Memory Exhaustion

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:

Large response payload triggering OOM Server crash from OOM

PoC: PoCs/pwn_c1_do_get_DoS.py

Vulnerabilities 5–6 — Denial of Service: do_make_note()

do_list_notes and do_make_note decompiled

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:

  • Heap exhaustion: A single large payload causes the JVM to run out of heap space and crash the process.
  • Disk exhaustion: Repeated requests with large payloads can fill the target filesystem entirely.

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.pyexercise caution before running.

Vulnerability 7 — Directory Traversal: do_read_note()

do_read_note() decompiled

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:

Directory traversal via do_read_note

PoC: PoCs/pwn_c1_do_read_note_directory_traversal.py

Vulnerability 8 — Hidden Backdoor: secret_shell

A 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.

secret_shell branch in command_loop do_shell method — the backdoor handler Backdoor exploit in action

PoC: PoCs/pwn_c1_mikejoooooonnnnneeesss.py

Summary of Findings

  • Remote Command Executiondo_ping(): unanchored regex allows shell metacharacter injection
  • Arbitrary File Readdo_get(): no URI scheme allowlist permits file:// reads
  • Server-Side Request Forgerydo_get(): server-issued requests expose internal services
  • Denial of Service (memory)do_get(): unbounded response loading exhausts JVM heap
  • Denial of Service (memory)do_make_note(): unbounded note content exhausts JVM heap
  • Denial of Service (disk)do_make_note(): repeated large writes exhaust filesystem capacity
  • Directory Traversal / Arbitrary File Readdo_read_note(): incomplete prefix check bypassed with ./
  • Backdoor Shell Accesssecret_shell: hidden command grants unauthenticated interactive shell

Challenge file: tenable_cli.jar