nepctf safe_bank

该文章已生成可运行项目,

萌新一枚,看到大佬的文章感觉好厉害,但是其实看的不是特别明白,就是对于还没学python的我来说,还是有好多细节不理解的,于是写这篇文章记录一下。不一定都是对的,有错误请指正。

首先是个登录框,目录和弱密码爆破无果后,看了下关于我们

然后注册了个用户

看到这以为是ssti,结果试了一下也没有。然后回去看了 下技术细节,发现jsonpickle,去查了下好像有漏洞,

也是改了下权限,但是是个假的flag

于是也看到了这篇文章从源码看JsonPickle反序列化利用与绕WAF-先知社区

我是真菜,比赛的时候以为直接拿它的payload打,然后用下拼接加编码,结果试了半天啥也不是。现在去分析一下为啥不行。先拿下大佬的题目源码分析一下

from flask import Flask, request, make_response, render_template, redirect, url_for
import jsonpickle
import base64
import json
import os
import time

app = Flask(__name__)
app.secret_key = os.urandom(24)

class Account:
    def __init__(self, uid, pwd):
        self.uid = uid
        self.pwd = pwd

class Session:
    def __init__(self, meta):
        self.meta = meta

users_db = [
    Account("admin", os.urandom(16).hex()),
    Account("guest", "guest")
]

def register_user(username, password):
    for acc in users_db:
        if acc.uid == username:
            return False
    users_db.append(Account(username, password))
    return True

FORBIDDEN = [
    'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
    'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb',  'pickle', 'marshal',
    'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
    'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
    'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
    'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
    '__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
    '__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

def waf(serialized):
    try:
        data = json.loads(serialized)
        payload = json.dumps(data, ensure_ascii=False)
        for bad in FORBIDDEN:
            if bad in payload:
                return bad
        return None
    except:
        return "error"

@app.route('/')
def root():
    return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        confirm_password = request.form.get('confirm_password')
        
        if not username or not password or not confirm_password:
            return render_template('register.html', error="所有字段都是必填的。")
        
        if password != confirm_password:
            return render_template('register.html', error="密码不匹配。")
            
        if len(username) < 4 or len(password) < 6:
            return render_template('register.html', error="用户名至少需要4个字符,密码至少需要6个字符。")
        
        if register_user(username, password):
            return render_template('index.html', message="注册成功!请登录。")
        else:
            return render_template('register.html', error="用户名已存在。")
    
    return render_template('register.html')

@app.post('/auth')
def auth():
    u = request.form.get("u")
    p = request.form.get("p")
    for acc in users_db:
        if acc.uid == u and acc.pwd == p:
            sess_data = Session({'user': u, 'ts': int(time.time())})
            token_raw = jsonpickle.encode(sess_data)
            b64_token = base64.b64encode(token_raw.encode()).decode()
            resp = make_response("登录成功。")
            resp.set_cookie("authz", b64_token)
            resp.status_code = 302
            resp.headers['Location'] = '/panel'
            return resp
    return render_template('index.html', error="登录失败。用户名或密码无效。")

@app.route('/panel')
def panel():
    token = request.cookies.get("authz")
    if not token:
        return redirect(url_for('root', error="缺少Token。"))
    
    try:
        decoded = base64.b64decode(token.encode()).decode()
    except:
        return render_template('error.html', error="Token格式错误。")
    
    ban = waf(decoded)
    if waf(decoded):
        return render_template('error.html', error=f"请不要黑客攻击!{ban}")
    
    try:
        sess_obj = jsonpickle.decode(decoded, safe=True)
        meta = sess_obj.meta
        
        if meta.get("user") != "admin":
            return render_template('user_panel.html', username=meta.get('user'))
        
        return render_template('admin_panel.html')
    except Exception as e:
        return render_template('error.html', error=f"数据解码失败。")

@app.route('/vault')
def vault():
    token = request.cookies.get("authz")
    if not token:
        return redirect(url_for('root'))

    try:
        decoded = base64.b64decode(token.encode()).decode()
        if waf(decoded):
            return render_template('error.html', error="请不要尝试黑客攻击!")
        sess_obj = jsonpickle.decode(decoded, safe=True)
        meta = sess_obj.meta
        
        if meta.get("user") != "admin":
            return render_template('error.html', error="访问被拒绝。只有管理员才能查看此页面。")
            
        flag = "NepCTF{fake_flag_this_is_not_the_real_one}"
            
        return render_template('vault.html', flag=flag)
    except:
        return redirect(url_for('root'))

@app.route('/about')
def about():
    return render_template('about.html')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=False)

其实flask框架也不是特别懂,这里先看下cookie是怎么验证的吧。

发现登录框的输入框是向/auth路由发post包。

@app.post('/auth')
def auth():
    u = request.form.get("u")
    p = request.form.get("p")
    for acc in users_db:
        if acc.uid == u and acc.pwd == p:
            sess_data = Session({'user': u, 'ts': int(time.time())})
            token_raw = jsonpickle.encode(sess_data)
            b64_token = base64.b64encode(token_raw.encode()).decode()
            resp = make_response("登录成功。")
            resp.set_cookie("authz", b64_token)
            resp.status_code = 302
            resp.headers['Location'] = '/panel'
            return resp
    return render_template('index.html', error="登录失败。用户名或密码无效。")

然后看到resp.set_cookie("authz",b64_token),这应该是设置cookie的,然后重定向到/panel

@app.route('/panel')
def panel():
    token = request.cookies.get("authz")
    if not token:
        return redirect(url_for('root', error="缺少Token。"))
    
    try:
        decoded = base64.b64decode(token.encode()).decode()
    except:
        return render_template('error.html', error="Token格式错误。")
    
    ban = waf(decoded)
    if waf(decoded):
        return render_template('error.html', error=f"请不要黑客攻击!{ban}")
    
    try:
        sess_obj = jsonpickle.decode(decoded, safe=True)
        meta = sess_obj.meta
        
        if meta.get("user") != "admin":
            return render_template('user_panel.html', username=meta.get('user'))
        
        return render_template('admin_panel.html')
    except Exception as e:
        return render_template('error.html', error=f"数据解码失败。")

这里看到先把token = request.cookies.get("authz"),cookie的authz赋值给token。然后验证cookie是否存在和能否base64解码。然后就进到waf了。

FORBIDDEN = [
    'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
    'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb',  'pickle', 'marshal',
    'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
    'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
    'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
    'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
    '__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
    '__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

def waf(serialized):
    try:
        data = json.loads(serialized)
        payload = json.dumps(data, ensure_ascii=False)
        for bad in FORBIDDEN:
            if bad in payload:
                return bad
        return None
    except:
        return "error"

然后先是

data = json.loads(serialized)

payload = json.dumps(data, ensure_ascii=False)

这里果然不能拼接,查了一下发现

JSON 对键的格式有严格要求:

  1. 键必须是字符串,且必须用双引号包裹。

这样就没法对标签进行拼接了,然后试下编码

json.loads自动把编码还原了,那这样还没到waf就被还原了,就没法绕过了。

然后如果是正常的数据就会进入

  try:
        sess_obj = jsonpickle.decode(decoded, safe=True)
        meta = sess_obj.meta

先在看看大佬的绕过方法吧

{"py/object": "__main__.Session", "meta": {"user": {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},"ts":1753446254}}

说实话不好理解,虽然看大佬的文章,跟着人家的思路知道这个代码可以干嘛,但是具体咋来的我也不懂。现在学习一下吧。

拷打了一下ai
 

1. 反序列化主入口 (jsonpickle/__init__.py)

# jsonpickle/__init__.py

def decode(string, backend=None, keys=False, safe=False, context=None):
    # 创建 Unpickler 实例
    unpickler = unpickler.Unpickler(
        backend=backend, 
        keys=keys, 
        safe=safe, 
        context=context
    )
    # ⚠️ 关键接入点:将 JSON 字符串转为 Python 对象
    data = backend.decode(string)
    # ⚠️ 核心入口:调用 restore 方法
    return unpickler.restore(data)
try: sess_obj = jsonpickle.decode(decoded, safe=True) 


 

这里的decode应该就是调用入口。

我们只传了payload和safe

看了下源码

基础反序列化backend.decode(string)将 JSON 字符串转换为 Python 字典:

确实最后又调用了restore

  1. 状态重置(reset 参数)
    如果 reset=True(默认行为),会先调用 self.reset(),清空 Unpickler 内部的对象引用缓存、代理对象等状态,确保本次恢复是 “干净” 的。

  2. 类注册(classes 参数)
    如果传入 classes(类或类的映射),会调用 self.register_classes(classes),将这些类注册到 Unpickler 中。这一步是为了让反序列化时能找到自定义类(比如不在全局作用域的类)。

  3. 核心恢复逻辑(self._restore(obj)
    这是反序列化的核心:_restore 方法会递归遍历 obj,识别 jsonpickle 特有的元数据标签(如 py/objectpy/setpy/newargsex 等),并根据标签逻辑重建对象。

    • 对基础类型(如字符串、字典)直接返回;
    • 对包含 py/object 的字典,会尝试导入并实例化对应的类;
    • 对 py/set 等集合标签,会重建为 Python 集合对象。

继续进入_restore(obj)

  1. 类型检查
    首先判断 obj 是否为 (str, list, dict, set, tuple) 中的一种:

    python

    if not isinstance(obj, (str, list, dict, set, tuple)):
        restore = _passthrough
    

     
    • 若 obj 不属于这些 “容器 / 可迭代” 类型,说明它不可能包含 jsonpickle 的元数据标签(如 py/objectpy/set 等),因此直接用 _passthrough 函数处理(即直接返回原对象)。
    • 注释强调 “不要把类型元组改成集合”,因为 isinstance 对集合的判断逻辑与元组不同,会导致错误。
  2. 标签解析分支
    若 obj 属于上述类型,调用 self._restore_tags(obj)

    python

    else:
        restore = self._restore_tags(obj)
    

     

    _restore_tags 是 jsonpickle 的标签解析核心,会识别 obj 中是否包含 py/objectpy/set 等元数据标签,并返回对应的处理函数(如实例化类、重建集合等)。

我们的payload之前backend.decode(string)将 JSON 字符串转换为 Python 字典,

是字典进入_restore_tags

_restore_tags里是

 def _restore_tags(
        self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
    ) -> Callable[[Any], Any]:
        """Return the restoration function for the specified object"""
        try:
            if not tags.RESERVED <= set(obj) and type(obj) not in (list, dict):
                return _passthrough
        except TypeError:
            pass
        if type(obj) is dict:
            if tags.TUPLE in obj:
                restore = self._restore_tuple
            elif tags.SET in obj:
                restore = self._restore_set  # type: ignore[assignment]
            elif tags.B64 in obj:
                restore = self._restore_base64  # type: ignore[assignment]
            elif tags.B85 in obj:
                restore = self._restore_base85  # type: ignore[assignment]
            elif tags.ID in obj:
                restore = self._restore_id
            elif tags.ITERATOR in obj:
                restore = self._restore_iterator  # type: ignore[assignment]
            elif tags.OBJECT in obj:
                restore = self._restore_object
            elif tags.TYPE in obj:
                restore = self._restore_type
            elif tags.REDUCE in obj:
                restore = self._restore_reduce
            elif tags.FUNCTION in obj:
                restore = self._restore_function
            elif tags.MODULE in obj:
                restore = self._restore_module
            elif tags.REPR in obj:
                if self.safe:
                    restore = self._restore_repr_safe
                else:
                    restore = self._restore_repr
            else:
                restore = self._restore_dict  # type: ignore[assignment]
        elif type(obj) is list:
            restore = self._restore_list  # type: ignore[assignment]
        else:
            restore = _passthrough  # type: ignore[assignment]
        return restore

方法核心逻辑

该方法接收一个序列化后的对象 obj,返回一个恢复函数(Callable),用于将 obj 转换回原始 Python 对象。

1. 前置检查:过滤无需标签解析的对象

python

try:
    if not tags.RESERVED <= set(obj) and type(obj) not in (list, dict):
        return _passthrough
except TypeError:
    pass

  • tags.RESERVED 是 jsonpickle 的保留标签集合(如 py/objectpy/setpy/tuple 等)。
  • 检查 obj 是否包含保留标签,且类型不是 list 或 dict
    • 若满足条件,说明 obj 无需标签解析,直接返回 _passthrough(即 “直接返回原对象”)。
    • 若 obj 是不可哈希类型(如 list),set(obj) 会触发 TypeError,此时通过 except 忽略检查,进入后续分支。
2. 字典类型(dict)的标签解析

若 obj 是字典,根据其中包含的保留标签,选择对应的恢复函数:

我们是"py/object"标签进入这个

class_name = obj[tags.OBJECT]

在 Python 中,中括号 [] 通常用于索引操作,表示从某个对象中获取特定位置或键对应的值。在 class_name = obj[tags.OBJECT] 这行代码中,[] 的具体含义取决于 obj 的类型:

1. 字典(dict)索引

如果 obj 是一个字典,obj[key] 表示获取字典中键 key 对应的值

这里就是把payload的__main__.Session赋值给class_name。

然后通过 loadclass 函数动态导入并获取类对象 cls

就是导入 __main__.Session 类
其实这个我也没看不太懂源码,。。一步一步来吧。

先看下参数第一个是__main__.Session,第二个是啥self._classes

self是_restore_object的第一个参数

往前看了一下一开始调用 context.restore(data, reset=reset, classes=classes)

的下一步就有self了

def restore(

        self, obj: Any, reset: bool = True, classes: Optional[ClassesType] = None

    )
ai一下。

在你给出的代码里,context.restore(data, reset=reset, classes=classes) 这行代码中的 context 其实是 Unpickler 类的一个实例。所以,当调用 restore 方法时,self 代表的就是这个 Unpickler 实例。下面来详细分析一下:

  1. context 的创建过程

python

运行

context = context or Unpickler(
    keys=keys,
    backend=backend,
    safe=safe,
    v1_decode=v1_decode,
    on_missing=on_missing,
    handle_readonly=handle_readonly,
)

要是用户没有特别提供 context 参数,系统就会自动创建一个 Unpickler 实例,并且把相关的配置参数传递给它。

  1. 方法调用的本质
    在 Python 里,当你通过实例去调用一个方法时,比如 context.restore(...),Python 会把这个实例(也就是 context)自动作为第一个参数传递给方法,这个参数就是 self。所以,在 restore 方法的实现中:
def restore(self, obj: Any, reset: bool = True, classes: Optional[ClassesType] = None):
    # self 就是调用这个方法的 Unpickler 实例
    ...

这里的 self 包含了实例的所有属性和配置,像 keysbackendsafe 这些参数都可以通过 self 来访问。

  1. 方法的作用
    restore 方法的主要功能是把 JSON 数据反序列化为 Python 对象。它会依据 JSON 数据的结构,运用 Unpickler 中配置好的规则和处理器,将数据还原成原始的 Python 对象。

总结来说,self 代表的是 Unpickler 实例,这个实例保存了反序列化过程中需要用到的所有配置信息和状态。

但是好像和self没关。继续ai

1. 处理 "__main__.Session"

步骤解析
  1. 参数传入

    • module_and_name = "__main__.Session"
    • classes = None(默认未传入自定义类映射)
  2. 跳过自定义类检查

    python

    # loadclass() 源码片段
    if classes:  # classes 为 None,跳过此分支
        try:
            return classes[module_and_name]
        except KeyError:
            # ...
    
  3. 分割模块名和类名

    python

    # loadclass() 源码片段
    names = module_and_name.split('.')  # ['__main__', 'Session']
    
  4. 尝试导入模块并获取类

    • 第一次分割尝试up_to = 1):
      • 模块名:__main__
      • 类名:Session

      python

      # loadclass() 源码片段
      for up_to in range(len(names) - 1, 0, -1):  # up_to = 1
          module = util.untranslate_module_name('.'.join(names[:up_to]))  # '__main__'
          try:
              __import__(module)  # 导入 __main__ 模块(当前运行的主模块)
              obj = sys.modules[module]  # 获取 __main__ 模块对象
              for class_name in names[up_to:]:  # 遍历 ['Session']
                  obj = getattr(obj, class_name)  # 获取 Session 类
              return obj  # 返回 Session 类对象
          except (AttributeError, ImportError, ValueError):
              continue
      
    • 成功获取:如果当前主模块中定义了 Session 类,则返回该类对象。

然后就是返回了session类,

回到原来的函数

  1. 查找自定义处理器

    python

    运行

    # _restore_object() 源码片段
    handler = handlers.get(cls, handlers.get(class_name))  # 查找 Session 类的自定义处理器
    

     
    • handlers 是一个全局注册表,存储类与处理器的映射关系。
    • 如果未找到自定义处理器(通常是这种情况),handler 为 None,进入默认逻辑。
    • 如果找到自定义处理器,执行 handler(self).restore(obj),但我们先分析默认情况。
  2. 默认对象恢复逻辑

    • 由于 handler 为 None,跳过 if handler is not None 分支,执行后续默认逻辑

继续ai

步骤 1:进入 _restore_object_instance 函数,初始化代理

_restore_object_instance 是恢复对象实例的核心函数,首先创建一个代理对象(处理循环引用):

def _restore_object_instance(
        self, obj: Dict[str, Any], cls: Type[Any], class_name: str = ''
    ) -> Any:
    # 1. 创建占位代理对象(允许子对象在父对象实例化前引用它)
    proxy = _Proxy()  # 临时代理,用于处理循环引用
    self._mkref(proxy)  # 记录代理引用,加入内部引用表

步骤 2:加载工厂函数(_loadfactory

接下来尝试从 obj 中加载工厂函数(通常用于集合、列表等有默认工厂的类型,此处 Session 类无工厂,返回 None):

    # 2. 加载工厂函数(若存在)
    factory = self._loadfactory(obj)  # 输入 obj 中无工厂相关字段,factory 为 None

步骤 3:解析实例化参数(args 和 kwargs

从 obj 中提取实例化 Session 类所需的参数(args 和 kwargs):

ai出错了又重开了个会话

前提:明确两个对象的结构

输入数据包含两个带py/object的对象:

  1. 外层Session对象:{"py/object": "__main__.Session", "meta": {...}}
    • py/newargsex标签,只有meta字段。
  2. 嵌套的glob.glob对象:{"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]}, ""]}
    • py/newargsex标签,包含函数调用参数。

步骤 1:_restore_object调用_restore_object_instance处理Session对象

Session对象的_restore_object流程结束后,进入_restore_object_instance

return self._restore_object_instance(obj, cls, class_name)  # obj是Session的序列化数据

步骤 2:_restore_object_instance处理Session对象(无py/newargsex

2.1 初始化代理
def _restore_object_instance(self, obj: Dict[str, Any], cls: Type[Any], class_name: str = '') -> Any:
    proxy = _Proxy()  # 创建代理
    self._mkref(proxy)  # 记录代理引用
2.2 加载工厂函数(Session无工厂)
factory = self._loadfactory(obj)  # factory = None(Session无工厂相关字段)
2.3 解析Session的实例化参数(无py/newargsex
# 检查Session的obj是否有py/newargsex标签
if has_tag(obj, tags.NEWARGSEX):  # Session的obj中无此标签,条件为False
    args, kwargs = obj[tags.NEWARGSEX]
else:
    args = getargs(obj, classes=self._classes)  # 调用getargs获取默认参数(返回空列表)
    kwargs = {}  # 无关键字参数

# 恢复参数(因args和kwargs为空,无实际操作)
if args:
    args = self._restore(args)
if kwargs:
    kwargs = self._restore(kwargs)
  • 结果args = []kwargs = {}Session实例化无需参数)。
2.4 创建Session实例(新式类)
is_oldstyle = not (isinstance(cls, type) or getattr(cls, '__meta__', None))  # 假设为新式类,is_oldstyle = False
try:
    if not is_oldstyle and hasattr(cls, '__new__'):
        # 调用Session的__new__方法创建实例(无参数)
        instance = cls.__new__(cls, *args, **kwargs)  # 即 Session.__new__(Session)
    else:
        instance = object.__new__(cls)
except TypeError:
    is_oldstyle = True  # 不触发
2.5 替换代理引用
proxy.reset(instance)  # 代理指向真实实例
self._swapref(proxy, instance)  # 引用表中替换为真实实例
2.6 恢复Session的实例变量(处理meta字段)
instance = self._restore_object_instance_variables(obj, instance)  # 关键:处理meta字段

步骤 3:_restore_object_instance_variables恢复Sessionmeta属性

完啦,大家对不起,之前的版本到这后面都是ai自动补的源码了,我服了,这ai也不提示,我源码是github下,大家还是对着源码看比较不容易错。

步骤 1:进入 _restore_object_instance_variables 函数

该函数是恢复实例属性的入口,核心逻辑分三步:

def _restore_object_instance_variables(
        self, obj: Dict[str, Any], instance: Any
    ) -> Any:
    # 1. 从字典恢复属性(处理 obj 中的字段,如 meta)
    instance = self._restore_from_dict(obj, instance)

    # 2. 处理序列子类(如 list/set 子类,当前 Session 实例不涉及)
    if has_tag(obj, tags.SEQ):  # obj 中无 "py/seq" 标签,条件不成立
        if hasattr(instance, 'append'):
            ...
        elif hasattr(instance, 'add'):
            ...

    # 3. 处理状态数据(当前 Session 实例不涉及)
    if has_tag(obj, tags.STATE):  # obj 中无 "py/state" 标签,条件不成立
        instance = self._restore_state(obj, instance)

    return instance  # 返回恢复属性后的实例

  • 核心流程集中在第一步:_restore_from_dict(obj, instance)

步骤 2:_restore_from_dict 处理 Session 的字段(重点是 meta

_restore_from_dict 是恢复属性的核心函数,作用是遍历 obj 中的键值对(排除保留标签),递归恢复值并赋值给 Session 实例。

2.1 初始化变量
def _restore_from_dict(
        self,
        obj: Dict[str, Any],
        instance: Any,
        ignorereserved: bool = True,
        restore_dict_items: bool = True,
    ) -> Any:
    restore_key = self._restore_key_fn()  # 获取键恢复函数(此处用于处理字典键)
    method = _obj_setattr  # 属性设置方法(默认使用 setattr)
    deferred = {}  # 存储延迟设置的属性(用于不可变对象)
2.2 遍历 obj 中的键值对

obj 中包含 {"py/object": "__main__.Session", "meta": {...}},遍历所有键:

    for k, v in util.items(obj):  # k 依次为 "py/object"、"meta"
        # 跳过保留标签(如 "py/object")
        if ignorereserved and k in tags.RESERVED:  # "py/object" 是保留标签,跳过
            continue
        
        # 处理键名(转换为字符串,用于命名栈)
        if isinstance(k, (int, float)):
            str_k = k.__str__()
        else:
            str_k = k  # k 为 "meta" 时,str_k = "meta"
        
        self._namestack.append(str_k)  # 将 "meta" 加入命名栈(用于调试/跟踪)
这里util.items()我去看了下源码

具体来说,这个函数做了这些事情:

  1. 接收两个参数:

    • obj:一个映射类型(Mapping[K, V])的对象,比如字典
    • exclude:一个可迭代对象,包含需要排除的键(默认是空元组,即不排除任何键)
  2. 函数逻辑:

    • 遍历 obj 中的所有键值对(k, v
    • 如果当前键 k 在 exclude 列表中,则跳过(continue
    • 否则,通过 yield 返回这个键值对(k, v
  3. 注释说明:
    这个函数不能简单地用 dict.items() 替代,因为它有 exclude 参数可以排除特定键,目前需要保留这个函数。

其实只传了一个参数obj。没用这函数的排除功能

后面自己写了。

又去拷打ai了。。。。。。。

_restore_from_dict 处理 meta 键

_restore_from_dict 方法负责遍历 obj 中的所有键(排除保留标签如 py/object),并将值恢复后赋值给实例。对于 meta 键:

def _restore_from_dict(
    self,
    obj: Dict[str, Any],
    instance: Any,
    ignorereserved: bool = True,
    restore_dict_items: bool = True,
) -> Any:
    restore_key = self._restore_key_fn()  # 用于恢复键(当前案例中为普通字符串键)
    method = _obj_setattr  # 用于设置属性的方法(默认用setattr)
    deferred = {}

    for k, v in util.items(obj):
        # 忽略保留标签(如py/object),只处理用户自定义键(如meta)
        if ignorereserved and k in tags.RESERVED:
            continue
        
        # 记录当前键名到命名栈(用于引用追踪)
        str_k = str(k)
        self._namestack.append(str_k)

        # 恢复键和值(当前案例中键为"meta",值为嵌套字典)
        if restore_dict_items:
            k = restore_key(k)  # 键"meta"无需特殊处理,直接保留
            value = self._restore(v)  # 递归恢复值(即meta对应的嵌套字典)

        # 将恢复后的值赋值给Session实例的meta属性
        if not k.startswith('__'):
            try:
                setattr(instance, k, value)  # 等价于 session.meta = 恢复后的meta字典
            except AttributeError:
                # 处理只读属性等异常(当前案例不涉及)
                ...

        # 如果值是代理对象,记录下来后续替换(当前案例中暂时不涉及)
        if isinstance(value, _Proxy):
            self._proxies.append((instance, k, value, method))

        self._namestack.pop()  # 退出当前键的命名栈

    return instance

关键步骤:

  • 遍历到 k="meta" 时,v 是 {"user": {...}, "ts": 1753446254}
  • 调用 self._restore(v) 递归处理 meta 的值(嵌套字典),进入下一步。

3. 递归处理 meta 的值(嵌套字典)

self._restore(v) 会对 meta 的值(嵌套字典)进行反序列化。根据 _restore 方法的逻辑:

def _restore(
    self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
) -> Any:
    if not isinstance(obj, (str, list, dict, set, tuple)):
        return _passthrough(obj)
    else:
        restore = self._restore_tags(obj)  # 确定恢复方法
        return restore(obj)

由于 v 是字典,_restore_tags 会返回 _restore_dict 作为处理方法(因为字典中没有特殊标签如 py/object,仅为普通字典)。

4. _restore_dict 处理 meta 的嵌套字典

_restore_dict 方法用于恢复普通字典(非对象),遍历其键值对(user 和 ts):

def _restore_dict(self, obj: Dict[str, Any]) -> Dict[str, Any]:
    data = {}
    if not self.v1_decode:
        self._mkref(data)  # 记录引用(用于循环引用处理)

    # 遍历字典中的键值对(user和ts)
    for k, v in util.items(obj):
        str_k = str(k)
        self._namestack.append(str_k)  # 记录键名("user"或"ts")
        
        # 恢复值(递归处理user和ts)
        data[k] = result = self._restore(v)
        
        # 如果值是代理对象,记录后续替换(当前案例中暂时不涉及)
        if isinstance(result, _Proxy):
            self._proxies.append((data, k, result, _obj_setvalue))
        
        self._namestack.pop()

    return data

此时 data 会被填充为 {"user": 恢复后的user对象, "ts": 1753446254},并作为 meta 的值返回给上一层,最终通过 setattr 赋值给 Session 实例的 meta 属性。

5. 处理 user 键(glob.glob 函数)

user 对应的值是 {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},属于带 py/object 标签的字典,处理流程如下:

  1. _restore 触发 _restore_tags:由于字典包含 py/object_restore_tags 返回 _restore_object 方法。

  2. _restore_object 加载函数

    def _restore_object(self, obj: Dict[str, Any]) -> Any:
        class_name = obj[tags.OBJECT]  # "glob.glob"
        cls = loadclass(class_name, classes=self._classes)  # 加载glob.glob函数
        # 无自定义处理器,进入实例恢复
        return self._restore_object_instance(obj, cls, class_name)
    
     
    • loadclass("glob.glob") 会导入 glob 模块并返回 glob.glob 函数(函数也是对象)。
  3. _restore_object_instance 处理参数

    def _restore_object_instance(self, obj: Dict[str, Any], cls: Type[Any], class_name: str = '') -> Any:
        # 处理py/newargsex参数
        if has_tag(obj, tags.NEWARGSEX):
            args, kwargs = obj[tags.NEWARGSEX]  # args = [{"py/set":["/*"]}, ""], kwargs = {}
        args = self._restore(args)  # 递归恢复参数:将{"py/set":["/*"]}恢复为set(["/*"])
        kwargs = self._restore(kwargs)  # 空字典
        
        # 函数无需实例化,直接返回(此处cls是glob.glob函数)
        instance = cls  # 函数本身作为"实例"
        # 恢复其他变量(当前案例无额外变量)
        instance = self._restore_object_instance_variables(obj, instance)
        return instance
    
     
    • 最终 user 键的值被恢复为 glob.glob 函数,且参数已处理为 set(["/*"])(后续调用函数时会使用这些参数)。

6. 处理 ts 键(整数)

ts 对应的值是 1753446254(整数),处理流程如下:

  • _restore 方法判断其为非容器类型,直接返回原值(_passthrough)。
  • 在 _restore_dict 中,data["ts"] 被赋值为 1753446254

最终结果

Session 实例的 meta 属性被设置为:

{
    "user": glob.glob,  # 函数对象,参数已处理为set(["/*"])
    "ts": 1753446254    # 整数
}

总结关键源码调用链

plaintext

_restore_object_instance_variables
└── _restore_from_dict(处理meta键)
    └── _restore(meta的值:嵌套字典)
        └── _restore_tags → _restore_dict(处理user和ts)
            ├── _restore(user的值:带py/object的字典)
            │   ├── _restore_tags → _restore_object
            │   │   ├── loadclass(加载glob.glob)
            │   │   └── _restore_object_instance(处理参数)
            │   └── 返回glob.glob函数
            └── _restore(ts的值:整数)
                └── _passthrough(直接返回1753446254)

好难。。。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值