CTF web漏洞合集 Python篇(二)python中的路径穿越

路径穿越 -- Path Traversal

0x01 成因

我们来看一段代码:

from flask import Flask, request, render_template, send_file
app = Flask(__name__)

@app.get("/<path:file_path>")
def download_file(file_path: str):
    return send_file(file_path)
if __name__ == '__main__':
    app.run()

若本地目录下有一个examples.txt,访问http://localhost:8000/example.txt即可下载example.txt

然而,这段代码有一个很明显的安全问题:输入的路径中可以带有../,也就是说,可以回到上级目录。若传入参数:file=/flag即可直接获取根目录下的flag

同样的,我们来看一段教程:

https://geek-docs.com/python/python-ask-answer/53_python_download_file_using_fastapi.html#:~:text=Python 使用

中有这样一段代码

若本地目录下有一个examples.txt,访问http://localhost:8000/download_file?file_path=example.txt即可下载,然而相同的是,当用户传入带../依然可以回到任意上级目录

显然,这个漏洞是在发生文件下载操作的时候可能触发,原理是将用户的输入直接拼接到将要下载的文件的路径里。若用户传入带'../'的参数,就可以回到任意上级目录,在已知目标文件路径的前提下实现任意文件下载

0x02 漏洞特征

存在将用户输入直接(或存在可能绕过的过滤)拼接到即将下载给用户的文件路径中的行为

漏洞必须带有以下特征:

  • 存在类似send_file功能的函数(即:存在下载功能)
  • 下载文件路径可控
  • 拥有足够的权限

此时路径穿越漏洞就成立了

POC: 漏洞处传入: ../../../../../../../../etc/passwd,若返回文件内容则确诊路径穿越

0x03 利用

确诊路径穿越后,迅速查看题目中的docker file,看看flag文件的路径,直接获取即可

然而,有些题目会将flag放到一些秘密路径里,这时就需要爆破路径或者其他漏洞获取

注意:含有../的URL在浏览器传入时浏览器会自动给你“优化”掉(GET中的参数不会,如果直接出现在URL路径里就会),要用burp传包

0x04 常见防护及绕过

../替换为空

一种最蠢的过滤就是直接将用户输入中的../替换成空,如:

from flask import Flask, request, render_template, send_file
app = Flask(__name__)

@app.get("/<path:file_path>")
def download_file(file_path: str):
    if '../' in file_path:
        file_path = file_path.replace('../', '')
    return send_file(file_path)
if __name__ == '__main__':
    app.run()

看似安全的防护措施实际上有一个很简单的绕过方法:双写绕过 —— 使用....//代替../

这样../被替换一次后还剩一个../依然可以完成路径穿越

禁止/..

同样看起来相当安全的过滤

from flask import Flask, request, render_template, send_file
app = Flask(__name__)

@app.get("/<path:file_path>")
def download_file(file_path: str):
    if '/..' in file_path:
        return 'Invalid path'
    return send_file(file_path)
if __name__ == '__main__':
    app.run()

各位可以看一下有什么思路

答案其实很简单,我们可以祭出路径穿越的神器:%5C(正反斜杠)

/%5C..在路径解析的时候会将\忽略,这个漏洞在open函数中也奏效,即:

from flask import Flask, request, render_template, send_file
app = Flask(__name__)

@app.get("/<path:file_path>")
def download_file(file_path: str):
    if '/..' in file_path:
        return 'Invalid path'
    with open(file_path, 'r') as f:
        return f.read()
if __name__ == '__main__':
    app.run()

禁止../

from flask import Flask, request, render_template, send_file
app = Flask(__name__)

@app.get("/<path:file_path>")
def download_file(file_path: str):
    if '../' in file_path:
        return 'Invalid path'
    with open(file_path, 'r') as f:
        return f.read()
if __name__ == '__main__':
    app.run()

与上一个一样,使用正反斜杠绕过

禁止../../和\

省流:正反斜杠G了

from flask import Flask, request, render_template, send_file
app = Flask(__name__)

@app.get("/<path:file_path>")
def download_file(file_path: str):
    if '../../' in file_path or '\\' in file_path:
        return 'Invalid path'
    with open(file_path, 'r') as f:
        return f.read()
if __name__ == '__main__':
    app.run()

这里禁止的是../../,可以使用.././.././代替(/./代表同级目录,穿越漏洞不会受到影响。)人类的创造力是无限的

同样的:禁止../..和\也可以用这个绕过

这个好像还挺罕见的。小本本记下来了,哪天我有机会出题就出这个((

%00截断(php版本<5.3.4)

如果程序试图在用户的输入后面拼接后缀,如:.jpg仅限于php版本<5.3.4时可以用%00截断(C语言特性)

由于本篇文章讨论python中的路径穿越,不多做解释

在某些情况下,问号符也有相同的效果

0x05 例题

【BuckeyeCTF 2024】SSFS

附件app.py:

from flask import Flask, request, render_template, send_file
from uuid import uuid4
import os
import hashlib

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 # 1MB

file_exts = {}

@app.route('/')
def index():
    return render_template('index.html')

def clear_uploads():
    upload_dir = 'uploads'
    if not os.path.exists(upload_dir):
        os.mkdir(upload_dir)

    files = os.listdir(upload_dir)
    if len(files) > 50:
        for file in files:
            os.remove(os.path.join(upload_dir, file))

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['file']
    ext = file.filename.split('.')[-1]

    if not file:
        return {'status': 'error', 'message': 'No file uploaded'}
    
    if ext not in ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']:
        return {'status': 'error', 'message': 'Invalid file type'}
    
    clear_uploads()

    file_id = str(uuid4())
    file_exts[file_id] = ext

    os.makedirs('uploads', exist_ok=True)
    
    with open(f'uploads/{file_id}', 'wb') as f:
        f.write(file.read())

    return {'status': 'success', 'message': 'File uploaded successfully', 'id': file_id}

@app.route('/search/<path:file_id>')
def search(file_id):
    if not os.path.exists('uploads/' + file_id):
        return {'status': 'error', 'message': 'File not found'}, 404

    return {'status': 'success', 'message': 'File found', 'id': file_id}

def filter_file_id(file_id : str):
    if len(file_id) > 36: # uuid4 length
        return None
    
    return file_id

@app.route('/download/<path:file_id>')
def download(file_id):
    file_id = filter_file_id(file_id)

    if file_id is None:
        return {'status': 'error', 'message': 'Invalid file id'}, 400

    if not os.path.exists('uploads/' + file_id):
        return {'status': 'error', 'message': 'File not found'}, 404
    
    if not os.path.isfile('uploads/' + file_id):
        return {'status': 'error', 'message': 'Invalid file id'}, 400

    return send_file('uploads/' + file_id, download_name=f"{file_id}.{file_exts.get(file_id, 'UNK')}")

if __name__ == '__main__':
    app.run(debug=True)

注意到:

return send_file('uploads/' + file_id, download_name=f"{file_id}.

下载,用户输入直接拼接,有权限,无过滤。

如上文,速览dockerfile

注意到

COPY flag.txt /flag.txt

打路径穿越即可

bctf{4lw4y5_35c4p3_ur_p4th5}

最近是又要写作业又要补习又要码博客又要腾时间CTF还要复习期中,国庆属于是拉满了

posted @ 2024-10-03 20:16  LamentXU  阅读(373)  评论(1编辑  收藏  举报