hijacking picoCTF 2023 Solution

Published: April 26, 2023

Description

SSH into the server and discover a hidden Python script you can run with sudo. The script imports base64, and you have write permission on base64.py in the Python library. Edit that file to inject a system call, then run the script with sudo to read the flag.

SSH into the provided server using the given credentials.

List hidden files in your home directory to find the server script.

Check what sudo allows you to run.

bash
ssh <USER>@<HOST> -p <PORT>
bash
ls -la
bash
sudo -l

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Discover the hidden script and sudo permissions
    Observation
    I noticed the challenge description mentioned a hidden Python script in the home directory, which suggested running ls -la to reveal dot-files and then sudo -l to confirm what elevated permissions were available for that script.
    After logging in, list all files including hidden ones. There is a hidden file called .server.py in the home directory. Running sudo -l shows you can run python3 .server.py as root without a password.
    bash
    ls -la
    bash
    cat .server.py
    bash
    sudo -l
    What didn't work first

    Tried: Run sudo su or sudo bash to get a root shell directly.

    sudo -l will show that only the specific command /usr/bin/python3 /home/user/.server.py is permitted with NOPASSWD. Running sudo su or sudo bash produces 'Sorry, user is not allowed to run /bin/bash as root', because the sudoers entry is scoped to exactly one binary and one script path.

    Tried: Search for SUID binaries with find / -perm -4000 hoping to escalate without touching the Python script.

    No exploitable SUID binary is present on this instance. The intended privilege path is exclusively through the writable Python module, so SUID searches return only standard system binaries with no known local exploits.

    Learn more

    sudo -l lists the commands the current user can run with elevated privileges. The output shows something like (root) NOPASSWD: /usr/bin/python3 /home/user/.server.py, meaning you can run that specific Python script as root without entering a password.

    The script itself imports base64 and os, and does something innocuous. The key is that it imports Python standard library modules, and those module files might be writable by your user.

  2. Step 2
    Find the writable Python library file
    Observation
    I noticed that .server.py imports base64, and since we had write access to the filesystem as the SSH user, this suggested checking whether base64.py in the Python standard library had misconfigured world-writable permissions.
    Check the permissions on the base64.py file in the Python standard library. It turns out to be writable by your user, which means you can inject code that will run when the script imports it.
    python
    python3 -c 'import base64; print(base64.__file__)'
    bash
    ls -la /usr/lib/python3.*/base64.py
    What didn't work first

    Tried: Check permissions on os.py instead of base64.py, since the script also imports os.

    os.py is owned by root and not world-writable, so editing it fails with 'Permission denied'. The misconfiguration is specific to base64.py, which was intentionally left writable. Verifying permissions with ls -la on each imported module is necessary rather than assuming all imports are equally misconfigured.

    Tried: Look for a writable .pth file in site-packages to add a custom path that shadows the real base64 module.

    site-packages .pth files affect the module search path, but the sudo rule invokes python3 with an absolute path and a restricted environment. The standard library directory is searched before site-packages in this configuration, so a shadow module in site-packages would never be reached.

    Learn more

    Python's import statement searches directories in order and executes the module file when it is first imported. If a module file is world-writable, any user can add arbitrary code to it. That code runs with whatever privileges the importing script has.

    This is a Python library hijacking attack. The misconfiguration is leaving a standard library file world-writable, which lets an unprivileged user inject code that runs as root when a sudo-enabled script imports that module.

  3. Step 3
    Inject code into base64.py
    Observation
    I noticed base64.py was world-writable and that Python executes module files top-to-bottom on import, which suggested appending an os.system call to read /root/.flag.txt so it would fire automatically when the sudo script imported base64.
    Add an os.system() call at the end of base64.py to read and print the flag file. The existing code must remain intact so the import does not fail.
    python
    python3 -c 'import base64; print(base64.__file__)'
    bash
    # Append a system call to base64.py
    bash
    echo 'import os; os.system("cat /root/.flag.txt")' >> /usr/lib/python3.8/base64.py
    What didn't work first

    Tried: Overwrite base64.py entirely with just the os.system line, removing the original module code.

    The server script calls functions defined in base64.py. If the module body is gone, Python raises AttributeError or NameError when those functions are looked up after import, and the script may crash before printing the flag. Appending to the end of the file preserves all original definitions so the import completes cleanly.

    Tried: Use the hardcoded path /usr/lib/python3.8/base64.py without first checking the actual Python version installed.

    The instance may run Python 3.9 or 3.11, in which case the library lives under /usr/lib/python3.9/ or /usr/lib/python3.11/. Writing to a non-existent path silently creates a new file that Python never imports. Running 'python3 -c import base64; print(base64.__file__)' gives the exact path for the running interpreter.

    Learn more

    Appending code to the end of the module works because Python executes the module file from top to bottom on import. Adding a line at the end is safer than inserting at the beginning, since it does not break the existing function definitions that the script may rely on.

    The flag is stored in /root/.flag.txt. That file is owned by root and not readable by your user directly, which is why privilege escalation is needed.

  4. Step 4
    Run the script with sudo to trigger the injection
    Observation
    I noticed that sudo -l permitted exactly 'python3 .server.py' as root with no password, so running that command would cause Python to import the now-poisoned base64.py under root privileges and print the flag.
    Run the server script with sudo. Python imports base64, executes your injected os.system() call as root, and the flag is printed to stdout.
    bash
    sudo python3 .server.py

    Expected output

    picoCTF{pYth0nn_libraryH!j@CK!n9_...}
    What didn't work first

    Tried: Run python3 .server.py without sudo, expecting the injected code to still read /root/.flag.txt.

    Without sudo the process runs as the unprivileged user, so cat /root/.flag.txt returns 'Permission denied' because root's home directory is not world-readable. The privilege escalation via sudo is what makes the injected os.system() call execute with root access.

    Tried: Run sudo python3 -c 'import base64' directly to test the injection before running .server.py.

    This is a valid sanity check and will trigger the injection, but the sudo rule only permits /usr/bin/python3 /home/user/.server.py as the exact command. Running sudo python3 -c '...' may be blocked by the sudoers entry, producing 'command not allowed'. Use the permitted command path to trigger the payload.

    Learn more

    When Python runs the sudo script, it imports base64. The import mechanism executes base64.py, which now contains your injected call. Since the script runs as root via sudo, the os.system() call also runs as root, and cat /root/.flag.txt reads and prints the flag.

    The fix for this misconfiguration: never leave Python standard library files writable by unprivileged users. Standard library permissions should be root-owned and not world-writable. For more on Linux privilege escalation see Linux CLI for CTF.

Flag

Reveal flag

picoCTF{pYth0nn_libraryH!j@CK!n9_...}

The trailing hash is generated per instance. Write access to base64.py lets you inject code that runs as root when the sudo script imports the module.

Key takeaway

Library hijacking exploits the trust an interpreter places in module search paths and file permissions. When a privileged process imports a module from a location writable by a lower-privileged user, that user can inject arbitrary code that inherits the elevated privileges at import time. The same class of attack applies to Python sys.path ordering, Node.js require() resolution, LD_PRELOAD for shared libraries, and DLL search order hijacking on Windows.

Related reading

Want more picoCTF 2023 writeups?

Useful tools for Binary Exploitation

What to try next