Skip to main content

hijacking picoCTF 2023 Solution

A Linux privilege escalation challenge involving a privileged script with an exploitable code loading flaw.

Published: April 26, 2023Updated: August 25, 2026

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 1Discover the hidden script and sudo permissions
    Observation
    The description mentions a hidden Python script in the home directory. ls -la reveals the dotfiles, and sudo -l shows what elevated permissions exist for it.
    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 shows only one command permitted without a password, that exact interpreter and script path. sudo su or sudo bash is refused, because the sudoers entry is scoped to a single binary and a single script.

    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 2Find the writable Python library file
    Observation
    .server.py imports base64, and the SSH user has write access to the filesystem. Worth checking whether base64.py in the standard library was left world-writable.
    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 root-owned and not world-writable, so editing it is denied. The misconfiguration is specific to base64.py, left writable on purpose. Check permissions on each imported module rather than assuming they are all equally exposed.

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

    A .pth file in site-packages does affect the module search path, but the sudo rule invokes the interpreter by absolute path in a restricted environment. Here the standard library is searched before site-packages, so a shadow module there is never 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 3Inject code into base64.py
    Observation
    base64.py is world-writable, and Python runs a module top to bottom on import. Append an os.system call reading the root flag file, and it fires the moment the sudo script imports 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. Wipe the module body and Python raises an AttributeError or NameError when those lookups happen, so the script may crash before printing anything. Append to the end of the file, and every original definition survives the import.

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

    The instance may run 3.9 or 3.11, which puts the library under a correspondingly named directory. Write to a path that does not exist and you silently create a file Python never imports. Ask the interpreter itself where the module lives and use that path.

    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 4Run the script with sudo to trigger the injection
    Observation
    sudo -l permits exactly that one script as root with no password. Running it makes Python import the poisoned base64 module with 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 fair sanity check and does trigger the injection, but the sudo rule permits one exact command. An inline -c invocation may be refused by the sudoers entry as a command not allowed. Use the permitted path to fire 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.

Interactive tools
  • Regex TesterTest regular expressions against a string with live match highlighting, flag toggles, and common CTF pattern shortcuts.
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • 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{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 its module search path and in file permissions. When a privileged process imports a module from a location a lower-privileged user can write, that user injects code which inherits the elevated privileges at import time. The same class covers Python's sys.path ordering, Node's require resolution, LD_PRELOAD for shared libraries, and DLL search order hijacking on Windows.

Related reading

Useful tools for Binary Exploitation

Where to go next