[NISACTF 2022]babyupload
本题考点
- python代码审计
- os.path.join() 处理路径拼接的问题
做题过程
-
经典的文件上传页面,F12查看源码
<!DOCTYPE html> <html> <body> <form action="/upload" method="post" enctype="multipart/form-data"> Select image to upload: <input type="file" name="file"> <input type="submit" value="Upload File" name="submit"> </form> <!-- /source --> </body> </html>
-
发现
/source
路径,访问后得到源码,然后进行代码审计from flask import Flask, request, redirect, g, send_from_directory import sqlite3 import os import uuid # 通用唯一识别码(Universally Unique Identifier),标准格式为xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx app = Flask(__name__) SCHEMA = """CREATE TABLE files ( id text primary key, path text ); """ def db(): g_db = getattr(g, '_database', None) if g_db is None: g_db = g._database = sqlite3.connect("database.db") return g_db @app.before_first_request def setup(): # 建立数据库连接 os.remove("database.db") cur = db().cursor() cur.executescript(SCHEMA) @app.route('/') def hello_world(): return """<!DOCTYPE html> <html> <body> <form action="/upload" method="post" enctype="multipart/form-data"> Select image to upload: <input type="file" name="file"> <input type="submit" value="Upload File" name="submit"> </form> <!-- /source --> </body> </html>""" @app.route('/source') def source(): return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True) @app.route('/upload', methods=['POST']) def upload(): if 'file' not in request.files: return redirect('/') file = request.files['file'] if "." in file.filename: # 检查上传的文件名是否含有点号,即过滤了后缀名 return "Bad filename!", 403 conn = db() cur = conn.cursor() uid = uuid.uuid4().hex # 生成文件的uuid try: # 检查文件是否重复 cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,)) except sqlite3.IntegrityError: return "Duplicate file" conn.commit() file.save('uploads/' + file.filename) # 保存文件在uploads/下 return redirect('/file/' + uid) # 返回文件的uuid @app.route('/file/<id>') def file(id): # 访问文件 conn = db() cur = conn.cursor() cur.execute("select path from files where id=?", (id,)) # 根据文件uuid,在数据库中查询文件名 res = cur.fetchone() # 将数据库查询结果返回 if res is None: return "File not found", 404 # print(res[0]) with open(os.path.join("uploads/", res[0]), "r") as f: # 将查询到的文件名与uploads/拼接 return f.read() if __name__ == '__main__': app.run(host='0.0.0.0', port=80)
-
综上,后端代码的逻辑如下:上传的文件不能有后缀名,上传后生成一个uuid,并将uuid和文件名存入数据库中,并返回文件的uuid。再通过
/file/uuid
访问文件,通过查询数据库得到对应文件名,在文件名前拼接uploads/
后读取该路径下上传的文件。 -
但肯定要想如何读取 flag 文件,在文件名前被
uploads/
拼接意味着只能读取上传后的文件,而且上传的文件没有后缀名,不能直接利用,但os.path.join()
函数存在绝对路径拼接漏洞绝对路径拼接漏洞
os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。
然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径
-
由此,当上传的文件名为 /flag ,上传后通过uuid访问文件后,查询到的文件名是 /flag ,那么进行路径拼接时,uploads/ 将被删除,读取到的就是根目录下的 flag 文件。
-
使用BurpSuite抓包后,修改文件名为 /flag 后发包,利用
/file/uuid
即可读取 flag。