Bulk Command Execution Across Multiple Cisco IOS Devices Over SSH From Windows

Cisco have just published an advisory for CVE-2023-20198 - Cisco IOS XE Software Web UI Privilege Escalation Vulnerability. This vulnerability allows for administrative (privilege 15) level accounts to be created via unauthenticated access to the web management UI of Cisco IOS XE devices.

The official advisory is here, and there is a Talos writeup here.

At the time of writing there is no patch and the recommendation from Cisco is to either disable the HTTP/S web interface, or limit access from trusted networks.

Given the critical nature of this vulnerability I wanted to share two ‘quick and dirty’ methods I’ve used in the past to execute SSH commands across a fleet Cisco IOS / IOS XE devices from a Window system. This assumes you have no centralised management in place and just need something up and running ASAP.

To keep the script examples as minimalist as possible I’m going to be making some assumptions

  • You have the IPs for all the devices you need to audit in a file called ips.txt. One IP per line.
  • You have a single account that works across all devices

In these examples I’ll be executing a few of the commands listed in the Cisco advisory, but there are some things to note here. I’ll be checking the log buffer for SYS-5-CONFIG_P entires, but these logs can be cleared with the clear log command or with a device reload. An attacker could also have disabled the web UI after giving themselves access via other means (SSH, etc). This is not a guide on auditing systems for indicators of compromise, this is just a guide on automating command execution on a collection of Cisco devices from a Windows machine, and I’m using the commands in the advisory as an example.

Let’s get started.

Using Posh-SSH to automate Cisco IOS / IOS XE commands

This method uses the PowerShell SSH module which can be installed via the PSGallery.

Install-Module -Name Posh-SSH -Scope CurrentUser

The PowerShell script is a single function that takes an IP address, a PSCredential object, and an array containing commands to execute.

function Configure-Cisco {
    Param (
        [Parameter(Mandatory)]
        [string]$IP,
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCredential]$Credentials,
        [Parameter(Mandatory)]
        [array]$Commands

    )

    try {
        Write-Host "$IP`: Creating new SSH Session"
        $SSH = New-SSHSession -ComputerName $IP -Credential $Credentials -AcceptKey -Force -ConnectionTimeout 10 -ErrorAction Stop -WarningAction SilentlyContinue
        $Stream = New-SSHShellStream -SSHSession $SSH 

        Write-Host "  + Executing commands"
        $Commands | % { $Stream.WriteLine($_); Start-Sleep -Seconds 1 }
        $Output = $Stream.Read().Trim()
        Write-Host "  + Removing SSH Session"
        Get-SSHSession | Remove-SSHSession | Out-Null
    
        return [PSCustomObject]@{
            IP     = $IP
            Output = $Output
        }
    }
    catch {
        Write-Host "  + Error at $IP" -ForegroundColor Red
        Write-Host "  + $_" -ForegroundColor Red
    }
}

The Commands parameter is an array of commands to execute on the device.

$Commands = @(
    "term len 0", 
    "show running-config | in ip http server|secure|active", 
    "show logging | in SYS-5-CONFIG_P"
)

Usage example:

$Creds = Get-Credential
$Data = Get-Content ips.txt | % {Configure-Cisco -IP $_ -Credentials $Creds -Commands $Commands}

Result - including one “error” due to bad password.

PS C:\> $Data = Get-Content ips.txt | % {Configure-Cisco -IP $_ -Credentials $Creds -Commands $Commands}

1.1.1.1: Creating new SSH Session
  + Executing commands
  + Removing SSH Session
2.2.2.2: Creating new SSH Session
  + Executing commands
  + Removing SSH Session
3.3.3.3: Creating new SSH Session
  + Executing commands
  + Removing SSH Session
4.4.4.4: Creating new SSH Session
  + Error at 4.4.4.4
  + Permission denied (password).
5.5.5.5: Creating new SSH Session
  + Executing commands
  + Removing SSH Session

PS C:\> $Data | fl

IP     : 1.1.1.1
Output : router1#term len 0
         router1#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router1#show logging | in SYS-5-CONFIG_P
         router1#

IP     : 2.2.2.2
Output : router2#term len 0
         router2#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router2#show logging | in SYS-5-CONFIG_P
         router2#

IP     : 3.3.3.3
Output : router3#term len 0
         router3#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router3#show logging | in SYS-5-CONFIG_P
         router3#

IP     : 5.5.5.5
Output : router5#term len 0
         router5#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router5#show logging | in SYS-5-CONFIG_P
         router5#

That’s it for method one. Method two involves plink.exe which comes bundled with PuTTY.

Method 2 - Using plink.exe to automate Cisco IOS / IOS XE commands

Same assumptions as before, device IP addresses in a file called ips.txt, and a single credential for all devices. Here I’m also assuming you have plink.exe in your PATH.

This time we will place our commands in a text file called commands.txt

C:\temp\commands.txt

term len 0
show running-config | in ip http server|secure|active
show logging | in SYS-5-CONFIG_P

Very minimal PowerShell here, we just want to avoid putting credentials on the console where they can be logged.

$Cred = Get-Credential
$Data = Get-Content ips.txt | % { 
    $Output = cmd.exe /c "plink.exe -ssh $_ -l $($Cred.Username) -pw $($Cred.GetNetworkCredential().Password) -batch < C:\temp\commands.txt"
    return [PSCustomObject]@{
        IP     = $_
        Output = ($Output -join "`r`n").Trim()
    }
}

The output here is going to be a little different. Login banners will be dumped to the console along with authentication errors/etc., but the $Data variable will still contain similar values as before.

PS C:\> $Data | fl

IP     : 1.1.1.1
Output : router1#term len 0
         router1#
         router1#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router1#
         router1#show logging | in SYS-5-CONFIG_P
         router1#
         router1#

IP     : 2.2.2.2
Output : router2#term len 0
         router2#
         router2#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router2#
         router2#show logging | in SYS-5-CONFIG_P
         router2#
         router2#

IP     : 3.3.3.3
Output : router3#term len 0
         router3#
         router3#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router3#
         router3#show logging | in SYS-5-CONFIG_P
         router3#
         router3#

IP     : 4.4.4.4
Output :

IP     : 5.5.5.5
Output : router4#term len 0
         router4#
         router4#show running-config | in ip http server|secure|active
         no ip http server
         no ip http secure-server
         router4#
         router4#show logging | in SYS-5-CONFIG_P
         router4#
         router4#

Once difference here is the router we have a credential error on (router4 - 4.4.4.4) is still present in the output, just with no associated data.

Other methods

While not in scope for this post I’m a big fan of exporting Cisco configurations to a Git repository or similar so changes can be tracked and a recent backup of the device config is always available. There are various ways to accomplish this, it can be as simple as using the above scripts but I wouldn’t recommend it. There are better commercial and free tools available designed for this very purpose.

RANCID was popular back in the day but has not been maintained, Oxidized is the more modern replacement, however, I’ve been liking Jazigo lately.

Once you have exported configurations you can use PowerShell or other tools to parse the config and look for devices with potentially vulnerable configurations. One big advantage of using source control for configuration versioning is the ability to go back and view historical configurations - this is going to allow you to see whether at attacker has potentially abused a vulnerable system and closed the door behind them by turning off the HTTP/S management UI.


If you enjoyed this post consider sharing it on , , , or , and .