From fa2ab95709e6fa004a77df0d8f5ebb3411a0023b Mon Sep 17 00:00:00 2001 From: 0xGrooted Date: Mon, 29 Sep 2025 17:11:34 +0100 Subject: [PATCH 1/4] Fix(meterpreter): Prevent truncation of remote file when using 'edit' (GH-20574) --- .../console/command_dispatcher/stdapi/fs.rb | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb index 676be4ace17e8..8d340c52ed066 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb @@ -566,7 +566,9 @@ def cmd_download(*args) def cmd_edit_help print_line('Edit a file on remote machine.') - print_line("Usage: edit file") + print_line("Usage: edit ") + print_line('The file is safely downloaded to a temporary location, edited locally,') + print_line('and then uploaded back to the remote path (atomic replace).') print_line end @@ -575,32 +577,29 @@ def cmd_edit_help # the contents to the remote machine after completion. # def cmd_edit(*args) - if args.empty? || args.include?('-h') + if args.include?('-h') || args.include?('-?') cmd_edit_help return true end - # Get a temporary file path - meterp_temp = Tempfile.new('meterp') - meterp_temp.binmode - temp_path = meterp_temp.path - - client_path = args[0] - client_path = client.fs.file.expand_path(client_path) if client_path =~ path_expand_regex - - # Try to download the file, but don't worry if it doesn't exist - client.fs.file.download_file(temp_path, client_path) rescue nil + if args.empty? || args.length > 1 + print_error("Usage: edit ") + return false + end - # Spawn the editor (default to vi) - editor = Rex::Compat.getenv('EDITOR') || 'vi' + path = args[0] - # If it succeeds, upload it to the remote side. - if (system("#{editor} #{temp_path}") == true) - client.fs.file.upload_file(client_path, temp_path) + begin + client.fs.file.edit(path) + print_status("Edited #{path}") + return true + rescue ::Rex::Post::Meterpreter::RequestError => e + print_error("Failed to edit #{path}: #{e.message}") + return false + rescue ::Exception => e + print_error("An error occurred during edit: #{e.class} #{e}") + return false end - - # Get rid of that pesky temporary file - ::File.delete(temp_path) rescue nil end alias :cmd_edit_tabs :cmd_cat_tabs From b649576533e591086f45602d430b457373107475 Mon Sep 17 00:00:00 2001 From: 0xGrooted Date: Sun, 5 Oct 2025 16:27:59 +0100 Subject: [PATCH 2/4] Updated Truncation logic in meterpreter --- .../console/command_dispatcher/stdapi/fs.rb | 88 ++++++++++++++++--- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb index 8d340c52ed066..fd73bdbb5059f 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb @@ -576,32 +576,92 @@ def cmd_edit_help # Downloads a file to a temporary file, spawns and editor, and then uploads # the contents to the remote machine after completion. # - def cmd_edit(*args) - if args.include?('-h') || args.include?('-?') - cmd_edit_help - return true - end + if args.empty? || args.length > 1 || args.include?('-h') + cmd_edit_help + return true + end - if args.empty? || args.length > 1 - print_error("Usage: edit ") + meterp_temp = Tempfile.new('meterp') + meterp_temp.binmode + temp_path = meterp_temp.path + + begin + client_path = args[0] + client_path = client.fs.file.expand_path(client_path) if client_path =~ path_expand_regex + + # Verify the file exists and is not a directory + begin + stat = client.fs.file.stat(client_path) + rescue ::Rex::Post::Meterpreter::RequestError => e + print_error("Cannot access #{client_path}: #{e.message}") return false end - path = args[0] + if stat.directory? + print_error("#{client_path} is a directory, not a file") + return false + end + # Download using the same approach as cmd_cat + print_status("Downloading #{client_path}...") + begin - client.fs.file.edit(path) - print_status("Edited #{path}") - return true + fd = client.fs.file.new(client_path, "rb") + begin + until fd.eof? + data = fd.read + meterp_temp.write(data) if data + end + rescue EOFError + # EOFError is raised if file is empty or EOF reached, which is normal + end + fd.close + + meterp_temp.flush + + # Verify something was downloaded for non-empty files + local_size = ::File.size?(temp_path) || 0 + + if local_size == 0 && stat.size > 0 + print_error("Download failed: expected #{stat.size} bytes but got 0") + return false + end + + print_status("Downloaded #{local_size} bytes") + rescue ::Rex::Post::Meterpreter::RequestError => e - print_error("Failed to edit #{path}: #{e.message}") + print_error("Failed to download #{client_path}: #{e.message}") return false - rescue ::Exception => e - print_error("An error occurred during edit: #{e.class} #{e}") + rescue => e + print_error("Failed to download #{client_path}: #{e.class} - #{e.message}") return false end + + # Close the temp file so the editor can open it + meterp_temp.close + + # Open the file in the user's editor + editor = Rex::Compat.getenv('EDITOR') || 'vi' + + if system("#{editor} #{temp_path}") + begin + print_status("Uploading changes to #{client_path}...") + client.fs.file.upload_file(client_path, temp_path) + print_status("Upload complete") + rescue ::Rex::Post::Meterpreter::RequestError => e + print_error("Failed to upload edited file to #{client_path}: #{e.message}") + end + else + print_error("Editor exited with an error. Upload cancelled.") + end + + ensure + meterp_temp.close! if meterp_temp && !meterp_temp.closed? end + true +end + alias :cmd_edit_tabs :cmd_cat_tabs def cmd_ls_help From 65148ffc09f79ff686b9a711598f6492537a52eb Mon Sep 17 00:00:00 2001 From: 0xGrooted Date: Sun, 5 Oct 2025 16:35:03 +0100 Subject: [PATCH 3/4] Updated fs.rb - formatting fix --- .../ui/console/command_dispatcher/stdapi/fs.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb index fd73bdbb5059f..388fd5728f2cc 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb @@ -566,16 +566,11 @@ def cmd_download(*args) def cmd_edit_help print_line('Edit a file on remote machine.') - print_line("Usage: edit ") - print_line('The file is safely downloaded to a temporary location, edited locally,') - print_line('and then uploaded back to the remote path (atomic replace).') + print_line("Usage: edit file") print_line end - # - # Downloads a file to a temporary file, spawns and editor, and then uploads - # the contents to the remote machine after completion. - # +def cmd_edit(*args) if args.empty? || args.length > 1 || args.include?('-h') cmd_edit_help return true @@ -661,7 +656,7 @@ def cmd_edit_help true end - + alias :cmd_edit_tabs :cmd_cat_tabs def cmd_ls_help From 4321f414483372b361cf8819c6749f8fda82c827 Mon Sep 17 00:00:00 2001 From: 0xGrooted Date: Sun, 5 Oct 2025 16:43:24 +0100 Subject: [PATCH 4/4] Update fs.rb --- .../console/command_dispatcher/stdapi/fs.rb | 144 +++++++++--------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb index 388fd5728f2cc..f3d4ba2482797 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb @@ -570,92 +570,98 @@ def cmd_edit_help print_line end -def cmd_edit(*args) - if args.empty? || args.length > 1 || args.include?('-h') - cmd_edit_help - return true +def cmd_edit_help + print_line('Edit a file on remote machine.') + print_line("Usage: edit file") + print_line end - meterp_temp = Tempfile.new('meterp') - meterp_temp.binmode - temp_path = meterp_temp.path - - begin - client_path = args[0] - client_path = client.fs.file.expand_path(client_path) if client_path =~ path_expand_regex - - # Verify the file exists and is not a directory - begin - stat = client.fs.file.stat(client_path) - rescue ::Rex::Post::Meterpreter::RequestError => e - print_error("Cannot access #{client_path}: #{e.message}") - return false + def cmd_edit(*args) + if args.empty? || args.length > 1 || args.include?('-h') + cmd_edit_help + return true end - if stat.directory? - print_error("#{client_path} is a directory, not a file") - return false - end + meterp_temp = Tempfile.new('meterp') + meterp_temp.binmode + temp_path = meterp_temp.path - # Download using the same approach as cmd_cat - print_status("Downloading #{client_path}...") - begin - fd = client.fs.file.new(client_path, "rb") + client_path = args[0] + client_path = client.fs.file.expand_path(client_path) if client_path =~ path_expand_regex + + # Verify the file exists and is not a directory begin - until fd.eof? - data = fd.read - meterp_temp.write(data) if data - end - rescue EOFError - # EOFError is raised if file is empty or EOF reached, which is normal + stat = client.fs.file.stat(client_path) + rescue ::Rex::Post::Meterpreter::RequestError => e + print_error("Cannot access #{client_path}: #{e.message}") + return false end - fd.close - - meterp_temp.flush - - # Verify something was downloaded for non-empty files - local_size = ::File.size?(temp_path) || 0 - - if local_size == 0 && stat.size > 0 - print_error("Download failed: expected #{stat.size} bytes but got 0") + + if stat.directory? + print_error("#{client_path} is a directory, not a file") return false end + + # Download using the same approach as cmd_cat + print_status("Downloading #{client_path}...") - print_status("Downloaded #{local_size} bytes") - - rescue ::Rex::Post::Meterpreter::RequestError => e - print_error("Failed to download #{client_path}: #{e.message}") - return false - rescue => e - print_error("Failed to download #{client_path}: #{e.class} - #{e.message}") - return false - end + begin + fd = client.fs.file.new(client_path, "rb") + begin + until fd.eof? + data = fd.read + meterp_temp.write(data) if data + end + rescue EOFError + # EOFError is raised if file is empty or EOF reached, which is normal + end + fd.close + + meterp_temp.flush + + # Verify something was downloaded for non-empty files + local_size = ::File.size?(temp_path) || 0 + + if local_size == 0 && stat.size > 0 + print_error("Download failed: expected #{stat.size} bytes but got 0") + return false + end + + print_status("Downloaded #{local_size} bytes") + + rescue ::Rex::Post::Meterpreter::RequestError => e + print_error("Failed to download #{client_path}: #{e.message}") + return false + rescue => e + print_error("Failed to download #{client_path}: #{e.class} - #{e.message}") + return false + end - # Close the temp file so the editor can open it - meterp_temp.close + # Close the temp file so the editor can open it + meterp_temp.close - # Open the file in the user's editor - editor = Rex::Compat.getenv('EDITOR') || 'vi' + # Open the file in the user's editor + editor = Rex::Compat.getenv('EDITOR') || 'vi' - if system("#{editor} #{temp_path}") - begin - print_status("Uploading changes to #{client_path}...") - client.fs.file.upload_file(client_path, temp_path) - print_status("Upload complete") - rescue ::Rex::Post::Meterpreter::RequestError => e - print_error("Failed to upload edited file to #{client_path}: #{e.message}") + if system("#{editor} #{temp_path}") + begin + print_status("Uploading changes to #{client_path}...") + client.fs.file.upload_file(client_path, temp_path) + print_status("Upload complete") + rescue ::Rex::Post::Meterpreter::RequestError => e + print_error("Failed to upload edited file to #{client_path}: #{e.message}") + end + else + print_error("Editor exited with an error. Upload cancelled.") end - else - print_error("Editor exited with an error. Upload cancelled.") + + ensure + meterp_temp.close! if meterp_temp && !meterp_temp.closed? end - - ensure - meterp_temp.close! if meterp_temp && !meterp_temp.closed? - end - true -end + true + end alias :cmd_edit_tabs :cmd_cat_tabs