Learnd From: https://goodapple.top/archives/1069
什么是Pickle?
pickle是Python中一个能够序列化和反序列化对象的模块。和其他语言类似,Python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。在Python中,*“Pickling”* 是将 Python 对象及其所拥有的层次结构转化为一个**二进制字节流**的过程,也就是我们常说的序列化,而 *“unpickling”* 是相反的操作,会将字节流转化回一个对象层次结构。
当然在Python 中并不止pickle一个模块能够进行这一操作,更原始的序列化模块如`marshal`,同样能够完成序列化的任务,不过两者的侧重点并不相同,`marshal`存在主要是为了支持 Python 的`.pyc`文件。现在开发时一般首选pickle。
pickle实际上可以看作一种**独立的语言**,通过对`opcode`的编写可以进行Python代码执行、覆盖变量等操作。直接编写的`opcode`灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
既然opcode能够执行Python代码,那自然就免不了安全问题。以下是Python在pickle文档中的警告。
|
常用opcode
在Python的pickle.py中,我们能够找到所有的opcode及其解释,常用的opcode如下,这里我们以V0版本为例
|
|
|
|
c |
获取一个全局对象或import一个模块 |
c[module]\n[instance]\n |
获得的对象入栈 |
o |
寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) |
o |
这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 |
i |
相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) |
i[module]\n[callable]\n |
这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 |
N |
实例化一个None |
N |
获得的对象入栈 |
S |
实例化一个字符串对象 |
S’xxx’\n(也可以使用双引号、'等python字符串形式) |
获得的对象入栈 |
V |
实例化一个UNICODE字符串对象 |
Vxxx\n |
获得的对象入栈 |
I |
实例化一个int对象 |
Ixxx\n |
获得的对象入栈 |
F |
实例化一个float对象 |
Fx.x\n |
获得的对象入栈 |
R |
选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 |
R |
函数和参数出栈,函数的返回值入栈 |
. |
程序结束,栈顶的一个元素作为pickle.loads()的返回值 |
. |
无 |
( |
向栈中压入一个MARK标记 |
( |
MARK标记入栈 |
t |
寻找栈中的上一个MARK,并组合之间的数据为元组 |
t |
MARK标记以及被组合的数据出栈,获得的对象入栈 |
) |
向栈中直接压入一个空元组 |
) |
空元组入栈 |
l |
寻找栈中的上一个MARK,并组合之间的数据为列表 |
l |
MARK标记以及被组合的数据出栈,获得的对象入栈 |
] |
向栈中直接压入一个空列表 |
] |
空列表入栈 |
d |
寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) |
d |
MARK标记以及被组合的数据出栈,获得的对象入栈 |
} |
向栈中直接压入一个空字典 |
} |
空字典入栈 |
p |
将栈顶对象储存至memo_n |
pn\n |
无 |
g |
将memo_n的对象压栈 |
gn\n |
对象被压栈 |
0 |
丢弃栈顶对象 |
0 |
栈顶对象被丢弃 |
b |
使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 |
b |
栈上第一个元素出栈 |
s |
将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 |
s |
第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 |
u |
寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 |
u |
MARK标记以及被组合的数据出栈,字典被更新 |
a |
将栈的第一个元素append到第二个元素(列表)中 |
a |
栈顶元素出栈,第二个元素(列表)被更新 |
e |
寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 |
e |
MARK标记以及被组合的数据出栈,列表被更新 |
ISCTF2024【新闻系统】
前面就是session 伪造不多说
源码:
from flask import * import pickle import base64
app = Flask(__name__) app.config["SECRET_KEY"] = "W3l1com_isCTF"
class News: def __init__(self, title, content) -> None: self.title = title self.content = content
def __repr__(self) -> str: return f"news(name={self.title}, words={self.content})"
class NewsList: def __init__(self) -> None: self.news_list = []
def create_news(self, title, content) -> None: news = News(title,content) self.news_list.append(news) def export_news(self, news_title) -> str | None: news = self.get_news(news_title) if news is not None: self.news_list.remove(news) return '删除成功' return None def add_news(self, serialized_news) -> None: try: news_data = base64.b64decode(serialized_news) black_list = ['create_news','export_news','add_news','get_news'] for i in black_list: if i in str(news_data): return False news = pickle.loads(news_data) if isinstance(news,News): for i in self.news_list: if i.title == news.title: return False self.news_list.append(news) return True return False except Exception: return False def get_news(self, news_title) -> News | None: for news in self.news_list: if str(news.title) == news_title: return news return None
newslist = NewsList()
@app.route("/") def index(): return redirect("/login")
@app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form.get('username') password = request.form.get('password') if username == 'test' and password == 'test111': session['username'] = username session['password'] = password session['status'] = 'user' return redirect('/news') else: session['login_error'] = True return render_template("login.html")
@app.route("/news") def news(): news = newslist.news_list return render_template("news.html",news = news)
@app.route('/admin',methods=['GET','POST']) def admin(): if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222': return redirect("/login") news = newslist.news_list return render_template("admin.html",news = news) @app.route("/create", methods=["POST"]) def create_news(): if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222': return redirect("/login") title = request.form.get('title') content = request.form.get('content') newslist.create_news(title,content) return redirect("/admin")
@app.route("/export", methods=["POST"]) def export_news(): if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222': return redirect("/login") news_title = request.form["title"] result = newslist.export_news(news_title) if result is not None: return jsonify({"result": result}) else: return jsonify({"error": "news not found"})
@app.route("/add", methods=["POST"]) def add_news(): if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222': return redirect("/login") serialized_news = request.form["serialized_news"] if newslist.add_news(serialized_news): return redirect("/admin") else: return jsonify({"error": "Failed to add news"})
if __name__ == "__main__": app.run(host="0.0.0.0", port=8888, debug=False, threaded=True)
|
伪造进入/admin
news_data = base64.b64decode(serialized_news)
|
serialized_news = request.form["serialized_news"] if newslist.add_news(serialized_news): return redirect("/admin")
|
这里可以看到add路由可以进行反序列化
参考了 https://xz.aliyun.com/t/16227?time__1311=GuD%3DPRxGrh8D%2FWNiQGkDuQrKj8DmE%2BbD#toc-7
自己暂时还不会构造 然后 可以得到flag