TL;DR

This blog is about a not new but very interesting vulnerability which is familiar to Prototype Pollution in Javascript but in Python.

Prototype Pollution

“Prototype pollution is a JavaScript vulnerability that enables an attacker to add arbitrary properties to global object prototypes, which may then be inherited by user-defined objects.” from PortSwigger. Prototype pollution itself doesn’t often cause much of a trouble, but when chains with other vulnerabilities, it definitely will. As this research does not dig deep into prototype pollution, I will explain the exploit as this simple concept: In javascript, objects can inherit attributes (properties) from others via “Object Prototype”.

let a = {test: "this is a test"};
a.__proto__.polluted = "polluted";
let b = {}
console.log(b.polluted) // return polluted

Why this is an issue? For instance:

var username = "Hacker";
var password = "Evil";
var users = {"admin": "REDACTED"}
function try_login(username, password) {
    if (username in users && users[username] === password) {
        console.log("Logged in");
    } else {
        console.log("Not logged in");
    }
}
try_login(username, password) // return Not logged in

How can we log in? If we can modify the Object.__proto__.Hacker = "Evil", then we can log in because the object users inherit the attributes Hacker:

// continue with the above snippet
var myObject = {};
myObject.__proto__[username] = password;
try_login(username, password) // return Logged in

This is a more realistic example:

// continue with the above snippet
function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (!target[key]) {
                target[key] = {};
            }
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

var theme = {background: "white", frontground: "black"}
merge(theme, JSON.parse('{"__proto__": {"Hacker2": "Evil2"}}'));
username = "Hacker2";
password = "Evil2";
try_login(username, password) // return Logged in

That is Prototype Pollution. There are more Prototype Pollution payloads, which help us achieve similar effect, and when this combines with the right gadget, we can exploit so much more, even RCE. To sum up, the vulnerability is that if we can control attributes of an object in javascript (inject things NOT initialized into it), we can do a Prototype Pollution. In real life, this vuln is found when we do insecure object recursive merge, property definition by path, or object clone, reference this.

Class Pollution in Javascript

Background

With that logic in mind, let’s take a look into Python. Python does not have Prototype, it should be safe right? Sadly, no.

Analysis

Consider this code snippet:

class Test:
    my_value = "test"
    def __init__(self):
        print("Hello from Test")

class SmallTest(Test):
    def __init__(self):
        print("Hello from SmallTest")

class mySmallTest(SmallTest):
    my_small_value = "small"
    def __init__(self):
        print("Hello from mySmallTest")

class MediumTest(Test):
    def __init__(self):
        print("Hello from MediumTest")
    def health_check(self):
        import os
        print(os.system(f"echo {self.my_value}"))

And try this:

# continue with above snippet
test = mySmallTest()
print(test.my_value) # test
print(test.my_small_value) # small
test.my_small_value = "smaller"
test.my_value = "tester"
print(test.my_value) # tester
print(test.my_small_value) # smaller
test2 = mySmallTest()
print(test2.my_value) # test
print(test2.my_small_value) # small

Can we inject the class mySmallTest by inject object test? When checking the attributes of object test, there is a very interesting attribute called __class__. What if I change the attribute my_small_value of test.__class__?

# continue with above snippet
print(test.__class__) # <class '__main__.mySmallTest'>
test.__class__.my_small_value = "smaller"
print(test.my_small_value) # smaller
test2 = mySmallTest()
print(test2.my_small_value) # smaller

So, we can change the class attributes by changing via <object>.__class__.<attribute>. How about my_value which is inherit from class Test

# continue with above snippet
test3 = Test()
test.__class__.my_value = "tester"
print(test.my_value) # tester
print(test2.my_value) # tester
print(test3.my_value) # test

Nah, we cannot change it. Or is it! Investigate further, the __class__ has attributes __base__ which returns it parents. This means that we can access and modify the parents attributes.

# continue with above snippet
print(test.__class__.__base__) # <class '__main__.SmallTest'>
print(test.__class__.__base__.__base__) # <class '__main__.Test'>
smallTest = SmallTest()
test.__class__.__base__.my_value = "tester from SmallTest"
print(test3.my_value) # test
print(smallTest.my_value) # tester from SmallTest
test.__class__.__base__.__base__.my_value = "tester from Test"
print(test3.my_value) # tester from Test
print(smallTest.my_value) # tester from SmallTest

So in that example snippet, we can execute arbitrary payload

# continue with above snippet
test.__class__.__base__.__base__.my_value = "evil && whoami"
mediumTest = MediumTest()
mediumTest.health_check() # Try this yourself, I won't delete your system :)

Accessing the globals

Moreover, we can access global attributes. The object test has a function called __init__ which has __globals__. In this case __init__ has __globals__ because we provide it our code, not default, so it has the __globals__ attribute, this holds for whatever functions we created ourselves, and not for defaults one.

# continue with above snippet
our_globals_var = "CLEAN"
print(test.__init__.__globals__)
print(our_globals_var) # CLEAN
test.__init__.__globals__['our_globals_var'] = "POLLUTED"
print(our_globals_var) # POLLUTED

Back to the login example mention in Prototype Pollution, we can kinda bypass by Class Pollution in Python.

class Theme:
    def __init__(self):
        pass

users = {"admin": "REDACTED"}

def try_login(username, password):
    if username in users and users[username] == password:
        print("Logged in")
    else:
        print("Not logged in")

username = "Hacker"
password = "Evil"
try_login(username, password)
myObject = Theme()
myObject.__init__.__globals__['users'][username] = password
try_login(username, password)

A more realistic example:

# continue with above snippet
def merge(target, source):
    if not isinstance(source, dict):
        return
    for key in source:
        source_value = source[key]
        if isinstance(source_value, dict):
            if hasattr(target, key):
                merge(getattr(target, key), source_value)
            elif key in target:
                merge(target[key], source_value)
            else:
                return
        else:
            target.setdefault(key, source_value)

myObject2 = Theme()
merge(myObject2, {"__init__":{"__globals__":{"users":{"Hacker2": "Evil2"}}}})
username = "Hacker2"
password = "Evil2"
try_login(username, password) # Logged in

What if the class in different files and import as module

# the_test.py
class Test:
    my_value = "test"
    def __init__(self):
        print("Hello from Test")
from the_test import Test

test = Test()
our_globals_var = "CLEAN"
print(test.__init__.__globals__)

There is no our_globals_var, that’s because it is in the_test.py file only, how could we access the globals in this case? With some pyjail experience, here is my way of doing so:

# continue with above snippet
print(test.__init__.__globals__['__builtins__']['help'].__repr__.__globals__['sys'].modules['__main__'].our_globals_var) # CLEAN
test.__init__.__globals__['__builtins__']['help'].__repr__.__globals__['sys'].modules['__main__'].our_globals_var = "POLLUTED"
print(our_globals_var) # POLLUTED

Actually, you can see that I mentions module __main__ in above payload which will returns the main file, or the running context. We can replace it with anything that is imported (you can check by print(test.__init__.__globals__['__builtins__']['help'].__repr__.__globals__['sys'].modules)).

Conclusion

As demonstrated, Class Pollution is an intriguing vulnerability and able to cause catastrophic result.

Reference