Skip to content
Subprocess

Subprocess

Running local commands from exploit scripts with readable shell command strings.

Run a command and wait for output

Use subprocess.run() when the command output is needed immediately. It starts the command and blocks Python’s main thread until the command exits. The next line of Python runs only after the command finishes.

subprocess.run() returns a CompletedProcess object containing the finished command’s standard output, standard error, and exit code.

import subprocess

absolute_payload_path = "<ABSOLUTE_PAYLOAD_PATH>"
command = f"php {absolute_payload_path}"
process = subprocess.run(command, shell=True, capture_output=True, text=True)

stdout = process.stdout.strip()
stderr = process.stderr
return_code = process.returncode

Standard output and standard error are the two byte streams a process normally uses for regular output and error messages. capture_output=True stores both streams instead of inheriting the terminal. text=True decodes them into Python strings instead of returning bytes. shell=True passes the readable command string to the system shell, so shell syntax such as pipes, redirection, command substitution, and environment-variable expansion remains available.

This is commonly used for in-band stages. A payload generator, decoder, or local command must finish before its output can be inserted into the next request.

Find by: subprocess, subprocess run, run command, wait for command, blocking process, capture output, stdout, stderr, return code, completedprocess, shell true, in band

Start a process without blocking Python

Use subprocess.Popen() when the command output is not needed immediately or when the process must keep running while the exploit continues. It starts the command and returns immediately, so Python’s main thread can continue to the next line.

Popen() starts a separate operating-system process, not a Python thread. The result is similar for the exploit: the new process runs alongside Python without blocking the main thread. This is useful for listeners, tunnels, callback servers, and hosted payload servers.

Popen() returns a process handle. The handle keeps the process identifier and provides methods such as terminate() for later cleanup.

import subprocess

command = "exec python3 -m http.server 8000"
process = subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)

# run later exploit stages here

process.terminate()

With shell=True, Python initially starts a shell process to interpret the command string. Without exec, that shell starts the long-running command as another child process, while the Popen handle continues to identify the shell. Terminating only the shell can leave its child running.

exec is a shell builtin that replaces the shell’s running program with python3 without creating another process. The process identifier remains unchanged, so the existing Popen handle now identifies the HTTP server itself. process.terminate() consequently sends the termination signal to the intended long-running process.

The choice is direct: run() waits because the output is needed now; Popen() returns immediately because the process must keep running or its output is not needed yet.

Find by: subprocess, popen, background process, long running process, process handle, terminate, exec shell, devnull, http server, shell true, blind exploitation, out of band, oob, listener, tunnel