Add the first real endpoint: deliver a payload reliably and predictably, with and without Flask's send_file helper.
I've showed how to create a minimal Flask server that serves a route. I'll expand on this by serving a file for download. For the OSWE course, several of the Proof of Concept (PoC) exploit scripts have required serving a file or two as part of the exploit chain.
We’ll build a small, runnable example first, then discuss behavior and trade-offs.
Minimal Download Server (single route)
Create download_server.py:
from pathlib import Pathfrom flask import Flask, send_file, abortapp =Flask(__name__)PAYLOAD_PATH=Path("payload.bin")ROUTE="/download"# In later parts we'll randomize this for stealth@app.route(ROUTE,methods=["GET"])defhandle_download():ifnotPAYLOAD_PATH.exists():abort(404)returnsend_file(PAYLOAD_PATH,as_attachment=True)if__name__=="__main__": app.run(host="127.0.0.1",port=8888,debug=False)
In the directory where download_server.py is, create or copy a file to payload.bin. The file can be any type; for this example its contents don’t matter.. When download_server.py is started, Flask will bind to localhost or 127.0.0.1 on port 8888. The point is simply to serve payload.bin when /download is requested with GET. The route decorator takes care of setting the route as I showed in the first part of this article series. In addition, one can define/restrict which HTTP Verbs are valid for a given route. I'm restricting /download to only serve the file if using HTTP GET. If someone would try a POST request, they'd receive a 405 Method Not Allowed error.
Unlike Python's http.server module, with Flask, the end user can control what content is visible and actionable. If I put more files in the directory, they will be effectively unavailable to prying eyes.
To create payload.bin, I simply used echo to insert text into the file (echo 'Download Server Test' >> payload.bin). Then, from a terminal start the download server:
To retrieve the file, one can use the curl command as shown below.
Downloading payload.bin using curl
What is going on under the hood, is that I've made a GET request to the download route. The PAYLOAD_PATH is tested to see if the file exists. If /download is requested, but payload.bin isn't present or readable by the Flask server, a 404 error will be returned. You can try this by renaming or moving payload.bin.
Flask returning a 404 error
I've moved payload.bin back and PAYLOAD_PATH.exists() will be True which allows the program to flow to return send_file(PAYLOAD_PATH, as_attachment=True). send_file will send the file contents to the client. Setting as_attachment=True tells the browser, for example, to save the file instead of displaying it. The default is to set as_attachment to False. I could also set the MIME type to say application/pdf or image/jpeg if needed. send_file will try to determine the MIME type from the filename. If the payload is ambiguous, one may want to set this to mimetype="application/octet-stream".
Example:
Now that we can deliver a payload reliably, the next step is to accept file uploads.