From cde660065c1ee5ce9d1a4a67d558888585e12a1c Mon Sep 17 00:00:00 2001 From: h00die Date: Sun, 1 Dec 2024 20:00:58 -0500 Subject: [PATCH] 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