From b9c8c63501c51b66693025b8e2f0a174bf162fea Mon Sep 17 00:00:00 2001 From: h00die Date: Tue, 26 Nov 2024 19:00:14 -0500 Subject: [PATCH 1/3] lib post linux comments and specs --- lib/msf/core/post/linux/busy_box.rb | 2 + lib/msf/core/post/linux/compile.rb | 189 ++++--- lib/msf/core/post/linux/kernel.rb | 635 ++++++++++++----------- lib/msf/core/post/linux/packages.rb | 33 ++ lib/msf/core/post/linux/priv.rb | 310 ++++++----- lib/msf/core/post/linux/process.rb | 64 +-- lib/msf/core/post/linux/system.rb | 29 +- spec/lib/msf/core/post/linux/kernel.rb | 112 ++++ spec/lib/msf/core/post/linux/packages.rb | 42 ++ 9 files changed, 878 insertions(+), 538 deletions(-) create mode 100644 lib/msf/core/post/linux/packages.rb create mode 100644 spec/lib/msf/core/post/linux/kernel.rb create mode 100644 spec/lib/msf/core/post/linux/packages.rb diff --git a/lib/msf/core/post/linux/busy_box.rb b/lib/msf/core/post/linux/busy_box.rb index 665aff49c6d6..77784cee192c 100644 --- a/lib/msf/core/post/linux/busy_box.rb +++ b/lib/msf/core/post/linux/busy_box.rb @@ -47,7 +47,9 @@ def busy_box_is_writable_dir?(dir_path) end # Checks some directories that usually are writable in devices running busybox + # # @return [String] If the function finds a writable directory, it returns the path. Else it returns nil + # def busy_box_writable_dir dirs = %w(/etc/ /mnt/ /var/ /var/tmp/) diff --git a/lib/msf/core/post/linux/compile.rb b/lib/msf/core/post/linux/compile.rb index 562f962a1bd6..e05b830a39b6 100644 --- a/lib/msf/core/post/linux/compile.rb +++ b/lib/msf/core/post/linux/compile.rb @@ -1,88 +1,109 @@ # -*- coding: binary -*- -module Msf -class Post -module Linux -module Compile - include ::Msf::Post::Common - include ::Msf::Post::File - include ::Msf::Post::Unix - - def initialize(info = {}) - super - register_options( [ - OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]), - OptEnum.new('COMPILER', [true, 'Compiler to use on target', 'Auto', ['Auto', 'gcc', 'clang']]), - ], self.class) - end - - def get_compiler - if has_gcc? - return 'gcc' - elsif has_clang? - return 'clang' - else - return nil - end - end - - def live_compile? - return false unless %w{ Auto True }.include?(datastore['COMPILE']) - - if datastore['COMPILER'] == 'gcc' && has_gcc? - vprint_good 'gcc is installed' - return true - elsif datastore['COMPILER'] == 'clang' && has_clang? - vprint_good 'clang is installed' - return true - elsif datastore['COMPILER'] == 'Auto' && get_compiler.present? - return true - end - unless datastore['COMPILE'] == 'Auto' - fail_with Module::Failure::BadConfig, "#{datastore['COMPILER']} is not installed. Set COMPILE False to upload a pre-compiled executable." - end - - false - end - - def upload_and_compile(path, data, compiler_args='') - write_file "#{path}.c", strip_comments(data) - - compiler = datastore['COMPILER'] - if datastore['COMPILER'] == 'Auto' - compiler = get_compiler - fail_with(Module::Failure::BadConfig, "Unable to find a compiler on the remote target.") unless compiler.present? - end - - compiler_cmd = "#{compiler} -o '#{path}' '#{path}.c'" - if session.type == 'shell' - compiler_cmd = "PATH=\"$PATH:/usr/bin/\" #{compiler_cmd}" - end - - unless compiler_args.to_s.blank? - compiler_cmd << " #{compiler_args}" - end - - verification_token = Rex::Text.rand_text_alphanumeric(8) - success = cmd_exec("#{compiler_cmd} && echo #{verification_token}")&.include?(verification_token) - - rm_f "#{path}.c" - - unless success - message = "#{path}.c failed to compile." - # don't mention the COMPILE option if it was deregistered - message << ' Set COMPILE to False to upload a pre-compiled executable.' if options.include?('COMPILE') - fail_with Module::Failure::BadConfig, message +module Msf + class Post + module Linux + module Compile + include ::Msf::Post::Common + include ::Msf::Post::File + include ::Msf::Post::Unix + + def initialize(info = {}) + super + register_options([ + OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]), + OptEnum.new('COMPILER', [true, 'Compiler to use on target', 'Auto', ['Auto', 'gcc', 'clang']]), + ], self.class) + end + + # Determines the available compiler on the target system. + # + # @return [String, nil] The name of the compiler ('gcc' or 'clang') if available, or nil if none are found. + def get_compiler + if has_gcc? + return 'gcc' + elsif has_clang? + return 'clang' + else + return nil + end + end + + # Checks whether the target supports live compilation based on the module's configuration and available tools. + # + # @return [Boolean] True if compilation is supported and a compiler is available; otherwise, False. + # @raise [Module::Failure::BadConfig] If the specified compiler is not installed and compilation is required. + def live_compile? + return false unless %w[Auto True].include?(datastore['COMPILE']) + + if datastore['COMPILER'] == 'gcc' && has_gcc? + vprint_good 'gcc is installed' + return true + elsif datastore['COMPILER'] == 'clang' && has_clang? + vprint_good 'clang is installed' + return true + elsif datastore['COMPILER'] == 'Auto' && get_compiler.present? + return true + end + + unless datastore['COMPILE'] == 'Auto' + fail_with Module::Failure::BadConfig, "#{datastore['COMPILER']} is not installed. Set COMPILE False to upload a pre-compiled executable." + end + + false + end + + # + # Uploads C code to the target, compiles it, and handles verification of the compiled binary. + # + # @param path [String] The path where the compiled binary will be created. + # @param data [String] The C code to compile. + # @param compiler_args [String] Additional arguments for the compiler command. + # @raise [Module::Failure::BadConfig] If compilation fails or no compiler is found. + # + def upload_and_compile(path, data, compiler_args = '') + write_file "#{path}.c", strip_comments(data) + + compiler = datastore['COMPILER'] + if datastore['COMPILER'] == 'Auto' + compiler = get_compiler + fail_with(Module::Failure::BadConfig, 'Unable to find a compiler on the remote target.') unless compiler.present? + end + + compiler_cmd = "#{compiler} -o '#{path}' '#{path}.c'" + if session.type == 'shell' + compiler_cmd = "PATH=\"$PATH:/usr/bin/\" #{compiler_cmd}" + end + + unless compiler_args.to_s.blank? + compiler_cmd << " #{compiler_args}" + end + + verification_token = Rex::Text.rand_text_alphanumeric(8) + success = cmd_exec("#{compiler_cmd} && echo #{verification_token}")&.include?(verification_token) + + rm_f "#{path}.c" + + unless success + message = "#{path}.c failed to compile." + # don't mention the COMPILE option if it was deregistered + message << ' Set COMPILE to False to upload a pre-compiled executable.' if options.include?('COMPILE') + fail_with Module::Failure::BadConfig, message + end + + chmod path + end + + # + # Strips comments from C source code. + # + # @param c_code [String] The C source code. + # @return [String] The C code with comments removed. + # + def strip_comments(c_code) + c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '') + end + end end - - chmod path end - - def strip_comments(c_code) - c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '') - end - -end # Compile -end # Linux -end # Post -end # Msf +end diff --git a/lib/msf/core/post/linux/kernel.rb b/lib/msf/core/post/linux/kernel.rb index 19b2dd8b4ff2..8d3a91472978 100644 --- a/lib/msf/core/post/linux/kernel.rb +++ b/lib/msf/core/post/linux/kernel.rb @@ -1,331 +1,368 @@ # -*- coding: binary -*- module Msf -class Post -module Linux -module Kernel - include ::Msf::Post::Common - include Msf::Post::File - # - # Returns uname output - # - # @return [String] - # - def uname(opts='-a') - cmd_exec("uname #{opts}").to_s.strip - rescue - raise "Failed to run uname #{opts}" - end + class Post + module Linux + module Kernel + include ::Msf::Post::Common + include Msf::Post::File - # - # Returns the kernel release - # - # @return [String] - # - def kernel_release - uname('-r') - end + # + # Returns uname output + # + # @param opt [String] uname options, defaults to -a + # @return [String] + # @raise [RuntimeError] If execution fails. + # + def uname(opts = '-a') + cmd_exec("uname #{opts}").to_s.strip + rescue StandardError + raise "Failed to run uname #{opts}" + end - # - # Returns the kernel version - # - # @return [String] - # - def kernel_version - uname('-v') - end + # + # Returns the kernel release + # + # @return [String] + # + def kernel_release + uname('-r') + end - # - # Returns the kernel name - # - # @return [String] - # - def kernel_name - uname('-s') - end + # + # Returns the kernel version + # + # @return [String] + # + def kernel_version + uname('-v') + end - # - # Returns the kernel hardware - # - # @return [String] - # - def kernel_hardware - uname('-m') - end + # + # Returns the kernel name + # + # @return [String] + # + def kernel_name + uname('-s') + end - # - # Returns the kernel hardware architecture - # Based on values from https://en.wikipedia.org/wiki/Uname - # - # @return [String] - # - def kernel_arch - arch = kernel_hardware - return ARCH_X64 if arch == 'x86_64' || arch == 'amd64' - return ARCH_AARCH64 if arch == 'aarch64' || arch == 'arm64' - return ARCH_ARMLE if arch.start_with?'arm' - return ARCH_X86 if arch.end_with?'86' - return ARCH_PPC if arch == 'ppc' - return ARCH_PPC64 if arch == 'ppc64' - return ARCH_PPC64LE if arch == 'ppc64le' - return ARCH_MIPS if arch == 'mips' - return ARCH_MIPS64 if arch == 'mips64' - return ARCH_SPARC if arch == 'sparc' - return ARCH_RISCV32LE if arch == 'riscv32' - return ARCH_RISCV64LE if arch == 'riscv64' - return ARCH_LOONGARCH64 if arch == 'loongarch64' - arch - end + # + # Returns the kernel hardware + # + # @return [String] + # + def kernel_hardware + uname('-m') + end - # - # Returns the kernel boot config - # - # @return [Array] - # - def kernel_config - release = kernel_release - output = read_file("/boot/config-#{release}").to_s.strip - return if output.empty? - config = output.split("\n").map(&:strip).reject(&:empty?).reject {|i| i.start_with? '#'} - config - rescue - raise 'Could not retrieve kernel config' - end + # + # Returns the kernel hardware architecture + # Based on values from https://en.wikipedia.org/wiki/Uname + # + # @return [String] + # + def kernel_arch + arch = kernel_hardware + return ARCH_X64 if arch == 'x86_64' || arch == 'amd64' + return ARCH_AARCH64 if arch == 'aarch64' || arch == 'arm64' + return ARCH_ARMLE if arch.start_with? 'arm' + return ARCH_X86 if arch.end_with? '86' + return ARCH_PPC if arch == 'ppc' + return ARCH_PPC64 if arch == 'ppc64' + return ARCH_PPC64LE if arch == 'ppc64le' + return ARCH_MIPS if arch == 'mips' + return ARCH_MIPS64 if arch == 'mips64' + return ARCH_SPARC if arch == 'sparc' + return ARCH_RISCV32LE if arch == 'riscv32' + return ARCH_RISCV64LE if arch == 'riscv64' + return ARCH_LOONGARCH64 if arch == 'loongarch64' - # - # Returns the kernel modules - # - # @return [Array] - # - def kernel_modules - read_file('/proc/modules').to_s.scan(/^[^ ]+/) - rescue - raise 'Could not determine kernel modules' - end + arch + end - # - # Returns a list of CPU flags - # - # @return [Array] - # - def cpu_flags - cpuinfo = read_file('/proc/cpuinfo').to_s + # + # Returns the kernel boot config with comments removed + # + # @return [Array] + # @raise [RuntimeError] If execution fails. + # + def kernel_config + release = kernel_release + output = read_file("/boot/config-#{release}").to_s.strip + return if output.empty? - return unless cpuinfo.include? 'flags' + config = output.split("\n").map(&:strip).reject(&:empty?).reject { |i| i.start_with? '#' } + config + rescue StandardError + raise 'Could not retrieve kernel config' + end - cpuinfo.scan(/^flags\s*:(.*)$/).flatten.join(' ').split(/\s/).map(&:strip).reject(&:empty?).uniq - rescue - raise'Could not retrieve CPU flags' - end + # + # Returns the kernel modules + # + # @return [Array] + # @raise [RuntimeError] If execution fails. + # + def kernel_modules + read_file('/proc/modules').to_s.scan(/^[^ ]+/) + rescue StandardError + raise 'Could not determine kernel modules' + end - # - # Returns true if kernel and hardware supports Supervisor Mode Access Prevention (SMAP), false if not. - # - # @return [Boolean] - # - def smap_enabled? - cpu_flags.include? 'smap' - rescue - raise 'Could not determine SMAP status' - end + # + # Returns a list of CPU flags + # + # @return [Array] + # @raise [RuntimeError] If execution fails. + # + def cpu_flags + cpuinfo = read_file('/proc/cpuinfo').to_s - # - # Returns true if kernel and hardware supports Supervisor Mode Execution Protection (SMEP), false if not. - # - # @return [Boolean] - # - def smep_enabled? - cpu_flags.include? 'smep' - rescue - raise 'Could not determine SMEP status' - end + return unless cpuinfo.include? 'flags' - # - # Returns true if Kernel Address Isolation (KAISER) is enabled - # - # @return [Boolean] - # - def kaiser_enabled? - cpu_flags.include? 'kaiser' - rescue - raise 'Could not determine KAISER status' - end + cpuinfo.scan(/^flags\s*:(.*)$/).flatten.join(' ').split(/\s/).map(&:strip).reject(&:empty?).uniq + rescue StandardError + raise 'Could not retrieve CPU flags' + end - # - # Returns true if Kernel Page-Table Isolation (KPTI) is enabled, false if not. - # - # @return [Boolean] - # - def kpti_enabled? - cpu_flags.include? 'pti' - rescue - raise 'Could not determine KPTI status' - end + # + # Returns true if kernel and hardware supports Supervisor Mode Access Prevention (SMAP), false if not. + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def smap_enabled? + cpu_flags.include? 'smap' + rescue StandardError + raise 'Could not determine SMAP status' + end - # - # Returns true if user namespaces are enabled, false if not. - # - # @return [Boolean] - # - def userns_enabled? - return false if read_file('/proc/sys/user/max_user_namespaces').to_s.strip.eql? '0' - return false if read_file('/proc/sys/kernel/unprivileged_userns_clone').to_s.strip.eql? '0' - true - rescue - raise 'Could not determine userns status' - end + # + # Returns true if kernel and hardware supports Supervisor Mode Execution Protection (SMEP), false if not. + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def smep_enabled? + cpu_flags.include? 'smep' + rescue StandardError + raise 'Could not determine SMEP status' + end - # - # Returns true if Address Space Layout Randomization (ASLR) is enabled - # - # @return [Boolean] - # - def aslr_enabled? - aslr = read_file('/proc/sys/kernel/randomize_va_space').to_s.strip - (aslr.eql?('1') || aslr.eql?('2')) - rescue - raise 'Could not determine ASLR status' - end + # + # Returns true if Kernel Address Isolation (KAISER) is enabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def kaiser_enabled? + cpu_flags.include? 'kaiser' + rescue StandardError + raise 'Could not determine KAISER status' + end - # - # Returns true if Exec-Shield is enabled - # - # @return [Boolean] - # - def exec_shield_enabled? - exec_shield = read_file('/proc/sys/kernel/exec-shield').to_s.strip - (exec_shield.eql?('1') || exec_shield.eql?('2')) - rescue - raise 'Could not determine exec-shield status' - end + # + # Returns true if Kernel Page-Table Isolation (KPTI) is enabled, false if not. + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def kpti_enabled? + cpu_flags.include? 'pti' + rescue StandardError + raise 'Could not determine KPTI status' + end - # - # Returns true if unprivileged bpf is disabled - # - # @return [Boolean] - # - def unprivileged_bpf_disabled? - unprivileged_bpf_disabled = read_file('/proc/sys/kernel/unprivileged_bpf_disabled').to_s.strip - return (unprivileged_bpf_disabled == '1' || unprivileged_bpf_disabled == '2') - rescue - raise 'Could not determine kernel.unprivileged_bpf_disabled status' - end + # + # Returns true if user namespaces are enabled, false if not. + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def userns_enabled? + return false if read_file('/proc/sys/user/max_user_namespaces').to_s.strip.eql? '0' + return false if read_file('/proc/sys/kernel/unprivileged_userns_clone').to_s.strip.eql? '0' - # - # Returns true if kernel pointer restriction is enabled - # - # @return [Boolean] - # - def kptr_restrict? - read_file('/proc/sys/kernel/kptr_restrict').to_s.strip.eql? '1' - rescue - raise 'Could not determine kernel.kptr_restrict status' - end + true + rescue StandardError + raise 'Could not determine userns status' + end - # - # Returns true if dmesg restriction is enabled - # - # @return [Boolean] - # - def dmesg_restrict? - read_file('/proc/sys/kernel/dmesg_restrict').to_s.strip.eql? '1' - rescue - raise 'Could not determine kernel.dmesg_restrict status' - end + # + # Returns true if Address Space Layout Randomization (ASLR) is enabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def aslr_enabled? + aslr = read_file('/proc/sys/kernel/randomize_va_space').to_s.strip + aslr.eql?('1') || aslr.eql?('2') + rescue StandardError + raise 'Could not determine ASLR status' + end - # - # Returns mmap minimum address - # - # @return [Integer] - # - def mmap_min_addr - mmap_min_addr = read_file('/proc/sys/vm/mmap_min_addr').to_s.strip - return 0 unless mmap_min_addr =~ /\A\d+\z/ - mmap_min_addr - rescue - raise 'Could not determine system mmap_min_addr' - end + # + # Returns true if Exec-Shield is enabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def exec_shield_enabled? + exec_shield = read_file('/proc/sys/kernel/exec-shield').to_s.strip + exec_shield.eql?('1') || exec_shield.eql?('2') + rescue StandardError + raise 'Could not determine exec-shield status' + end - # - # Returns true if Linux Kernel Runtime Guard (LKRG) kernel module is installed - # - def lkrg_installed? - directory?('/proc/sys/lkrg') - rescue - raise 'Could not determine LKRG status' - end + # + # Returns true if unprivileged bpf is disabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def unprivileged_bpf_disabled? + unprivileged_bpf_disabled = read_file('/proc/sys/kernel/unprivileged_bpf_disabled').to_s.strip + return unprivileged_bpf_disabled == '1' || unprivileged_bpf_disabled == '2' + rescue StandardError + raise 'Could not determine kernel.unprivileged_bpf_disabled status' + end - # - # Returns true if grsecurity is installed - # - def grsec_installed? - File.exists?('/dev/grsec') && File.chardev?('/dev/grsec') - rescue - raise 'Could not determine grsecurity status' - end + # + # Returns true if kernel pointer restriction is enabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def kptr_restrict? + read_file('/proc/sys/kernel/kptr_restrict').to_s.strip.eql? '1' + rescue StandardError + raise 'Could not determine kernel.kptr_restrict status' + end - # - # Returns true if PaX is installed - # - def pax_installed? - read_file('/proc/self/status').to_s.include? 'PaX:' - rescue - raise 'Could not determine PaX status' - end + # + # Returns true if dmesg restriction is enabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def dmesg_restrict? + read_file('/proc/sys/kernel/dmesg_restrict').to_s.strip.eql? '1' + rescue StandardError + raise 'Could not determine kernel.dmesg_restrict status' + end - # - # Returns true if SELinux is installed - # - # @return [Boolean] - # - def selinux_installed? - cmd_exec('id').to_s.include? 'context=' - rescue - raise 'Could not determine SELinux status' - end + # + # Returns mmap minimum address + # + # @return [Integer] + # @raise [RuntimeError] If execution fails. + # + def mmap_min_addr + mmap_min_addr = read_file('/proc/sys/vm/mmap_min_addr').to_s.strip + return 0 unless mmap_min_addr =~ /\A\d+\z/ - # - # Returns true if SELinux is in enforcing mode - # - # @return [Boolean] - # - def selinux_enforcing? - return false unless selinux_installed? - - sestatus = cmd_exec('/usr/sbin/sestatus').to_s.strip - raise unless sestatus.include?('SELinux') - - return true if sestatus =~ /Current mode:\s*enforcing/ - false - rescue - raise 'Could not determine SELinux status' - end + mmap_min_addr + rescue StandardError + raise 'Could not determine system mmap_min_addr' + end - # - # Returns true if Yama is installed - # - # @return [Boolean] - # - def yama_installed? - ptrace_scope = read_file('/proc/sys/kernel/yama/ptrace_scope').to_s.strip - return true if ptrace_scope =~ /\A\d\z/ - false - rescue - raise 'Could not determine Yama status' - end + # + # Returns true if Linux Kernel Runtime Guard (LKRG) kernel module is installed + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def lkrg_installed? + directory?('/proc/sys/lkrg') + rescue StandardError + raise 'Could not determine LKRG status' + end + + # + # Returns true if grsecurity is installed + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def grsec_installed? + File.exist?('/dev/grsec') && File.chardev?('/dev/grsec') + rescue StandardError + raise 'Could not determine grsecurity status' + end + + # + # Returns true if PaX is installed + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def pax_installed? + read_file('/proc/self/status').to_s.include? 'PaX:' + rescue StandardError + raise 'Could not determine PaX status' + end + + # + # Returns true if SELinux is installed + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def selinux_installed? + cmd_exec('id').to_s.include? 'context=' + rescue StandardError + raise 'Could not determine SELinux status' + end + + # + # Returns true if SELinux is in enforcing mode + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def selinux_enforcing? + return false unless selinux_installed? + + sestatus = cmd_exec('/usr/sbin/sestatus').to_s.strip + raise unless sestatus.include?('SELinux') + + return true if sestatus =~ /Current mode:\s*enforcing/ + + false + rescue StandardError + raise 'Could not determine SELinux status' + end + + # + # Returns true if Yama is installed + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def yama_installed? + ptrace_scope = read_file('/proc/sys/kernel/yama/ptrace_scope').to_s.strip + return true if ptrace_scope =~ /\A\d\z/ + + false + rescue StandardError + raise 'Could not determine Yama status' + end + + # + # Returns true if Yama is enabled + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def yama_enabled? + return false unless yama_installed? - # - # Returns true if Yama is enabled - # - # @return [Boolean] - # - def yama_enabled? - return false unless yama_installed? - !read_file('/proc/sys/kernel/yama/ptrace_scope').to_s.strip.eql? '0' - rescue - raise 'Could not determine Yama status' + !read_file('/proc/sys/kernel/yama/ptrace_scope').to_s.strip.eql? '0' + rescue StandardError + raise 'Could not determine Yama status' + end + end + end end -end # Kernel -end # Linux -end # Post -end # Msf +end diff --git a/lib/msf/core/post/linux/packages.rb b/lib/msf/core/post/linux/packages.rb new file mode 100644 index 000000000000..d30033eab76a --- /dev/null +++ b/lib/msf/core/post/linux/packages.rb @@ -0,0 +1,33 @@ +# -*- coding: binary -*- + +module Msf + class Post + module Linux + module Packages + include ::Msf::Post::Linux::System + + # + # Determines the version of an installed package + # + # @param package The package name to check for + # @return [Rex::Version] nil if OS is not supported or package is not installed + # + def installed_package_version?(package) + info = get_sysinfo + + if ['ubuntu', 'debian'].include? info[:distro] + package = cmd_exec("dpkg -l #{package} | grep \'^ii\'") + return nil unless package.start_with?('ii') + + package = package.split(' ')[2] + package = package.gsub('+', '.') + return Rex::Version.new(package) + else + vprint_error('installed_package_version? is being called on an unsupported OS') + end + nil + end + end + end + end +end diff --git a/lib/msf/core/post/linux/priv.rb b/lib/msf/core/post/linux/priv.rb index e8d8a302cd92..59abd7ed5f15 100644 --- a/lib/msf/core/post/linux/priv.rb +++ b/lib/msf/core/post/linux/priv.rb @@ -1,125 +1,197 @@ # -*- coding: binary -*- module Msf -class Post -module Linux -module Priv - include ::Msf::Post::Common - - # - # Returns true if running as root, false if not. - # @return [Boolean] - # - def is_root? - if command_exists?('id') - user_id = cmd_exec('id -u') - clean_user_id = user_id.to_s.gsub(/[^\d]/, '') - if clean_user_id.empty? - raise "Could not determine UID: #{user_id.inspect}" + class Post + module Linux + module Priv + include ::Msf::Post::Common + + # + # Returns true if running as root, false if not. + # + # @return [Boolean] + # @raise [RuntimeError] If execution fails. + # + def is_root? + if command_exists?('id') + user_id = cmd_exec('id -u') + clean_user_id = user_id.to_s.gsub(/[^\d]/, '') + if clean_user_id.empty? + raise "Could not determine UID: #{user_id.inspect}" + end + + return (clean_user_id == '0') + end + user = whoami + data = cmd_exec('while read line; do echo $line; done #{new_path_file}") + end + + # + # Copies the content of one file to another using a command execution + # + # @param origin_file [String] the path to the source file + # @param final_file [String] the path to the destination file + # @return [String] the output of the command + # + def cp_cmd(origin_file, final_file) + file_origin = read_file(origin_file) + cmd_exec("echo '#{file_origin}' > '#{final_file}'") + end + + # + # Retrieves the binary name of a process given its PID + # + # @param pid [Integer] the process ID + # @return [String] the binary name of the process + # + def binary_of_pid(pid) + binary = read_file("/proc/#{pid}/cmdline") + if binary == '' # binary.empty? + binary = read_file("/proc/#{pid}/comm") + end + if binary[-1] == "\n" + binary = binary.split("\n")[0] + end + return binary + end + + # + # Generates a sequence of numbers from `first` to `last` with a given `increment` + # + # @param first [Integer] the starting number of the sequence + # @param increment [Integer] the step increment between each number in the sequence + # @param last [Integer] the ending number of the sequence + # @return [Array] an array containing the sequence of numbers + # + def seq(first, increment, last) + result = [] + (first..last).step(increment) do |i| + result.insert(-1, i) + end + return result + end + + # + # Returns the number of lines, words, and characters in a file + # + # @param file [String] the path to the file + # @return [Array] an array containing the number of lines, words, characters, and the file name + # + def wc_cmd(file) + [nlines_file(file), nwords_file(file), nchars_file(file), file] + end + + # + # Returns the number of characters in a file + # + # @param file [String] the path to the file + # @return [Integer] the number of characters in the file + # + def nchars_file(file) + nchars = 0 + lines = read_file(file).split("\n") + nchars = lines.length + lines.each do |line| + line.gsub(/ /, ' ' => '') + nchars_line = line.length + nchars += nchars_line + end + nchars + end + + # + # Returns the number of words in a file + # + # @param file [String] the path to the file + # @return [Integer] the number of words in the file + # + def nwords_file(file) + nwords = 0 + lines = read_file(file).split("\n") + lines.each do |line| + words = line.split(' ') + nwords_line = words.length + nwords += nwords_line + end + return nwords + end + + # + # Returns the number of lines in a file + # + # @param file [String] the path to the file + # @return [Integer] the number of lines in the file + # + def nlines_file(file) + lines = read_file(file).split("\n") + nlines = lines.length + return nlines + end + + # + # Returns the first `n` lines of a file + # + # @param file [String] the path to the file + # @param nlines [Integer] the number of lines to return + # @return [Array] an array containing the first `n` lines of the file + # + def head_cmd(file, nlines) + lines = read_file(file).split("\n") + result = lines[0..nlines - 1] + return result + end + + # + # Returns the last `n` lines of a file + # + # @param file [String] the path to the file + # @param nlines [Integer] the number of lines to return + # @return [Array] an array containing the last `n` lines of the file + # + def tail_cmd(file, nlines) + lines = read_file(file).split("\n") + result = lines[-1 * nlines..] + return result + end + + # + # Searches for a specific string in a file and returns the lines that contain the string + # + # @param file [String] the path to the file + # @param string [String] the string to search for + # @return [Array] an array containing the lines that include the specified string + # + def grep_cmd(file, string) + result = [] + lines = read_file(file).split("\n") + + lines.each do |line| + if line.include?(string) + result.insert(-1, line) + end + end + return result + end end - return (clean_user_id == '0') end - user = whoami - data = cmd_exec('while read line; do echo $line; done #{new_path_file}") - end - - def cp_cmd(origin_file, final_file) - file_origin = read_file(origin_file) - cmd_exec("echo '#{file_origin}' > #{final_file}") - end - - def binary_of_pid(pid) - binary = read_file("/proc/#{pid}/cmdline") - if binary == "" #binary.empty? - binary = read_file("/proc/#{pid}/comm") - end - if binary[-1] == "\n" - binary = binary.split("\n")[0] - end - return binary - end - - def seq(first, increment, last) - result = [] - (first..last).step(increment) do |i| - result.insert(-1, i) - end - return result - end - - def wc_cmd(file) - [nlines_file(file), nwords_file(file), nchars_file(file), file] - end - - def nchars_file(file) - nchars = 0 - lines = read_file(file).split("\n") - nchars = lines.length() - lines.each do |line| - line.gsub(/[ ]/, ' ' => '') - nchars_line = line.length() - nchars = nchars + nchars_line - end - return nchars end - - def nwords_file(file) - nwords = 0 - lines = read_file(file).split("\n") - lines.each do |line| - words = line.split(" ") - nwords_line = words.length() - nwords = nwords + nwords_line - end - return nwords - end - - def nlines_file(file) - lines = read_file(file).split("\n") - nlines = lines.length() - return nlines - end - - def head_cmd(file, nlines) - lines = read_file(file).split("\n") - result = lines[0..nlines-1] - return result - end - - def tail_cmd(file, nlines) - lines = read_file(file).split("\n") - result = lines[-1*(nlines)..-1] - return result - end - - def grep_cmd(file, string) - result = [] - lines = read_file(file).split("\n") - - lines.each do |line| - if line.include?(string) - result.insert(-1, line) - end - end - return result - end - - - -end # Priv -end # Linux -end # Post -end # Msf +end diff --git a/lib/msf/core/post/linux/process.rb b/lib/msf/core/post/linux/process.rb index 7f6ecc9cd257..bbc37aa4fbf9 100644 --- a/lib/msf/core/post/linux/process.rb +++ b/lib/msf/core/post/linux/process.rb @@ -1,36 +1,40 @@ # -*- coding: binary -*- - module Msf -class Post -module Linux - -module Process + class Post + module Linux + module Process + include Msf::Post::Process - include Msf::Post::Process + def initialize(info = {}) + super( + update_info( + info, + 'Compat' => { + 'Meterpreter' => { + 'Commands' => %w[ + stdapi_sys_process_attach + stdapi_sys_process_memory_read + ] + } + } + ) + ) + end - def initialize(info = {}) - super( - update_info( - info, - 'Compat' => { - 'Meterpreter' => { - 'Commands' => %w[ - stdapi_sys_process_attach - stdapi_sys_process_memory_read - ] - } - } - ) - ) + # + # Reads a specified length of memory from a given base address of a process + # + # @param base_address [Integer] the starting address to read from + # @param length [Integer] the number of bytes to read + # @param pid [Integer] the process ID (optional, default is 0) + # @return [String] the read memory content + # + def mem_read(base_address, length, pid: 0) + proc_id = session.sys.process.open(pid, PROCESS_READ) + proc_id.memory.read(base_address, length) + end + end + end end - - def mem_read(base_address, length, pid: 0) - proc_id = session.sys.process.open(pid, PROCESS_READ) - data = proc_id.memory.read(base_address, length) - end - -end # Process -end # Linux -end # Post -end # Msf +end diff --git a/lib/msf/core/post/linux/system.rb b/lib/msf/core/post/linux/system.rb index de658fe748b0..4becac73e5fe 100644 --- a/lib/msf/core/post/linux/system.rb +++ b/lib/msf/core/post/linux/system.rb @@ -132,8 +132,10 @@ def get_sysinfo # Gathers all SUID files on the filesystem. # NOTE: This uses the Linux `find` command. It will most likely take a while to get all files. # Consider specifying a more narrow find path. + # # @param findpath The path on the system to start searching # @return [Array] + # def get_suid_files(findpath = '/') cmd_exec("find #{findpath} -perm -4000 -print -xdev").to_s.split("\n").delete_if { |i| i.include? 'Permission denied' } rescue StandardError @@ -142,7 +144,9 @@ def get_suid_files(findpath = '/') # # Gets the $PATH environment variable + # # @return [String] + # def get_path cmd_exec('echo $PATH').to_s rescue StandardError @@ -151,6 +155,7 @@ def get_path # # Gets basic information about the system's CPU. + # # @return [Hash] # def get_cpu_info @@ -171,6 +176,7 @@ def get_cpu_info # # Gets the hostname of the system + # # @return [String] # def get_hostname @@ -188,6 +194,7 @@ def get_hostname # # Gets the name of the current shell + # # @return [String] # def get_shell_name @@ -202,6 +209,7 @@ def get_shell_name # # Gets the pid of the current shell + # # @return [String] # def get_shell_pid @@ -210,6 +218,7 @@ def get_shell_pid # # Checks if the system has gcc installed + # # @return [Boolean] # def has_gcc? @@ -220,6 +229,7 @@ def has_gcc? # # Checks if the system has clang installed + # # @return [Boolean] # def has_clang? @@ -230,6 +240,7 @@ def has_clang? # # Checks if `file_path` is mounted on a noexec mount point + # # @return [Boolean] # def noexec?(file_path) @@ -245,6 +256,7 @@ def noexec?(file_path) # # Checks if `file_path` is mounted on a nosuid mount point + # # @return [Boolean] # def nosuid?(file_path) @@ -260,6 +272,7 @@ def nosuid?(file_path) # # Checks for protected hardlinks on the system + # # @return [Boolean] # def protected_hardlinks? @@ -270,6 +283,7 @@ def protected_hardlinks? # # Checks for protected symlinks on the system + # # @return [Boolean] # def protected_symlinks? @@ -280,6 +294,7 @@ def protected_symlinks? # # Gets the version of glibc + # # @return [String] # def glibc_version @@ -292,6 +307,7 @@ def glibc_version # # Gets the mount point of `filepath` + # # @param [String] filepath The filepath to get the mount point # @return [String] # @@ -303,6 +319,7 @@ def get_mount_path(filepath) # # Gets all the IP directions of the device + # # @return [Array] # def ips @@ -323,6 +340,7 @@ def ips # # Gets all the interfaces of the device + # # @return [Array] # def interfaces @@ -338,6 +356,7 @@ def interfaces # # Gets all the macs of the device + # # @return [Array] # def macs @@ -354,9 +373,10 @@ def macs result end - # Parsing information based on: https://github.com/sensu-plugins/sensu-plugins-network-checks/blob/master/bin/check-netstat-tcp.rb # + # Parsing information based on: https://github.com/sensu-plugins/sensu-plugins-network-checks/blob/master/bin/check-netstat-tcp.rb # Gets all the listening tcp ports in the device + # # @return [Array] # def listen_tcp_ports @@ -377,8 +397,8 @@ def listen_tcp_ports end # Parsing information based on: https://github.com/sensu-plugins/sensu-plugins-network-checks/blob/master/bin/check-netstat-tcp.rb - # # Gets all the listening udp ports in the device + # # @return [Array] # def listen_udp_ports @@ -400,6 +420,7 @@ def listen_udp_ports # # Determine if system is a container + # # @return [String] # def get_container_type @@ -443,11 +464,7 @@ def get_container_type end container_type end - # System end - # Linux end - # Post end - # Msf end diff --git a/spec/lib/msf/core/post/linux/kernel.rb b/spec/lib/msf/core/post/linux/kernel.rb new file mode 100644 index 000000000000..704ff9a82474 --- /dev/null +++ b/spec/lib/msf/core/post/linux/kernel.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +RSpec.describe Msf::Post::Linux::Kernel do + subject do + mod = Msf::Module.new + mod.extend(Msf::Post::Linux::Kernel) + mod + end + + describe '#uname' do + context 'it returns an ubuntu kernel' do + it 'returns the kernel information' do + allow(subject).to receive(:cmd_exec).and_return('Linux kali 6.11.2-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.11.2-1kali1 (2024-10-15) x86_64 GNU/Linux ') + expect(subject.uname).to eq('Linux kali 6.11.2-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.11.2-1kali1 (2024-10-15) x86_64 GNU/Linux') + end + end + end + + describe '#kernel_release' do + context 'it returns an ubuntu kernel release' do + it 'returns 6.11.2-amd64' do + allow(subject).to receive(:cmd_exec).and_return('6.11.2-amd64 ') + expect(subject.kernel_release).to eq('6.11.2-amd64') + end + end + end + + describe '#kernel_version' do + context 'it returns an ubuntu kernel version' do + it 'returns 6.11.2-amd64' do + allow(subject).to receive(:cmd_exec).and_return('#1 SMP PREEMPT_DYNAMIC Kali 6.11.2-1kali1 (2024-10-15) ') + expect(subject.kernel_version).to eq('#1 SMP PREEMPT_DYNAMIC Kali 6.11.2-1kali1 (2024-10-15)') + end + end + end + + describe '#kernel_name' do + context 'it returns an ubuntu kernel name' do + it 'returns Linux' do + allow(subject).to receive(:cmd_exec).and_return('Linux ') + expect(subject.kernel_name).to eq('Linux') + end + end + end + + describe '#kernel_hardware' do + context 'it returns an ubuntu kernel hardware' do + it 'returns x86_64' do + allow(subject).to receive(:cmd_exec).and_return('x86_64 ') + expect(subject.kernel_hardware).to eq('x86_64') + end + end + end + + describe '#kernel_arch' do + context 'it returns an ubuntu kernel arch' do + it 'returns x64' do + allow(subject).to receive(:cmd_exec).and_return('x86_64 ') + expect(subject.kernel_arch).to eq('x64') + end + it 'returns aarch64' do + allow(subject).to receive(:cmd_exec).and_return('aarch64 ') + expect(subject.kernel_arch).to eq('aarch64') + end + it 'returns aarch64' do + allow(subject).to receive(:cmd_exec).and_return('arm ') + expect(subject.kernel_arch).to eq('armle') + end + it 'returns x86' do + allow(subject).to receive(:cmd_exec).and_return('i686 ') + expect(subject.kernel_arch).to eq('x86') + end + it 'returns ppc' do + allow(subject).to receive(:cmd_exec).and_return('ppc ') + expect(subject.kernel_arch).to eq('ppc') + end + it 'returns ppc64' do + allow(subject).to receive(:cmd_exec).and_return('ppc64 ') + expect(subject.kernel_arch).to eq('ppc64') + end + it 'returns ppc64le' do + allow(subject).to receive(:cmd_exec).and_return('ppc64le ') + expect(subject.kernel_arch).to eq('ppc64le') + end + it 'returns mips' do + allow(subject).to receive(:cmd_exec).and_return('mips ') + expect(subject.kernel_arch).to eq('mips') + end + it 'returns mips64' do + allow(subject).to receive(:cmd_exec).and_return('mips64 ') + expect(subject.kernel_arch).to eq('mips64') + end + it 'returns sparc' do + allow(subject).to receive(:cmd_exec).and_return('sparc ') + expect(subject.kernel_arch).to eq('sparc') + end + it 'returns riscv32le' do + allow(subject).to receive(:cmd_exec).and_return('riscv32 ') + expect(subject.kernel_arch).to eq('riscv32le') + end + it 'returns riscv64le' do + allow(subject).to receive(:cmd_exec).and_return('riscv64 ') + expect(subject.kernel_arch).to eq('riscv64le') + end + it 'returns loongarch64' do + allow(subject).to receive(:cmd_exec).and_return('loongarch64 ') + expect(subject.kernel_arch).to eq('loongarch64') + end + + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/core/post/linux/packages.rb b/spec/lib/msf/core/post/linux/packages.rb new file mode 100644 index 000000000000..75cf0cd4c7f9 --- /dev/null +++ b/spec/lib/msf/core/post/linux/packages.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +RSpec.describe Msf::Post::Linux::Packages do + subject do + mod = Msf::Module.new + mod.extend(Msf::Post::Linux::Packages) + mod + end + + describe '#installed_package_version?' do + context 'when the OS isnt supported' do + it 'returns nil' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"foobar", :version=>""}) + expect(subject.installed_package_version?('test')).to be_nil + end + end + + context 'when the Ubuntu package isnt installed' do + it 'returns nil' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"Linux ubuntu22 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux", :distro=>"ubuntu", :version=>"Ubuntu 22.04.5 LTS"}) + allow(subject).to receive(:cmd_exec).and_return('dpkg-query: no packages found matching example') + expect(subject.installed_package_version?('test')).to be_nil + end + end + + context 'when the Ubuntu package is installed' do + it 'returns 3.5-5ubuntu2.1' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"Linux ubuntu22 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux", :distro=>"ubuntu", :version=>"Ubuntu 22.04.5 LTS"}) + allow(subject).to receive(:cmd_exec).and_return('ii needrestart 3.5-5ubuntu2.1 all check which daemons need to be restarted after library upgrades') + expect(subject.installed_package_version?('test')).to eq(Rex::Version.new('3.5-5ubuntu2.1')) + end + end + + context 'when the Ubuntu package is installed with a + in the version number' do + it 'returns 1.34.dfsg.pre.1ubuntu0.1.22.04.2' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"Linux ubuntu22 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux", :distro=>"ubuntu", :version=>"Ubuntu 22.04.5 LTS"}) + allow(subject).to receive(:cmd_exec).and_return('ii tar 1.34+dfsg-1ubuntu0.1.22.04.2 amd64 GNU version of the tar archiving utility') + expect(subject.installed_package_version?('test')).to eq(Rex::Version.new("1.34.dfsg.pre.1ubuntu0.1.22.04.2")) + end + end + end +end From 61705db8be3930498ebf6c58a2d41f6557651af6 Mon Sep 17 00:00:00 2001 From: h00die Date: Wed, 27 Nov 2024 16:07:40 -0500 Subject: [PATCH 2/3] more specs for linux post libraries --- lib/msf/core/post/linux/packages.rb | 57 ++- lib/msf/core/post/linux/system.rb | 4 +- spec/lib/msf/core/post/linux/packages.rb | 64 ++- spec/lib/msf/core/post/linux/system.rb | 566 +++++++++++++++++++++++ 4 files changed, 670 insertions(+), 21 deletions(-) create mode 100644 spec/lib/msf/core/post/linux/system.rb diff --git a/lib/msf/core/post/linux/packages.rb b/lib/msf/core/post/linux/packages.rb index d30033eab76a..ba83baa19f7c 100644 --- a/lib/msf/core/post/linux/packages.rb +++ b/lib/msf/core/post/linux/packages.rb @@ -12,18 +12,59 @@ module Packages # @param package The package name to check for # @return [Rex::Version] nil if OS is not supported or package is not installed # - def installed_package_version?(package) + def installed_package_version(package) info = get_sysinfo - if ['ubuntu', 'debian'].include? info[:distro] - package = cmd_exec("dpkg -l #{package} | grep \'^ii\'") - return nil unless package.start_with?('ii') + if ['debian', 'ubuntu'].include?info[:distro] + package_version = cmd_exec("dpkg -l #{package} | grep \'^ii\'") + return nil unless package_version.start_with?('ii') - package = package.split(' ')[2] - package = package.gsub('+', '.') - return Rex::Version.new(package) + package_version = package_version.split(' ')[2] + package_version = package_version.gsub('+', '.') + return Rex::Version.new(package_version) + elsif ['redhat', 'fedora'].include?(info[:distro]) + package_version = cmd_exec("rpm -q #{package}") + return nil if package_version.include?('is not installed') + + # dnf-4.18.0-2.fc39.noarch + # remove package name at the beginning + package_version = package_version.split("#{package}-")[1] + # remove arch at the end + package_version = package_version.sub(/\.[^.]*$/, '') + return Rex::Version.new(package_version) + # XXX not tested on live system + # https://docs.oracle.com/cd/E23824_01/html/821-1451/gkunu.html + elsif ['solaris', 'oracle'].include?(info[:distro]) + package_version = cmd_exec("pkg info #{package}") + return nil unless package_version.include?('State: Installed') + + package_version = package_version.match(/Version: (.+)/)[1] + return Rex::Version.new(package_version) + elsif ['freebsd'].include?(info[:distro]) + package_version = cmd_exec("pkg info #{package}") + return nil unless package_version.include?('Version') + + package_version = package_version.match(/Version\s+:\s+(.+)/)[1] + return Rex::Version.new(package_version) + # XXX not tested on live system + elsif ['gentoo'].include?(info[:distro]) + # https://wiki.gentoo.org/wiki/Equery + package_version = cmd_exec("equery --quiet list #{package}") + return nil if package_version.include?('No packages found') + + package_version = package_version.split('/')[1] + # make gcc-1.1 to 1.1 + package_version = package_version.sub(/.*?-/, '') + return Rex::Version.new(package_version) + # XXX not tested on live system + elsif ['arch'].include?(info[:distro]) + package_version = cmd_exec("pacman -Qi #{package}") + return nil unless package_version.include?('Version') + + package_version = package_version.match(/Version\s+:\s+(.+)/)[1] + return Rex::Version.new(package_version) else - vprint_error('installed_package_version? is being called on an unsupported OS') + vprint_error('installed_package_version is being called on an unsupported OS') end nil end diff --git a/lib/msf/core/post/linux/system.rb b/lib/msf/core/post/linux/system.rb index 4becac73e5fe..03d321054901 100644 --- a/lib/msf/core/post/linux/system.rb +++ b/lib/msf/core/post/linux/system.rb @@ -7,6 +7,7 @@ module System include ::Msf::Post::Common include ::Msf::Post::File include ::Msf::Post::Unix + include Msf::Auxiliary::Report # # Returns a Hash containing Distribution Name, Version and Kernel Information @@ -14,7 +15,6 @@ module System def get_sysinfo system_data = {} etc_files = cmd_exec('ls /etc').split - kernel_version = cmd_exec('uname -a') system_data[:kernel] = kernel_version @@ -442,6 +442,8 @@ def get_container_type return 'Docker' when /lxc/i return 'LXC' + else + return 'Unknown' end else # Check for the "container" environment variable diff --git a/spec/lib/msf/core/post/linux/packages.rb b/spec/lib/msf/core/post/linux/packages.rb index 75cf0cd4c7f9..e389355ae029 100644 --- a/spec/lib/msf/core/post/linux/packages.rb +++ b/spec/lib/msf/core/post/linux/packages.rb @@ -7,35 +7,75 @@ mod end - describe '#installed_package_version?' do - context 'when the OS isnt supported' do - it 'returns nil' do - allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"foobar", :version=>""}) - expect(subject.installed_package_version?('test')).to be_nil + describe '#installed_package_version' do + context 'when the OS isnt supported' do + it 'returns nil' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"unsupported", :version=>""}) + expect(subject.installed_package_version('test')).to be_nil + end end - end - context 'when the Ubuntu package isnt installed' do + context 'when the Ubuntu/Debian package isnt installed' do it 'returns nil' do allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"Linux ubuntu22 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux", :distro=>"ubuntu", :version=>"Ubuntu 22.04.5 LTS"}) allow(subject).to receive(:cmd_exec).and_return('dpkg-query: no packages found matching example') - expect(subject.installed_package_version?('test')).to be_nil + expect(subject.installed_package_version('test')).to be_nil end end - context 'when the Ubuntu package is installed' do + context 'when the Ubuntu/Debian package is installed' do it 'returns 3.5-5ubuntu2.1' do allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"Linux ubuntu22 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux", :distro=>"ubuntu", :version=>"Ubuntu 22.04.5 LTS"}) allow(subject).to receive(:cmd_exec).and_return('ii needrestart 3.5-5ubuntu2.1 all check which daemons need to be restarted after library upgrades') - expect(subject.installed_package_version?('test')).to eq(Rex::Version.new('3.5-5ubuntu2.1')) + expect(subject.installed_package_version('test')).to eq(Rex::Version.new('3.5-5ubuntu2.1')) end end - context 'when the Ubuntu package is installed with a + in the version number' do + context 'when the Ubuntu/Debian package is installed with a + in the version number' do it 'returns 1.34.dfsg.pre.1ubuntu0.1.22.04.2' do allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"Linux ubuntu22 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux", :distro=>"ubuntu", :version=>"Ubuntu 22.04.5 LTS"}) allow(subject).to receive(:cmd_exec).and_return('ii tar 1.34+dfsg-1ubuntu0.1.22.04.2 amd64 GNU version of the tar archiving utility') - expect(subject.installed_package_version?('test')).to eq(Rex::Version.new("1.34.dfsg.pre.1ubuntu0.1.22.04.2")) + expect(subject.installed_package_version('test')).to eq(Rex::Version.new("1.34.dfsg.pre.1ubuntu0.1.22.04.2")) + end + end + + context 'when distro is redhat or fedora' do + it 'returns the package version' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"redhat", :version=>""}) + allow(subject).to receive(:cmd_exec).and_return('curl-8.2.1-3.fc39.x86_64') + expect(subject.installed_package_version('curl')).to eq(Rex::Version.new('8.2.1-3.fc39')) + end + end + + context 'when distro is solaris' do + it 'returns the package version' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"solaris", :version=>""}) + allow(subject).to receive(:cmd_exec).and_return('State: Installed\nVersion: 1.2.3') + expect(subject.installed_package_version('test')).to eq(Rex::Version.new('1.2.3')) + end + end + + context 'when distro is freebsd' do + it 'returns the package version' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"freebsd", :version=>""}) + allow(subject).to receive(:cmd_exec).and_return('Version : 1.2.3') + expect(subject.installed_package_version('test')).to eq(Rex::Version.new('1.2.3')) + end + end + + context 'when distro is gentoo' do + it 'returns the package version' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"gentoo", :version=>""}) + allow(subject).to receive(:cmd_exec).and_return('sys-devel/gcc-4.3.2-r3') + expect(subject.installed_package_version('test')).to eq(Rex::Version.new('4.3.2-r3')) + end + end + + context 'when distro is arch' do + it 'returns the package version' do + allow(subject).to receive(:get_sysinfo).and_return({:kernel=>"", :distro=>"arch", :version=>""}) + allow(subject).to receive(:cmd_exec).and_return('Version : 1.2.3') + expect(subject.installed_package_version('test')).to eq(Rex::Version.new('1.2.3')) end end end diff --git a/spec/lib/msf/core/post/linux/system.rb b/spec/lib/msf/core/post/linux/system.rb new file mode 100644 index 000000000000..ef339da2af4a --- /dev/null +++ b/spec/lib/msf/core/post/linux/system.rb @@ -0,0 +1,566 @@ +require 'spec_helper' + +RSpec.describe Msf::Post::Linux::System do + subject do + mod = Msf::Module.new + mod.extend(Msf::Post::Linux::System) + mod + end + + describe '#get_sysinfo' do + context 'when the system is Debian' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('debian_version') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.15.0-20-generic (buildd@lgw01-amd64)') + allow(subject).to receive(:read_file).with('/etc/issue').and_return('Debian GNU/Linux 9 \\n \\l') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('debian') + expect(sysinfo[:version]).to eq('Debian GNU/Linux 9') + expect(sysinfo[:kernel]).to eq('Linux version 4.15.0-20-generic (buildd@lgw01-amd64)') + end + end + + context 'when the system is Ubuntu' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('debian_version') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.15.0-20-generic (buildd@lgw01-amd64) Ubuntu') + allow(subject).to receive(:read_file).with('/etc/issue').and_return('Ubuntu 18.04.1 LTS \\n \\l') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('ubuntu') + expect(sysinfo[:version]).to eq('Ubuntu 18.04.1 LTS') + expect(sysinfo[:kernel]).to eq('Linux version 4.15.0-20-generic (buildd@lgw01-amd64) Ubuntu') + end + end + + context 'when the system is Amazon or CentOS' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('system-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.14.88-88.76.amzn2.x86_64 (mockbuild@gobi-build-60008) (gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC))') + allow(subject).to receive(:read_file).with('/etc/system-release').and_return('Amazon Linux 2') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('amazon') + expect(sysinfo[:version]).to eq('Amazon Linux 2') + expect(sysinfo[:kernel]).to eq('Linux version 4.14.88-88.76.amzn2.x86_64 (mockbuild@gobi-build-60008) (gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC))') + end + end + + context 'when the system is Alpine' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('alpine-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.19.0-0-virt (buildozer@build-3-10-x86_64)') + allow(subject).to receive(:read_file).with('/etc/alpine-release').and_return('3.10.2') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('alpine') + expect(sysinfo[:version]).to eq('3.10.2') + expect(sysinfo[:kernel]).to eq('Linux version 4.19.0-0-virt (buildozer@build-3-10-x86_64)') + end + end + + context 'when the system is Fedora' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('fedora-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 5.3.7-301.fc31.x86_64 (mockbuild@bkernel01.phx2.fedoraproject.org)') + allow(subject).to receive(:read_file).with('/etc/fedora-release').and_return('Fedora release 31 (Thirty One)') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('fedora') + expect(sysinfo[:version]).to eq('Fedora release 31 (Thirty One)') + expect(sysinfo[:kernel]).to eq('Linux version 5.3.7-301.fc31.x86_64 (mockbuild@bkernel01.phx2.fedoraproject.org)') + end + end + + context 'when the system is Oracle Linux' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('enterprise-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.14.35-1818.3.3.el7uek.x86_64 (mockbuild@x86-ol7-builder-02)') + allow(subject).to receive(:read_file).with('/etc/enterprise-release').and_return('Oracle Linux Server release 7.6') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('oracle') + expect(sysinfo[:version]).to eq('Oracle Linux Server release 7.6') + expect(sysinfo[:kernel]).to eq('Linux version 4.14.35-1818.3.3.el7uek.x86_64 (mockbuild@x86-ol7-builder-02)') + end + end + + context 'when the system is RedHat' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('redhat-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 3.10.0-957.21.3.el7.x86_64 (mockbuild@x86-01.bsys.centos.org)') + allow(subject).to receive(:read_file).with('/etc/redhat-release').and_return('Red Hat Enterprise Linux Server release 7.6 (Maipo)') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('redhat') + expect(sysinfo[:version]).to eq('Red Hat Enterprise Linux Server release 7.6 (Maipo)') + expect(sysinfo[:kernel]).to eq('Linux version 3.10.0-957.21.3.el7.x86_64 (mockbuild@x86-01.bsys.centos.org)') + end + end + + context 'when the system is Arch' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('arch-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 5.3.7-arch1-1-ARCH (builduser@heftig-29959)') + allow(subject).to receive(:read_file).with('/etc/arch-release').and_return('Arch Linux') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('arch') + expect(sysinfo[:version]).to eq('Arch Linux') + expect(sysinfo[:kernel]).to eq('Linux version 5.3.7-arch1-1-ARCH (builduser@heftig-29959)') + end + end + + context 'when the system is Slackware' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('slackware-version') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.4.14 (root@darkstar)') + allow(subject).to receive(:read_file).with('/etc/slackware-version').and_return('Slackware 14.2') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('slackware') + expect(sysinfo[:version]).to eq('Slackware 14.2') + expect(sysinfo[:kernel]).to eq('Linux version 4.4.14 (root@darkstar)') + end + end + + context 'when the system is Mandrake' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('mandrake-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 2.6.12-12mdk (nplanel@no.mandriva.com)') + allow(subject).to receive(:read_file).with('/etc/mandrake-release').and_return('Mandrake Linux release 10.2 (Limited Edition 2005)') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('mandrake') + expect(sysinfo[:version]).to eq('Mandrake Linux release 10.2 (Limited Edition 2005)') + expect(sysinfo[:kernel]).to eq('Linux version 2.6.12-12mdk (nplanel@no.mandriva.com)') + end + end + + context 'when the system is SuSE' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('SuSE-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.12.14-lp151.28.36-default (geeko@buildhost)') + allow(subject).to receive(:read_file).with('/etc/SuSE-release').and_return('openSUSE Leap 15.1') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('suse') + expect(sysinfo[:version]).to eq('openSUSE Leap 15.1') + expect(sysinfo[:kernel]).to eq('Linux version 4.12.14-lp151.28.36-default (geeko@buildhost)') + end + end + + context 'when the system is OpenSUSE' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('SUSE-brand') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.12.14-lp151.28.36-default (geeko@buildhost)') + allow(subject).to receive(:read_file).with('/etc/SUSE-brand').and_return('VERSION = 15.1') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('suse') + expect(sysinfo[:version]).to eq('15.1') + expect(sysinfo[:kernel]).to eq('Linux version 4.12.14-lp151.28.36-default (geeko@buildhost)') + end + end + + context 'when the system is Gentoo' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('gentoo-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.19.57-gentoo (root@localhost)') + allow(subject).to receive(:read_file).with('/etc/gentoo-release').and_return('Gentoo Base System release 2.6') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('gentoo') + expect(sysinfo[:version]).to eq('Gentoo Base System release 2.6') + expect(sysinfo[:kernel]).to eq('Linux version 4.19.57-gentoo (root@localhost)') + end + end + + context 'when the system is Openwall' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('owl-release') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 2.6.32-431.el6.x86_64 (mockbuild@c6b8.bsys.dev.centos.org)') + allow(subject).to receive(:read_file).with('/etc/owl-release').and_return('Openwall GNU/*/Linux 3.1 (2014-09-26)') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('openwall') + expect(sysinfo[:version]).to eq('Openwall GNU/*/Linux 3.1 (2014-09-26)') + expect(sysinfo[:kernel]).to eq('Linux version 2.6.32-431.el6.x86_64 (mockbuild@c6b8.bsys.dev.centos.org)') + end + end + + context 'when the system is Generic Linux' do + it 'returns the correct system information' do + allow(subject).to receive(:cmd_exec).with('ls /etc').and_return('issue') + allow(subject).to receive(:cmd_exec).with('uname -a').and_return('Linux version 4.19.0-0-virt (buildozer@build-3-10-x86_64)') + allow(subject).to receive(:read_file).with('/etc/issue').and_return('Generic Linux') + allow(subject).to receive(:report_host) + + sysinfo = subject.get_sysinfo + + expect(sysinfo[:distro]).to eq('linux') + expect(sysinfo[:version]).to eq('Generic Linux') + expect(sysinfo[:kernel]).to eq('Linux version 4.19.0-0-virt (buildozer@build-3-10-x86_64)') + end + end + end + + describe '#get_suid_files' do + context 'when there are no permission denied errors' do + it 'returns the list of SUID files' do + suid_files = "/usr/bin/passwd\n/usr/bin/sudo\n" + allow(subject).to receive(:cmd_exec).with("find / -perm -4000 -print -xdev").and_return(suid_files) + + result = subject.get_suid_files + + expect(result).to eq(['/usr/bin/passwd', '/usr/bin/sudo']) + end + end + + context 'when there are permission denied errors' do + it 'filters out the permission denied errors' do + suid_files = "/usr/bin/passwd\nfind: ‘/root’: Permission denied\n/usr/bin/sudo\n" + allow(subject).to receive(:cmd_exec).with("find / -perm -4000 -print -xdev").and_return(suid_files) + + result = subject.get_suid_files + + expect(result).to eq(['/usr/bin/passwd', '/usr/bin/sudo']) + end + end + + context 'when an error occurs' do + it 'raises an error' do + allow(subject).to receive(:cmd_exec).with("find / -perm -4000 -print -xdev").and_raise(StandardError) + + expect { subject.get_suid_files }.to raise_error('Could not retrieve all SUID files') + end + end + end + + describe '#get_path' do + it 'returns the system path' do + allow(subject).to receive(:cmd_exec).with('echo $PATH').and_return('/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') + expect(subject.get_path).to eq('/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') + end + + it 'raises an error if unable to determine path' do + allow(subject).to receive(:cmd_exec).with('echo $PATH').and_raise(StandardError) + expect { subject.get_path }.to raise_error('Unable to determine path') + end + end + + describe '#get_cpu_info' do + it 'returns the CPU information' do + cpuinfo = "processor\t: 0\nvendor_id\t: GenuineIntel\ncpu MHz\t\t: 2400.000\nmodel name\t: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz\n" + allow(subject).to receive(:read_file).with('/proc/cpuinfo').and_return(cpuinfo) + expect(subject.get_cpu_info).to eq({ speed_mhz: 2400, product: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz', vendor: 'GenuineIntel', cores: 1 }) + end + + it 'raises an error if unable to get CPU information' do + allow(subject).to receive(:read_file).with('/proc/cpuinfo').and_raise(StandardError) + expect { subject.get_cpu_info }.to raise_error('Could not get CPU information') + end + end + + describe '#get_hostname' do + it 'returns the hostname using uname' do + allow(subject).to receive(:command_exists?).with('uname').and_return(true) + allow(subject).to receive(:cmd_exec).with('uname -n').and_return('test-hostname') + allow(subject).to receive(:report_host) + expect(subject.get_hostname).to eq('test-hostname') + end + + it 'returns the hostname using /proc/sys/kernel/hostname' do + allow(subject).to receive(:command_exists?).with('uname').and_return(false) + allow(subject).to receive(:read_file).with('/proc/sys/kernel/hostname').and_return('test-hostname') + allow(subject).to receive(:report_host) + expect(subject.get_hostname).to eq('test-hostname') + end + + it 'raises an error if unable to retrieve hostname' do + allow(subject).to receive(:cmd_exec).with('uname -n').and_raise(StandardError) + expect { subject.get_hostname }.to raise_error('Unable to retrieve hostname') + end + end + + describe '#get_shell_name' do + it 'returns the shell name using ps' do + allow(subject).to receive(:command_exists?).with('ps').and_return(true) + allow(subject).to receive(:cmd_exec).with('ps -p $$').and_return("PID TTY TIME CMD\n 1 ? 00:00:00 bash") + expect(subject.get_shell_name).to eq('bash') + end + + it 'returns the shell name using echo $0' do + allow(subject).to receive(:command_exists?).with('ps').and_return(false) + allow(subject).to receive(:cmd_exec).with('echo $0').and_return('-bash') + expect(subject.get_shell_name).to eq('bash') + end + + it 'raises an error if unable to gather shell name' do + allow(subject).to receive(:cmd_exec).with('ps -p $$').and_raise(StandardError) + expect { subject.get_shell_name }.to raise_error('Unable to gather shell name') + end + end + + describe '#get_shell_pid' do + it 'returns the shell pid' do + allow(subject).to receive(:cmd_exec).with('echo $$').and_return('1234') + expect(subject.get_shell_pid).to eq('1234') + end + end + + describe '#has_gcc?' do + it 'returns true if gcc is installed' do + allow(subject).to receive(:command_exists?).with('gcc').and_return(true) + expect(subject.has_gcc?).to be true + end + + it 'raises an error if unable to check for gcc' do + allow(subject).to receive(:command_exists?).with('gcc').and_raise(StandardError) + expect { subject.has_gcc? }.to raise_error('Unable to check for gcc') + end + end + + describe '#has_clang?' do + it 'returns true if clang is installed' do + allow(subject).to receive(:command_exists?).with('clang').and_return(true) + expect(subject.has_clang?).to be true + end + + it 'raises an error if unable to check for clang' do + allow(subject).to receive(:command_exists?).with('clang').and_raise(StandardError) + expect { subject.has_clang? }.to raise_error('Unable to check for clang') + end + end + + describe '#noexec?' do + it 'returns true if the file path is mounted on a noexec mount point' do + mount_content = "/dev/sda1 / ext4 rw,noexec 0 0\n" + allow(subject).to receive(:read_file).with('/proc/mounts').and_return(mount_content) + allow(subject).to receive(:get_mount_path).with('/path/to/file').and_return('/') + expect(subject.noexec?('/path/to/file')).to be true + end + + it 'raises an error if unable to check for noexec volume' do + allow(subject).to receive(:read_file).with('/proc/mounts').and_raise(StandardError) + expect { subject.noexec?('/path/to/file') }.to raise_error('Unable to check for noexec volume') + end + end + + describe '#nosuid?' do + it 'returns true if the file path is mounted on a nosuid mount point' do + mount_content = "/dev/sda1 / ext4 rw,nosuid 0 0\n" + allow(subject).to receive(:read_file).with('/proc/mounts').and_return(mount_content) + allow(subject).to receive(:get_mount_path).with('/path/to/file').and_return('/') + expect(subject.nosuid?('/path/to/file')).to be true + end + + it 'raises an error if unable to check for nosuid volume' do + allow(subject).to receive(:read_file).with('/proc/mounts').and_raise(StandardError) + expect { subject.nosuid?('/path/to/file') }.to raise_error('Unable to check for nosuid volume') + end + end + + describe '#protected_hardlinks?' do + it 'returns true if protected hardlinks are enabled' do + allow(subject).to receive(:read_file).with('/proc/sys/fs/protected_hardlinks').and_return('1') + expect(subject.protected_hardlinks?).to be true + end + + it 'raises an error if unable to determine protected_hardlinks status' do + allow(subject).to receive(:read_file).with('/proc/sys/fs/protected_hardlinks').and_raise(StandardError) + expect { subject.protected_hardlinks? }.to raise_error('Could not determine protected_hardlinks status') + end + end + + describe '#protected_symlinks?' do + it 'returns true if protected symlinks are enabled' do + allow(subject).to receive(:read_file).with('/proc/sys/fs/protected_symlinks').and_return('1') + expect(subject.protected_symlinks?).to be true + end + + it 'raises an error if unable to determine protected_symlinks status' do + allow(subject).to receive(:read_file).with('/proc/sys/fs/protected_symlinks').and_raise(StandardError) + expect { subject.protected_symlinks? }.to raise_error('Could not determine protected_symlinks status') + end + end + + describe '#glibc_version' do + it 'returns the glibc version' do + allow(subject).to receive(:command_exists?).with('ldd').and_return(true) + allow(subject).to receive(:cmd_exec).with('ldd --version').and_return('ldd (GNU libc) 2.27') + expect(subject.glibc_version).to eq('2.27') + end + + it 'raises an error if glibc is not installed' do + allow(subject).to receive(:command_exists?).with('ldd').and_return(false) + expect { subject.glibc_version }.to raise_error('glibc is not installed') + end + + it 'raises an error if unable to determine glibc version' do + allow(subject).to receive(:cmd_exec).with('ldd --version').and_raise(StandardError) + expect { subject.glibc_version }.to raise_error('Could not determine glibc version') + end + end + + describe '#get_mount_path' do + it 'returns the mount path of the file' do + allow(subject).to receive(:cmd_exec).with('df "/path/to/file" | tail -1').and_return('/dev/sda1 101141520 52963696 42993928 56% /') + expect(subject.get_mount_path('/path/to/file')).to eq('/') + end + + it 'raises an error if unable to get mount path' do + allow(subject).to receive(:cmd_exec).with('df "/path/to/file" | tail -1').and_raise(StandardError) + expect { subject.get_mount_path('/path/to/file') }.to raise_error('Unable to get mount path of /path/to/file') + end + end + + describe '#ips' do + it 'returns all IP addresses of the device' do + fib_trie_content = " +-- 192.168.1.1/32 host LOCAL\n" + allow(subject).to receive(:read_file).with('/proc/net/fib_trie').and_return(fib_trie_content) + expect(subject.ips).to eq(['192.168.1.1']) + end + end + + describe '#interfaces' do + it 'returns all interfaces of the device' do + interfaces_content = "/sys/class/net/eth0\n/sys/class/net/lo\n" + allow(subject).to receive(:cmd_exec).with('for fn in /sys/class/net/*; do echo $fn; done').and_return(interfaces_content) + expect(subject.interfaces).to eq(['eth0', 'lo']) + end + end + + describe '#macs' do + it 'returns all MAC addresses of the device' do + macs_content = "/sys/class/net/eth0\n/sys/class/net/lo\n" + allow(subject).to receive(:cmd_exec).with('for fn in /sys/class/net/*; do echo $fn; done').and_return(macs_content) + allow(subject).to receive(:read_file).with('/sys/class/net/eth0/address').and_return('00:11:22:33:44:55') + allow(subject).to receive(:read_file).with('/sys/class/net/lo/address').and_return('00:00:00:00:00:00') + allow(subject).to receive(:report_host) + expect(subject.macs).to eq(['00:11:22:33:44:55', '00:00:00:00:00:00']) + end + end + + describe '#listen_tcp_ports' do + it 'returns all listening TCP ports of the device' do + tcp_content = " 0: 0100007F:0016 00000000:0000 0A\n" + allow(subject).to receive(:read_file).with('/proc/net/tcp').and_return(tcp_content) + expect(subject.listen_tcp_ports).to eq([22]) + end + end + + describe '#listen_udp_ports' do + it 'returns all listening UDP ports of the device' do + udp_content = " 0: 0100007F:0035 00000000:0000 07\n" + allow(subject).to receive(:read_file).with('/proc/net/udp').and_return(udp_content) + expect(subject.listen_udp_ports).to eq([53]) + end + end + + describe '#get_container_type' do + it 'returns Docker if /.dockerenv exists' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(true) + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('Docker') + end + + it 'returns Docker if /.dockerinit exists' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(true) + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('Docker') + end + + it 'returns Podman if /run/.containerenv exists' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(false) + allow(subject).to receive(:file?).with('/run/.containerenv').and_return(true) + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('Podman') + end + + it 'returns LXC if /dev/lxc exists' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(false) + allow(subject).to receive(:file?).with('/run/.containerenv').and_return(false) + allow(subject).to receive(:directory?).with('/dev/lxc').and_return(true) + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('LXC') + end + + it 'returns WSL if /proc/sys/kernel/osrelease contains WSL' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(false) + allow(subject).to receive(:file?).with('/run/.containerenv').and_return(false) + allow(subject).to receive(:directory?).with('/dev/lxc').and_return(false) + allow(subject).to receive(:file?).with('/proc/sys/kernel/osrelease').and_return(true) + allow(subject).to receive(:read_file).with('/proc/sys/kernel/osrelease').and_return(["4.4.0-19041-Microsoft"]) + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('WSL') + end + + it 'returns Docker if /proc/1/cgroup contains docker' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(false) + allow(subject).to receive(:file?).with('/run/.containerenv').and_return(false) + allow(subject).to receive(:directory?).with('/dev/lxc').and_return(false) + allow(subject).to receive(:file?).with('/proc/sys/kernel/osrelease').and_return(false) + allow(subject).to receive(:read_file).with('/proc/1/cgroup').and_return('1:name=systemd:/docker/1234567890abcdef') + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('Docker') + end + + it 'returns LXC if /proc/1/cgroup contains lxc' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(false) + allow(subject).to receive(:file?).with('/run/.containerenv').and_return(false) + allow(subject).to receive(:directory?).with('/dev/lxc').and_return(false) + allow(subject).to receive(:file?).with('/proc/sys/kernel/osrelease').and_return(false) + allow(subject).to receive(:read_file).with('/proc/1/cgroup').and_return('1:name=systemd:/lxc/1234567890abcdef') + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('LXC') + end + + it 'returns Unknown if no container type is detected' do + allow(subject).to receive(:file?).with('/.dockerenv').and_return(false) + allow(subject).to receive(:file?).with('/.dockerinit').and_return(false) + allow(subject).to receive(:file?).with('/run/.containerenv').and_return(false) + allow(subject).to receive(:directory?).with('/dev/lxc').and_return(false) + allow(subject).to receive(:file?).with('/proc/sys/kernel/osrelease').and_return(false) + allow(subject).to receive(:read_file).with('/proc/1/cgroup').and_return('') + allow(subject).to receive(:get_env).with('container').and_return(nil) + allow(subject).to receive(:report_host) + expect(subject.get_container_type).to eq('Unknown') + end + end +end \ No newline at end of file From cde660065c1ee5ce9d1a4a67d558888585e12a1c Mon Sep 17 00:00:00 2001 From: h00die Date: Sun, 1 Dec 2024 20:00:58 -0500 Subject: [PATCH 3/3] more specs for linux post libraries --- lib/msf/core/post/linux/busy_box.rb | 217 +++++++++--------- lib/msf/core/post/linux/compile.rb | 8 +- spec/lib/msf/core/post/linux/compile_spec.rb | 121 ++++++++++ .../post/linux/{kernel.rb => kernel_spec.rb} | 0 .../linux/{packages.rb => packages_spec.rb} | 0 spec/lib/msf/core/post/linux/priv_spec.rb | 139 +++++++++++ spec/lib/msf/core/post/linux/process_spec.rb | 33 +++ .../post/linux/{system.rb => system_spec.rb} | 0 8 files changed, 409 insertions(+), 109 deletions(-) create mode 100644 spec/lib/msf/core/post/linux/compile_spec.rb rename spec/lib/msf/core/post/linux/{kernel.rb => kernel_spec.rb} (100%) rename spec/lib/msf/core/post/linux/{packages.rb => packages_spec.rb} (100%) create mode 100644 spec/lib/msf/core/post/linux/priv_spec.rb create mode 100644 spec/lib/msf/core/post/linux/process_spec.rb rename spec/lib/msf/core/post/linux/{system.rb => system_spec.rb} (100%) diff --git a/lib/msf/core/post/linux/busy_box.rb b/lib/msf/core/post/linux/busy_box.rb index 77784cee192c..af84fee730ca 100644 --- a/lib/msf/core/post/linux/busy_box.rb +++ b/lib/msf/core/post/linux/busy_box.rb @@ -1,111 +1,116 @@ # -*- coding: binary -*- - module Msf -class Post -module Linux -module BusyBox - - include ::Msf::Post::Common - include ::Msf::Post::File - - # Checks if the file exists in the target - # - # @param file_path [String] the target file path - # @return [Boolean] true if files exists, false otherwise - # @note Msf::Post::File#file? doesnt work because test -f is not available in busybox - def busy_box_file_exist?(file_path) - contents = read_file(file_path) - if contents.nil? || contents.empty? - return false - end - - true - end - - # Checks if the directory is writable in the target - # - # @param dir_path [String] the target directory path - # @return [Boolean] true if target directory is writable, false otherwise - def busy_box_is_writable_dir?(dir_path) - res = false - rand_str = Rex::Text.rand_text_alpha(16) - file_path = "#{dir_path}/#{rand_str}" - - cmd_exec("echo #{rand_str}XXX#{rand_str} > #{file_path}") - Rex::sleep(0.3) - rcv = read_file(file_path) - - if rcv.include?("#{rand_str}XXX#{rand_str}") - res = true - end - - cmd_exec("rm -f #{file_path}") - Rex::sleep(0.3) - - res - end - - # Checks some directories that usually are writable in devices running busybox - # - # @return [String] If the function finds a writable directory, it returns the path. Else it returns nil - # - def busy_box_writable_dir - dirs = %w(/etc/ /mnt/ /var/ /var/tmp/) - - dirs.each do |d| - return d if busy_box_is_writable_dir?(d) - end - - nil - end - - - # Writes data to a file - # - # @param file_path [String] the file path to write on the target - # @param data [String] the content to be written - # @param prepend [Boolean] if true, prepend the data to the target file. Otherwise, overwrite - # the target file - # @return [Boolean] true if target file is writable and it was written. Otherwise, false. - # @note BusyBox commands are limited and Msf::Post::File#write_file doesn't work here, because - # of it is necessary to implement an specific method. - def busy_box_write_file(file_path, data, prepend = false) - if prepend - dir = busy_box_writable_dir - return false unless dir - cmd_exec("cp -f #{file_path} #{dir}tmp") - Rex::sleep(0.3) + class Post + module Linux + module BusyBox + include ::Msf::Post::Common + include ::Msf::Post::File + + # + # Checks if the file exists in the target + # + # @param file_path [String] the target file path + # @return [Boolean] true if files exists, false otherwise + # @note Msf::Post::File#file? doesnt work because test -f is not available in busybox + # + def busy_box_file_exist?(file_path) + contents = read_file(file_path) + if contents.nil? || contents.empty? + return false + end + + true + end + + # + # Checks if the directory is writable in the target + # + # @param dir_path [String] the target directory path + # @return [Boolean] true if target directory is writable, false otherwise + # + def busy_box_is_writable_dir?(dir_path) + res = false + rand_str = Rex::Text.rand_text_alpha(16) + file_path = "#{dir_path}/#{rand_str}" + + cmd_exec("echo #{rand_str}XXX#{rand_str} > #{file_path}") + Rex.sleep(0.3) + rcv = read_file(file_path) + + if rcv.include?("#{rand_str}XXX#{rand_str}") + res = true + end + + cmd_exec("rm -f #{file_path}") + Rex.sleep(0.3) + + res + end + + # + # Checks some directories that usually are writable in devices running busybox + # + # @return [String] If the function finds a writable directory, it returns the path. Else it returns nil + # + def busy_box_writable_dir + dirs = %w[/etc/ /mnt/ /var/ /var/tmp/] + + dirs.each do |d| + return d if busy_box_is_writable_dir?(d) + end + + nil + end + + # + # Writes data to a file + # + # @param file_path [String] the file path to write on the target + # @param data [String] the content to be written + # @param prepend [Boolean] if true, prepend the data to the target file. Otherwise, overwrite + # the target file + # @return [Boolean] true if target file is writable and it was written. Otherwise, false. + # @note BusyBox commands are limited and Msf::Post::File#write_file doesn't work here, because + # of it is necessary to implement an specific method. + # + def busy_box_write_file(file_path, data, prepend = false) + if prepend + dir = busy_box_writable_dir + return false unless dir + + cmd_exec("cp -f #{file_path} #{dir}tmp") + Rex.sleep(0.3) + end + + rand_str = Rex::Text.rand_text_alpha(16) + cmd_exec("echo #{rand_str} > #{file_path}") + Rex.sleep(0.3) + + unless read_file(file_path).include?(rand_str) + return false + end + + cmd_exec("echo \"\"> #{file_path}") + Rex.sleep(0.3) + + lines = data.lines.map(&:chomp) + lines.each do |line| + cmd_exec("echo #{line.chomp} >> #{file_path}") + Rex.sleep(0.3) + end + + if prepend + cmd_exec("cat #{dir}tmp >> #{file_path}") + Rex.sleep(0.3) + + cmd_exec("rm -f #{dir}tmp") + Rex.sleep(0.3) + end + + true + end + end end - - rand_str = Rex::Text.rand_text_alpha(16) - cmd_exec("echo #{rand_str} > #{file_path}") - Rex::sleep(0.3) - - unless read_file(file_path).include?(rand_str) - return false - end - - cmd_exec("echo \"\"> #{file_path}") - Rex::sleep(0.3) - - lines = data.lines.map(&:chomp) - lines.each do |line| - cmd_exec("echo #{line.chomp} >> #{file_path}") - Rex::sleep(0.3) - end - - if prepend - cmd_exec("cat #{dir}tmp >> #{file_path}") - Rex::sleep(0.3) - - cmd_exec("rm -f #{dir}tmp") - Rex::sleep(0.3) - end - - true end -end # Busybox -end # Linux -end # Post -end # Msf +end diff --git a/lib/msf/core/post/linux/compile.rb b/lib/msf/core/post/linux/compile.rb index e05b830a39b6..9857174fee7f 100644 --- a/lib/msf/core/post/linux/compile.rb +++ b/lib/msf/core/post/linux/compile.rb @@ -5,6 +5,7 @@ class Post module Linux module Compile include ::Msf::Post::Common + include ::Msf::Post::Linux::System include ::Msf::Post::File include ::Msf::Post::Unix @@ -62,14 +63,15 @@ def live_compile? # @raise [Module::Failure::BadConfig] If compilation fails or no compiler is found. # def upload_and_compile(path, data, compiler_args = '') - write_file "#{path}.c", strip_comments(data) - compiler = datastore['COMPILER'] if datastore['COMPILER'] == 'Auto' compiler = get_compiler - fail_with(Module::Failure::BadConfig, 'Unable to find a compiler on the remote target.') unless compiler.present? + fail_with(Module::Failure::BadConfig, 'Unable to find a compiler on the remote target.') if compiler.nil? end + # only upload the file if a compiler exists + write_file "#{path}.c", strip_comments(data) + compiler_cmd = "#{compiler} -o '#{path}' '#{path}.c'" if session.type == 'shell' compiler_cmd = "PATH=\"$PATH:/usr/bin/\" #{compiler_cmd}" diff --git a/spec/lib/msf/core/post/linux/compile_spec.rb b/spec/lib/msf/core/post/linux/compile_spec.rb new file mode 100644 index 000000000000..ae65103707b9 --- /dev/null +++ b/spec/lib/msf/core/post/linux/compile_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +RSpec.describe Msf::Post::Linux::Compile do + subject do + mod = Msf::Module.new + mod.extend(Msf::Post::Linux::Compile) + mod + end + + describe '#get_compiler' do + context 'when gcc is available' do + it 'returns gcc' do + allow(subject).to receive(:has_gcc?).and_return(true) + expect(subject.get_compiler).to eq('gcc') + end + end + + context 'when clang is available' do + it 'returns clang' do + allow(subject).to receive(:has_gcc?).and_return(false) + allow(subject).to receive(:has_clang?).and_return(true) + expect(subject.get_compiler).to eq('clang') + end + end + + context 'when no compiler is available' do + it 'returns nil' do + allow(subject).to receive(:has_gcc?).and_return(false) + allow(subject).to receive(:has_clang?).and_return(false) + expect(subject.get_compiler).to be_nil + end + end + + describe '#live_compile?' do + context 'when COMPILE is not Auto or True' do + it 'returns false' do + allow(subject).to receive(:datastore).and_return({ 'COMPILE' => 'False' }) + expect(subject.live_compile?).to be false + end + end + + context 'when COMPILE is Auto or True' do + it 'returns true if gcc is specified and available' do + allow(subject).to receive(:datastore).and_return({ 'COMPILE' => 'Auto', 'COMPILER' => 'gcc' }) + allow(subject).to receive(:has_gcc?).and_return(true) + expect(subject.live_compile?).to be true + end + + it 'returns true if clang is specified and available' do + allow(subject).to receive(:datastore).and_return({ 'COMPILE' => 'Auto', 'COMPILER' => 'clang' }) + allow(subject).to receive(:has_clang?).and_return(true) + expect(subject.live_compile?).to be true + end + + it 'returns true if Auto is specified and a compiler is available' do + allow(subject).to receive(:datastore).and_return({ 'COMPILE' => 'Auto', 'COMPILER' => 'Auto' }) + allow(subject).to receive(:get_compiler).and_return('gcc') + expect(subject.live_compile?).to be true + end + + it 'raises an error if the specified compiler is not available' do + allow(subject).to receive(:datastore).and_return({ 'COMPILE' => 'True', 'COMPILER' => 'gcc' }) + allow(subject).to receive(:has_gcc?).and_return(false) + expect { subject.live_compile? }.to raise_error(Msf::Module::Failure::BadConfig, 'gcc is not installed. Set COMPILE False to upload a pre-compiled executable') + end + end + + describe '#upload_and_compile' do + let(:source) { '/path/to/source.c' } + let(:destination) { '/tmp/source.c' } + let(:output) { '/tmp/output' } + let(:session) { double('session') } + + before do + allow(subject).to receive(:get_compiler).and_return('gcc') + allow(subject).to receive(:session).and_return(session) + end + + it 'uploads the source file and compiles it' do + expect(subject).to receive(:upload_file).with(destination, source) + expect(subject).to receive(:cmd_exec).with("gcc #{destination} -o #{output}") + expect(subject).to receive(:write_file).and_return('/tmp/foo') + allow(session).to receive(:type).and_return('meterpreter') + + subject.upload_and_compile(source, destination, output) + end + + it 'raises an error if no compiler is available' do + allow(subject).to receive(:get_compiler).and_return(nil) + + expect { subject.upload_and_compile(source, destination, output) }.to raise_error('No compiler available on target') + end + end + + describe '#strip_comments' do + it 'removes comments from the source code' do + source_code = <<-CODE + // This is a single line comment + int main() { + /* This is a + multi-line comment */ + printf("Hello, world!"); + return 0; + } + CODE + + expected_output = <<-CODE + + int main() { + #{' '} + printf("Hello, world!"); + return 0; + } + CODE + + expect(subject.strip_comments(source_code)).to eq(expected_output) + end + end + end + end +end diff --git a/spec/lib/msf/core/post/linux/kernel.rb b/spec/lib/msf/core/post/linux/kernel_spec.rb similarity index 100% rename from spec/lib/msf/core/post/linux/kernel.rb rename to spec/lib/msf/core/post/linux/kernel_spec.rb diff --git a/spec/lib/msf/core/post/linux/packages.rb b/spec/lib/msf/core/post/linux/packages_spec.rb similarity index 100% rename from spec/lib/msf/core/post/linux/packages.rb rename to spec/lib/msf/core/post/linux/packages_spec.rb diff --git a/spec/lib/msf/core/post/linux/priv_spec.rb b/spec/lib/msf/core/post/linux/priv_spec.rb new file mode 100644 index 000000000000..66fb223d15ea --- /dev/null +++ b/spec/lib/msf/core/post/linux/priv_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +RSpec.describe Msf::Post::Linux::Priv do + subject do + mod = Msf::Module.new + mod.extend(Msf::Post::Linux::Priv) + mod + end + + before do + allow(subject).to receive(:command_exists?).and_return(true) + allow(subject).to receive(:cmd_exec).and_return('') + end + + describe '#is_root?' do + context 'when the id command exists' do + it 'returns true if the user ID is 0' do + allow(subject).to receive(:cmd_exec).with('id -u').and_return('0') + expect(subject.is_root?).to be true + end + + it 'returns false if the user ID is not 0' do + allow(subject).to receive(:cmd_exec).with('id -u').and_return('1000') + expect(subject.is_root?).to be false + end + + it 'raises an error if the user ID cannot be determined' do + allow(subject).to receive(:cmd_exec).with('id -u').and_return('abc') + expect { subject.is_root? }.to raise_error(RuntimeError, 'Could not determine UID: "abc"') + end + end + end + + describe '#cp_cmd' do + it 'copies the content of one file to another' do + origin_file = '/path/to/origin' + final_file = '/path/to/destination' + file_content = 'file content' + + allow(subject).to receive(:read_file).with(origin_file).and_return(file_content) + expect(subject).to receive(:cmd_exec).with("echo '#{file_content}' > #{final_file}") + + subject.cp_cmd(origin_file, final_file) + end +end + +describe '#binary_of_pid' do + it 'retrieves the binary name of a process given its PID' do + pid = 1234 + cmdline_content = '/usr/bin/bash' + comm_content = 'bash' + + allow(subject).to receive(:read_file).with("/proc/#{pid}/cmdline").and_return(cmdline_content) + expect(subject.binary_of_pid(pid)).to eq('/usr/bin/bash') + + allow(subject).to receive(:read_file).with("/proc/#{pid}/cmdline").and_return('') + allow(subject).to receive(:read_file).with("/proc/#{pid}/comm").and_return(comm_content) + expect(subject.binary_of_pid(pid)).to eq('bash') + end +end + +describe '#seq' do + it 'generates a sequence of numbers from first to last with a given increment' do + expect(subject.seq(1, 2, 10)).to eq([1, 3, 5, 7, 9]) + expect(subject.seq(0, 5, 20)).to eq([0, 5, 10, 15, 20]) + end +end + +describe '#wc_cmd' do + it 'returns the number of lines, words, and characters in a file' do + file = '/path/to/file' + allow(subject).to receive(:nlines_file).with(file).and_return(10) + allow(subject).to receive(:nwords_file).with(file).and_return(20) + allow(subject).to receive(:nchars_file).with(file).and_return(100) + + expect(subject.wc_cmd(file)).to eq([10, 20, 100, file]) + end +end + +describe '#nchars_file' do + it 'returns the number of characters in a file' do + file = '/path/to/file' + file_content = "Hello\nWorld" + allow(subject).to receive(:read_file).with(file).and_return(file_content) + + expect(subject.nchars_file(file)).to eq(10) + end +end + +describe '#nwords_file' do + it 'returns the number of words in a file' do + file = '/path/to/file' + file_content = "Hello World\nThis is a test" + allow(subject).to receive(:read_file).with(file).and_return(file_content) + + expect(subject.nwords_file(file)).to eq(5) + end +end + +describe '#nlines_file' do + it 'returns the number of lines in a file' do + file = '/path/to/file' + file_content = "Hello\nWorld\nThis is a test" + allow(subject).to receive(:read_file).with(file).and_return(file_content) + + expect(subject.nlines_file(file)).to eq(3) + end +end + +describe '#head_cmd' do + it 'returns the first n lines of a file' do + file = '/path/to/file' + file_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + allow(subject).to receive(:read_file).with(file).and_return(file_content) + + expect(subject.head_cmd(file, 3)).to eq(["Line 1", "Line 2", "Line 3"]) + end +end + +describe '#tail_cmd' do + it 'returns the last n lines of a file' do + file = '/path/to/file' + file_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + allow(subject).to receive(:read_file).with(file).and_return(file_content) + + expect(subject.tail_cmd(file, 3)).to eq(["Line 3", "Line 4", "Line 5"]) + end +end + +describe '#grep_cmd' do + it 'searches for a specific string in a file and returns the lines that contain the string' do + file = '/path/to/file' + file_content = "Hello World\nThis is a test\nHello again" + allow(subject).to receive(:read_file).with(file).and_return(file_content) + + expect(subject.grep_cmd(file, 'Hello')).to eq(["Hello World", "Hello again"]) + end +end +end \ No newline at end of file diff --git a/spec/lib/msf/core/post/linux/process_spec.rb b/spec/lib/msf/core/post/linux/process_spec.rb new file mode 100644 index 000000000000..72d4e97449d7 --- /dev/null +++ b/spec/lib/msf/core/post/linux/process_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +RSpec.describe Msf::Post::Linux::Process do + subject do + mod = Msf::Module.new + mod.extend(Msf::Post::Linux::Process) + mod + end + + describe '#mem_read' do + let(:base_address) { 0x1000 } + let(:length) { 64 } + let(:pid) { 1234 } + let(:memory_content) { 'memory content' } + let(:PROCESS_READ) {(1 << 0)} + + it 'reads memory from the specified base address and length' do + expect(subject).to receive(:open).with(pid, PROCESS_READ).and_return(1) + expect(memory).to receive(:read).with(base_address, length).and_return(memory_content) + + result = subject.mem_read(base_address, length, pid: pid) + expect(result).to eq(memory_content) + end + + it 'uses the default pid if not specified' do + expect(subject).to receive(:open).with(0, PROCESS_READ).and_return(1) + expect(memory).to receive(:read).with(base_address, length).and_return(memory_content) + + result = subject.mem_read(base_address, length) + expect(result).to eq(memory_content) + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/core/post/linux/system.rb b/spec/lib/msf/core/post/linux/system_spec.rb similarity index 100% rename from spec/lib/msf/core/post/linux/system.rb rename to spec/lib/msf/core/post/linux/system_spec.rb