Finding Cisco IOS XE CVE-2023-20198 With ConfigSources

  • 19 October 2023
  • 1 reply
  • 110 views

Userlevel 3
Badge +1

On October 16, 2023, Cisco published a vulnerability that affects IOS XE machines running the built-in web server: https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxe-webui-privesc-j22SaA4z

 

This is tracked as https://nvd.nist.gov/vuln/detail/CVE-2023-20198

 

By adding a simple Config Check to an existing Cisco IOS ConfigSource, LogicMonitor can help people quickly identify which resources have the web server enabled.  Here is an example:

 

  1. Name: Cisco-CSCwh87343-Check
  2. Check type: "Use Groovy Script"
  3. Groovy script: 
    /*
    The built-in string variable 'config' contains the entire contents of the configuration file.
    The following example will trigger an alert when the configuration file contains the string "blue".
    if (config.contains("blue")) {
    return 1;
    } else {
    return 0;
    }
    */

    if (config.contains("ip http")) {
    return 1;
    } else {
    return 0;
    }

     

  4. Then trigger this type of alert: Warning
  5. Description: "Search for presence of Cisco
    CSCwh87343 vulnerability"

 

 

Caveats: 

-This will apply to all devices where the ConfigSource is used, even though all devices may not be affected by the vulnerability

-This assumes usage of ConfigSources and specifically the Cisco_iOS ConfigSource

 

Thanks to Todd Ritter for finding this CVE and Creating the ConfigSource

 


1 reply

Userlevel 7
Badge +20

I built a standalone property source for this. Checking if “ip http” server is in the config is not enough (what if “no ip http server” is present?). Also, it’s a good idea to highlight the ones that have public ip addresses. Also, there’s a mitigation for it that you can check for.

Here’s my PS code. I apply it to “system.sysinfo =~ "IOSXE" && ssh.user && ssh.pass && isCisco()”:

import com.santaba.agent.groovyapi.expect.Expect
import com.santaba.common.groovyapi.expect.expectj.ExpectJ
import java.util.regex.Pattern
import com.jcraft.jsch.JSch

deviceType = hostProps.get("system.categories")

if (deviceType.contains("Cisco")){
def device = null
try {
device = new CiscoIOSDevice(hostProps)
} catch (all) {
println("http.server.error=Fail to initiate device object: ${all.getMessage()}")
throw new RuntimeException("Failure to initiate the device.\nMessage: ${all.getMessage()}")
return 1
}
try {
runningConfig = device.LM_getCollectionOutput("running-config | include ip http se")
}
catch (all) {
println("http.server.error=Fail to fetch running config: ${all.getMessage()}")
throw new RuntimeException("Failure to fetch the running config.\nMessage: ${all.getMessage()}")
return 2
}
ipaddresses = hostProps.get("system.ips", "")
device.Cisco_LogoutDevice()
runningConfig.eachLine{
if (it.contains("ip http server")){
if (it.tokenize(" ")[0] == "no"){println("http.server=false")}
else {println("http.server=true")}
}
if (it.contains("ip http secure-server")){
if (it.tokenize(" ")[0] == "no"){println("http.server.secure=false")}
else {println("http.server.secure=true")}
}
if (it.contains("ip http active-session-modules none")){
if (it.tokenize(" ")[0] == "no"){println("http.server.active_session_modules=false")}
else {println("http.server.active_session_modules=true")}
}
if (it.contains("ip http secure-active-session-modules none")){
if (it.tokenize(" ")[0] == "no"){println("http.server.secure_active_session_modules=false")}
else {println("http.server.secure_active_session_modules=true")}
}
}
privateips = []
publicips = []
ipaddresses.tokenize(",").each{
try{
publicips.add((it =~ /\b(?!(10(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2[0-9]|3[0-1])(\.\d{1,3}){2}))(\d{1,3}(\.\d{1,3}){3})\b/)[0][0])
} catch (x){
try {
privateips.add((it =~ /\b(([0-9]{1,3}\.){3}[0-9]{1,3}\b)/)[0][0])
} catch (y){}
}
}
println("http.server.private.ips=${privateips.join(',')}")
println("http.server.public.ips=${publicips.join(',')}")
return 0
} else {
println("http.server.error=Not a supported device type")
}

/**
* Cisco IOS command execution
*/
class CiscoIOSDevice {
private hostProps = null
private host = null
private user = [:]
private pass = [:]
private creds = [:]
private enable_pass = []// If the user has specified an enable password, this will get set further down.
private enable_level = null
private priv_exec_mode = false // Notes what EXEC mode we are in. Initially we assume USER EXEC.
private escalated_privs = false // If we need to escalate our prives from $ to #, we should know.
private parser_view = false

private enum Cisco_ENUMUserPrivilegeState {
USER, PRIV
}
private user_privilege_mode = Cisco_ENUMUserPrivilegeState.USER

private Expect cli

private prompt = ""
private mode = ">"
private full_prompt = ""

// The following variables are set to show what type of IOS device we have, based off the sysinfo property.
private sysinfo = null
private isCisco_ASA = false
private isCisco_WAAS = false
private isCisco_c6800 = false
private isCisco_Cat_3750 = false
private isCisco_Cat = false
private isCisco_PIX = false
private isCisco_Standard = false

private enum CiscoModel {
STANDARD, ASA, WAAS, CATALYST, CATALYST_3750, PIX, C6800
}
private CiscoModel cisco_model

private raw_show_output = null
private show_entries = null

private use_ssh = true // We'll assume ssh unless otherwise specified.
private telnet_port = "23"

private connected = false // Describes the current device ssh connectivity state.
private script_timeout = 60 // seconds
private max_conn_attempts = 3 // Attempt the ssh connection this many times after failure.
private conn_attempt_pause = 5 // Seconds to pause between ssh connection attempts.

private error_message = null
// Get's populated with the last critical error. Used by the init method when throwing an exception


CiscoIOSDevice(hostProps) {
if (!init(hostProps)) {
if (error_message) {
throw new RuntimeException("Initialization process failed! Reason: \"${error_message}\".\n\n")
}
else {
throw new RuntimeException("Initialization process failed!\n\n")
}
}
}

/**
* Called by the Class Constructor, this is the main 'loop' that calls the necessary methods to make a successful connection
* to the device. It is also responsible for multi-attempt logic.
* @param hostProps The hostProps object retrieved from the CollectorDB, and used to extract necessary device properties
* @return TRUE if successful. FALSE otherwise.
*/
private boolean init(hostProps) {
this.hostProps = hostProps
get_lm_props()
Cisco_DetermineIOSDeviceType()

def conn_attempt_num = 1
connected = false

while (!connected && conn_attempt_num < (max_conn_attempts + 1)) {
// Attempt to connect to the device. If we fail, and the failure occurs after connectivity is established,
// we want to disconnect from the device. This is typically means we received a "Permission denied" error.
if (!Cisco_ConnectToDevice()) {
if (connected) {
Cisco_LogoutDevice()
}
error_message = "Could not establish ssh session with host"
}

// Check if we are within a parser view.
if (connected) {
Cisco_ShowParserView()
}

//if ( connected && !escalate_device_privs() ) {
if (connected && !Cisco_EscalateDevicePrivileges()) {
if (connected) {
Cisco_LogoutDevice()
}
error_message = "Could not properly escalate privileges on the device. Ensure that 'ssh.enable.pass' property has been set for the device if necessary"
}

if (connected && !Cisco_SetTerminal()) {
if (connected) {
Cisco_LogoutDevice()
}
}

if (!connected) {
conn_attempt_num++
sleep(conn_attempt_pause * 1000)
}
}

return connected && escalated_privs
}

private get_lm_props() {
// Grab all the hostProp entries
def propKeys = this.hostProps.keySet()

// Iterate over the hostProp entries, determining what values we have set
propKeys.each { key ->
switch (key.toLowerCase()) {
case ~/^config\.forceview$/:
if(this.hostProps.get(key).toLowerCase() == "true") {
this.parser_view = true;
}
break
case ~/^system\.hostname$/:
this.host = this.hostProps.get(key)
break
case ~/ssh\.user$/:
this.creds.put this.hostProps.get(key).toString(), this.hostProps.get("ssh.pass")
if (!this.enable_pass.contains(this.hostProps.get("ssh.pass"))) {
this.enable_pass.add(this.hostProps.get("ssh.pass"))
}
break
case ~/config\.user$/:
this.creds.put this.hostProps.get(key).toString(), this.hostProps.get("config.pass")
if (!this.enable_pass.contains((this.hostProps.get("config.pass")))) {
this.enable_pass.add(this.hostProps.get("config.pass"))
}
break
case ~/ssh\.enable\.pass$/:
if (!this.enable_pass.contains((this.hostProps.get("ssh.enable.pass")))) {
this.enable_pass = ["${this.hostProps.get("ssh.enable.pass")}", *this.enable_pass]
}
break
case ~/config\.enable\.pass$/:
if (!this.enable_pass.contains((this.hostProps.get("config.enable.pass")))) {
this.enable_pass = ["${this.hostProps.get("config.enable.pass")}", *enable_pass]
}
break
case ~/^system\.sysinfo$/:
this.sysinfo = this.hostProps.get(key)
break
case ~/^(config|ssh)\.enable\.level$/:
this.enable_level = this.hostProps.get(key)
break
case ~/^configsource\.use\.telnet$/:
if (this.hostProps.get(key) == "1" || this.hostProps.get(key) == "true" || this.hostProps.get(key) == "yes") {
this.use_ssh = false
}
break
case ~/^configsource\.telnet\.port/:
this.telnet_port = this.hostProps.get(key)
break
case ~/^configsource\.script\.timeout/:
this.script_timeout = (this.hostProps.get(key).toInteger() > 0 && this.hostProps.get(key).toInteger() <= 300) ? this.hostProps.get(key).toInteger() : 60
break
}
}

/*
* In this section of code, we test the values of host, user, and pass -- ensuring
* they did indeed get set. If not, the script will certainly fail, so catch it
* now and provide a useful error message.
*/
if (!this.host) {
System.err << "(debug::fatal) Could not retrieve the system.hostname value. Exiting."
return false
}

if (this.creds.isEmpty()) {
System.err << "(debug::fatal) Could not retrieve the ssh.user/ssh.pass device property. This is required for authentication. Exiting."

return false
}

return true
}

/**
* Takes the SYSINFO value from hostProps and attempts to determine what sort of underlying IOS platform we are working with.
* Some devices behave differently and thus require different logic.
* @return
*/
private Cisco_DetermineIOSDeviceType() {
switch (this.sysinfo.trim()) {
case { it.startsWith("Cisco Adaptive Security Appliance") }:
this.isCisco_ASA = true
this.cisco_model = CiscoModel.ASA
break
case { it.startsWith("Cisco Wide Area Application Services") }:
this.isCisco_WAAS = true
this.cisco_model = CiscoModel.WAAS
break
case { it.startsWith("Cisco IOS Software, c68") }:
this.isCisco_c6800 = true
this.cisco_model = CiscoModel.C6800
break
case { it.contains("C3750") }:
this.isCisco_Cat_3750 = true
this.cisco_model = CiscoModel.CATALYST_3750
break
case ~/^(?:Cisco )?IOS Software(?:\s\[.*\])?,?\s*Catalyst.*, .*Version\s+(.*)/:
this.isCisco_Cat = true
this.isCisco_Standard = true
break
case { it.startsWith("Cisco IOS") }:
this.isCisco_Standard = true
break
case { it.contains("Cisco PIX") }:
this.isCisco_PIX = true
break
default:
this.isCisco_Standard = true
break
}
}

/**
* This method is responsible for establishing the connection to a device.
* @return TRUE if successful. FALSE otherwise
*/
private boolean Cisco_ConnectToDevice() {
def err_state = true
if (this.use_ssh) {
// open an ssh connection and wait for the prompt
this.creds.each { lm_username, lm_password ->
try {
if (err_state) {
this.cli = Expect.open(this.host, lm_username, lm_password, this.script_timeout)
err_state = false
}
}
catch (Exception all) {
return false
}
}

if (this.cli == null) {
return false
}
}
else {
this.creds.each { lm_username, lm_password ->
// telnet session
try {
if (err_state) {
this.cli = Expect.open(this.host, this.telnet_port.toInteger(), this.script_timeout)
err_state = false
this.cli.expect("Username:")
this.cli.send(lm_username + "\n")
this.cli.expect("Password:")
this.cli.send(lm_password + "\n")
}
}
catch (Exception all) {
return false
}
}
}

// Let's ensure that the cli object was created OR that we have performed the previous methods without exiting an error state.
// This ensures that the script doesn't continue on if connectivity was not established, returning control back to the retry loop.
if (this.cli == null || err_state) {
return false
}

// Connection successfully established
connected = true

/* INITIAL MATCH ----------------------------------------------------------------------------------------------------------------------------
In order to accommodate ascii art banners and such, we need essentially look for the last line sent to us after connection.
Everything before that tends to be noise.
*/

def raw_prompt_line = ""

try {
// The following expect method should match on most any cisco ios device prompt. Later we can take this value and parse it for the prompt
this.cli.expect('[a-zA-Z0-9\\-\\_\\.\\/]+[>#][\\s+]?\$')
this.cli.send("\n")
this.cli.expect('[a-zA-Z0-9\\-\\_\\.\\/]+[>#][\\s+]?\$')

// TODO: move this over to precompiled regex matchers
raw_prompt_line = ("${this.cli.matched()}" =~ /([a-zA-Z0-9\-_\.\/]+[>#])[\s+]?$/)[0][1]

}
catch (Exception all) {
return false
}

/* -- PROMPT DETECTION ---------------------------------------------------------------------------------------------------------------------
We have progressed this far, we should be able to deconstruct the prompt line into 'prompt' and 'mode'
*/

try {
this.prompt = raw_prompt_line[0..-2]
this.mode = raw_prompt_line[-1]
this.full_prompt = this.prompt + this.mode
}
catch (Exception all) {
return false
}

// Check to see what the previous expect command matched. This will us which user mode we have been dropped into.
if (this.mode == "#") {
this.priv_exec_mode = true
}

return true

}

/**
* This method will attempt to escalate our user privileges to PRIV
* @return boolean Will return TRUE if successful; FALSE otherwise.
*/
private boolean Cisco_EscalateDevicePrivileges() {
if (this.user_privilege_mode == Cisco_ENUMUserPrivilegeState.PRIV) {
this.escalated_privs = true; return true
}
if (this.priv_exec_mode) {
escalated_privs = true; return true
}

def exit_loop = false
def password_position = 0

while (!exit_loop) {
def response
if (password_position == 0) {
this.cli.send("enable\n")
}

try {
this.cli.expect("[Pp]assword:", "${prompt}\\s*#", full_prompt)
response = this.cli.matched()
}
catch (Exception e) { }

if (response =~ /${this.prompt}\s*#/) {
this.mode = "#"
this.full_prompt = Pattern.quote(response)
this.priv_exec_mode = true
this.user_privilege_mode = Cisco_ENUMUserPrivilegeState.PRIV
this.escalated_privs = true
exit_loop = true
}
else {
this.cli.send(enable_pass[password_position] + "\n")
}

if (password_position == enable_pass.size()) {
exit_loop = true
}
else {
password_position++
}
}

return (this.user_privilege_mode == Cisco_ENUMUserPrivilegeState.PRIV)
}

private Cisco_SetTerminal() {
try {
if (this.isCisco_WAAS || this.isCisco_Standard || this.isCisco_c6800 || this.isCisco_Cat_3750) {
this.cli.send("terminal length 0\n")
this.cli.expect(this.full_prompt)
this.cli.send("terminal width 0\n")
this.cli.expect("${this.full_prompt}\\s*\$")
}
else {
this.cli.send("terminal pager 0\n")
this.cli.expect("${this.full_prompt}\\s*\$")
}
}
catch (Exception all) {
return false
}

return true
}

private Cisco_ShowParserView() {
try {
this.cli.send("show parser view\n")
this.cli.expect(this.full_prompt, "No view is active")
this.cli.before()
}
catch (Exception all) {
return false
}

if (this.cli.before().toString().contains("Current view is")) {
this.parser_view = true
}
}

def Cisco_GetShowCommands() {
this.cli.send("show ?")

try {
if (this.isCisco_ASA || this.isCisco_PIX) {
this.cli.expect("${this.full_prompt} show")
}
else if (this.isCisco_WAAS || this.isCisco_c6800) {
this.cli.expect(this.full_prompt)
}
else {
this.cli.expect("${this.full_prompt}show")
}
}
catch (Exception all) {
return false
}

try {
this.raw_show_output = this.cli.before()
}
catch (Exception all) {
return false
}

return this.raw_show_output
}

def Cisco_ParseShowCommands() {
this.raw_show_output.eachLine() { line ->
if (!show_entries) {
show_entries = [:]
}

switch (line.trim()) {
case { it.isEmpty() }:
break
case { it.startsWith("show ?") }:
break
case { it.startsWith(this.full_prompt) }:
break
default:
try {
def cmd = line.trim().tokenize()[0]
def desc = line.trim().tokenize()[1..-1].join(" ")
this.show_entries << ["${cmd}": "${desc.trim()}"]
}
catch (all) {
// some sort of odd output. ignore.
break
}
}
}
}

def LM_getActiveDiscoveryOutput(desired_show_commands) {
// Need to ensure that we are actually connected to the device before proceeding
if (!connected) {
return false
}

def output = ""

if (!this.raw_show_output) {
this.Cisco_GetShowCommands()
}
if (!this.show_entries) {
this.Cisco_ParseShowCommands()
}

this.show_entries.each { key, value ->
if (desired_show_commands.contains(key.toString())) {
output += "${key}##${key}##${value}\n"
}
}

return output
}

def LM_getCollectionOutput(show_operator) {
// Need to ensure that we are actually connected to the device before proceeding
if (!connected) {
return false
}

def output = ""
def response = null

// In the event that we are within a parser view, we need to add 'view full' to a 'running-config' request.
if (parser_view && show_operator == "running-config") {
show_operator += " view full"
}

def conn_attempt_num = 1

while ((conn_attempt_num < (this.max_conn_attempts + 1)) && response == null) {
conn_attempt_num++

// Send the requested show command to the device
if (this.isCisco_c6800 || this.isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA || this.isCisco_Cat) {
if (this.escalated_privs) {
this.cli.send("show ${show_operator}\nexit\nexit\n")
}
else {
this.cli.send("show ${show_operator}\nexit\n")
}
}
else {
this.cli.send("show ${show_operator}\n")
}

// Attempt to match
try {
if (isCisco_c6800 || isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA) {
this.cli.expectClose()

response = this.cli.stdout()
}
else {
// Match prompt on a line by itself with or without surrounding whitespace.
// Obviate case where config returns with embedded prompt and terminates
// the config prematurely.
this.cli.expect(/^\s*${this.full_prompt}\s*$/)

response = this.cli.before()
}
}
catch (Exception all) { }
}

response = response.trim()

try {
def found_current_config_entry = true

if (this.isCisco_c6800 || this.isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA) {
found_current_config_entry = false
}

response.eachLine() { line ->
switch (line) {
case ~/^(.show|show) ${show_operator}/:
if (!found_current_config_entry) {
found_current_config_entry = true
}
break
case ~/^: Saved/:
if (!found_current_config_entry) {
found_current_config_entry = true
}
output += "${line}\n"
break
case ~/^${full_prompt}\s?show ${show_operator}.*/:
if (!found_current_config_entry) {
found_current_config_entry = true
}
break
case ~/^Building configuration.*/:
break
case ~/.+\d+ (day|days|hour|hours|year|years|week|weeks|minute|minutes).*/:
break
case ~/(^[Ss]witch\s|^)[Uu]ptime.*/:
break
case ~/^${full_prompt}.*/:
break
case ~/^(Current configuration|Using \d+ out of \d+ bytes).*/:
found_current_config_entry = true
break
case ~/^ntp clock-period.*/:
break
case ~/^Logoff/:
break
case ~/^Load for five secs.*/:
break
case ~/^Time source is.*/:
break
case ~/^\w+\s+\w+\s+\d+\s\d+:\d+:\d+\.\d+\s+\w+/:
break
default:
if (found_current_config_entry) {
output += "${line}\n"
}
break
}
}
}
catch (Exception all) { }

return output
}

def Cisco_LogoutDevice() {
// If we were never connected, just return true. Most likely poor logic that allowed us to call this method even though we never established a connection.
if (!connected) {
return true
}

if (this.isCisco_c6800 || this.isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA || this.isCisco_Cat) {
return true
} // The c6800 and Catalyst 3750 series routines will have already exited the device.

// logout from the device
if (this.escalated_privs) {
this.cli.send("\nexit\nexit\n")
this.cli.expect("\$")
}
else {
this.cli.send("\nexit\n");
this.cli.expect("#", "${this.full_prompt}exit")
}

connected = false

// close the ssh connection handle then print the config
try {
this.cli.expectClose()
}
catch (Exception all) {
return false
}
}
}

 

Reply