前言
至于为什么要写这篇文章呢?因为我认为这算是一个很高质量的题目,况且对于我对python并不熟悉来说,我们需要深入源码去分析才能做到更好的理解,所以打算单开一篇文章写这个题的思路和过程
原来不是flag?
1 | 欢迎来到全世界最安全的银行系统!我们采用了最先进的安保系统来保护您的账户。但最近有传言说,即使是最安全的系统也可能存在漏洞。你能绕过我们的安全措施,进入金库获取宝藏吗? |
打开题目是一个登录口,在/about下有提示
从这里可以得出几个信息,这里的话是python的flask框架开发的web应用,然后是用json进行数据传输的,在会话管理方面是用的jsonpickle去进行传输token的。
话不多说,我们先注册个账号进去看一下
admin用户存在?我们换个1111/111111注册一下(要求用户名大于4位,密码大于6位)
提示需要管理员权限才能访问,那么就需要伪造admin身份了,直接看cookie的结构
1 | authz=eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogIjExMTEiLCAidHMiOiAxNzUzODU4ODMyfX0= |
之前提示是base64编码的,拿去解码一下
明了了,user就是我们刚刚注册的用户名,我们改成admin试一下,一开始以为需要注意后面的时间戳的,但是好像貌似不需要?
拿到管理员身份了,但是保险库中是一个假的flag。。。好吧,挖了个坑
深究源码
因为是提示了jsonpickle,所以去先知社区翻了一下文章
从里面可以看到jsonpickle的反序列化恢复对象其实是基于本身定义的一些标签去进行的。话不多说,直接拖一个源码下来分析一下
https://github.com/jsonpickle/jsonpickle
jsonpickle/tags.py
文件下有反序列化支持的标签,标签对应的处理函数见jsonpickle/unpickler.py
。
1 | #tags.py |
反序列化的流程,先看decode函数
1 | def decode( |
解释一下几个重要参数:
string: 要解码的 JSON 字符串。
keys: 如果设置为True,则jsonpickle将解码非字符串类型的字典键
backend: 指定一个解码后端(
JSONBackend
)。如果未提供,默认为json
safe:一个非常重要的安全开关。如果为 False,它会使用 eval() 来恢复某些旧格式的对象,这存在安全风险,可能被用来执行恶意代码。默认为 True,即安全模式。
1 | data = backend.decode(string) |
这里的话利用backend的decode方法先将传入的字符串进行解码
1 | return context.restore(data, reset=reset, classes=classes) |
调用context的restore方法去将解码后的数据恢复成python对象。然后我们继续根据restore
继续跟进_restore
如果 obj
是字符串、列表、字典、集合或元组中的一种,程序会调用 self._restore_tags(obj)
,继续跟进
1 | def _restore_tags( |
这里的话就是重要逻辑了, 根据标签去恢复对象,可以看到当对象为字典的时候逻辑还是蛮多的。我们找几个有意思的看一下
py/object
先看看处理逻辑
1 | def _restore_object(self, obj: Dict[str, Any]) -> Any: |
先是从处理对象中提取出py/object键的值作为class_name类名,我们跟进看一下这个loadclass方法
1 | def loadclass( |
其实就是一个加载指定模块中类的一个方法,先是检查classes字典是否存在,存在则把里面的str键对应的值返回,如果没找到的话就会分离出最后一部分类名并再次查找。例如,'datetime.datetime'
会先查找 'datetime.datetime'
,如果找不到,再尝试查找 'datetime'
。如果两者都没有找到,则跳过这部分逻辑。
如果classes字典并不存在的话,就会通过从系统中动态加载模块去获取类,这里的话就没必要说了
其实这里的话就是一个获取模块中类的过程,跟进一下return中的函数
重点看几个逻辑
1 | if has_tag(obj, tags.NEWARGSEX): |
如果obj字典中包含tags.NEWARGSEX标签,就会从该标签中获取args
和 kwargs
,这两个变量分别表示的是位置参数和关键字参数
如果没有这个标签的话会调用getargs方法去获取参数,并设置关键字参数为空
如果这两个变量存在,就会调用_restore去反序列化,恢复它们的原始数据结构。
1 | try: |
如果是新式类且该类定义了 __new__
方法,使用 cls.__new__
来实例化对象。如果有工厂方法,则将工厂方法作为参数传递给 __new__
。
如果不是新式类的话就自动实例一个空对象
1 | if is_oldstyle: |
如果是旧式类,就直接利用cls(*args)
调用构造函数,并且这里会触发__init__
方法
这里的话还会恢复实例中的变量,也就是从obj中提取数据并赋值给实例的各个属性变量
最终返回恢复出来的实例。
我们写个demo测试一下
1 | import jsonpickle |
在_restore_object
中打断点进入
可以看到在执行了loadclass之后成功找到glob方法并赋值给cls,步入return语句,这里的话会先判断是否有newargsex标签,我们这里没用上,所以到else中获取到arg的值,随后进入_restore方法
这里的话就是再次调用_restore_tags
去获取args了,因为这里的话args是一个列表,所以会进入最后一个list的语句调用_restore_list
往后走,最终的话就是会调用glob,glob("/*")
不小心把代码写错了,应该是这样的
1 | import jsonpickle |
然后我们具体看一下那两个参数标签
py/newargs和py/newargsex

py/newargsex反序列化处理逻辑
py/newargs反序列化处理逻辑
把刚刚的代码换成py/newargsex跑一下
出现了一个报错,其实就是因为我们传入的内容格式不正确导致的,由于py/newargsex
的值应该是一个包含两个元素的列表:[args, kwargs]
,所以需要改成
1 | import jsonpickle |
payload总结
- 读目录
1 | glob.glob函数 |
- 读文件
1 | linecache.getlines函数 |
- RCE
1 | {"py/object" : "subprocess.run","py/newargs" : ["calc"]} |
然后我们可以试一下
初次尝试
在尝试之前,我们需要注意一个点,就是关于回显位的问题,我们如何将反序列化后的执行结果返回到页面中进行渲染?
先把之前的cookie拿过来看一下
1 | {"py/object": "__main__.Session", "meta": {"user": "1111", "ts": 1754290309}} |
再结合/panel路由的页面

不难看出,user中的内容就是会回显到页面中,那我们不妨把poc翻到user的值中
1 | import base64 |
成功回显了根目录下的文件,证明我们的思路是正确的
有一个flag和readflag文件,先尝试读一下flag
1 | payload = { |
但是没有返回内容,估计是有权限,那么就想到需要RCE去执行readflag了
厚积薄发
先读一下源码/app/app.py,拿到后有点乱,丢给ai处理一下
1 | from flask import Flask, request, make_response, render_template, redirect, url_for |
当看到黑名单的时候心就已经凉了一半
1 | FORBIDDEN = [ |
方法一:清理黑名单
这个方法来自LAMENTXU师傅的文章https://www.cnblogs.com/LAMENTXU/articles/19007988
具体是什么样的呢?
用到一个内置函数list.clear()
我们本地测试一下
因为这里的黑名单也是list类型,那我们是否可以调用FORBIDDEN.clear()去清空黑名单呢?
写个poc
1 | payload = { |
返回none,那我们检验一下是否被置空了
1 | {"py/object" : "subprocess.run","py/newargs" : ["whoami"]} |
这里的话因为run函数执行之后是返回一个对象,所以没有回显,但是返回0是说明执行成功了,那我们尝试换成subprocess.getoutput
1 | {"py/object" : "subprocess.getoutput","py/newargs" : ["whoami"]} |
成功回显,那我们RCE执行/readflag就行了
方法二:map类触发
map()
函数的基本语法如下:
1 | map(function, iterable, ...) |
function
: 一个函数,用于对每个可迭代对象的元素执行操作。iterable
: 一个或多个可迭代对象,可以是列表、元组、集合等。
所以map的参数需要用[]
或者{}
包裹
但是map中的函数触发是需要被类用才能触发,比如bytes,list类接受迭代器参数初始化的时候
我们写个demo测试一下
1 | import os |
这里只返回了123,说明bytes类能触发map的函数调用
bytes在new的时候会触发map的实例化,比如这样就可以触发rce,
所以最终的poc
1 | payload = { |