Bulk Command Execution Across Multiple Cisco IOS Devices Over SSH From Windows
Posted on October 18, 2023
- and tagged as
- cisco,
- powershell,
- security
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.