123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- #!/usr/bin/env ruby
- # -*- coding: binary -*-
- #
- # $Id$
- #
- # This keeps the framework up-to-date
- #
- # $Revision$
- #
- msfbase = __FILE__
- while File.symlink?(msfbase)
- msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
- end
- class Msfupdate
- attr_reader :stdin
- attr_reader :stdout
- attr_reader :stderr
- def initialize(msfbase_dir, stdin = $stdin, stdout = $stdout, stderr = $stderr)
- @msfbase_dir = msfbase_dir
- @stdin = stdin
- @stdout = stdout
- @stderr = stderr
- end
- def usage(io = stdout)
- help = "usage: msfupdate [options...]\n"
- help << "Options:\n"
- help << "-h, --help show help\n"
- help << " --git-remote REMOTE git remote to use (default upstream)\n" if git?
- help << " --git-branch BRANCH git branch to use (default master)\n" if git?
- help << " --offline-file FILE offline update file to use\n" if binary_install?
- io.print help
- end
- def parse_args(args)
- begin
- # GetoptLong uses ARGV, but we want to use the args parameter
- # Copy args into ARGV, then restore ARGV after GetoptLong
- real_args = ARGV.clone
- ARGV.clear
- args.each { |arg| ARGV << arg }
- require 'getoptlong'
- opts = GetoptLong.new(
- ['--help', '-h', GetoptLong::NO_ARGUMENT],
- ['--git-remote', GetoptLong::REQUIRED_ARGUMENT],
- ['--git-branch', GetoptLong::REQUIRED_ARGUMENT],
- ['--offline-file', GetoptLong::REQUIRED_ARGUMENT]
- )
- begin
- opts.each do |opt, arg|
- case opt
- when '--help'
- usage
- maybe_wait_and_exit
- when '--git-remote'
- @git_remote = arg
- when '--git-branch'
- @git_branch = arg
- when '--offline-file'
- @offline_file = File.expand_path(arg)
- end
- end
- rescue GetoptLong::Error
- stderr.puts "#{$PROGRAM_NAME}: try 'msfupdate --help' for more information"
- maybe_wait_and_exit 0x20
- end
- # Handle the old wait/nowait argument behavior
- if ARGV[0] == 'wait' || ARGV[0] == 'nowait'
- @actually_wait = (ARGV.shift == 'wait')
- end
- ensure
- # Restore the original ARGV value
- ARGV.clear
- real_args.each { |arg| ARGV << arg }
- end
- end
- def validate_args
- valid = true
- if binary_install? || apt?
- if @git_branch
- stderr.puts "[-] ERROR: git-branch is not supported on this installation"
- valid = false
- end
- if @git_remote
- stderr.puts "[-] ERROR: git-remote is not supported on this installation"
- valid = false
- end
- end
- if apt? || git?
- if @offline_file
- stderr.puts "[-] ERROR: offline-file option is not supported on this installation"
- valid = false
- end
- end
- valid
- end
- def apt?
- File.exist?(File.expand_path(File.join(@msfbase_dir, '.apt')))
- end
- # Are you an installer, or did you get here via a source checkout?
- def binary_install?
- File.exist?(File.expand_path(File.join(@msfbase_dir, "..", "engine", "update.rb"))) && !apt?
- end
- def git?
- File.directory?(File.join(@msfbase_dir, ".git"))
- end
- def run!
- validate_args || maybe_wait_and_exit(0x13)
- stderr.puts "[*]"
- stderr.puts "[*] Attempting to update the Metasploit Framework..."
- stderr.puts "[*]"
- stderr.puts ""
- # Bail right away, no waiting around for consoles.
- unless Process.uid.zero? || File.stat(@msfbase_dir).owned?
- stderr.puts "[-] ERROR: User running msfupdate does not own the Metasploit installation"
- stderr.puts "[-] Please run msfupdate as the same user who installed Metasploit."
- maybe_wait_and_exit 0x10
- end
- Dir.chdir(@msfbase_dir) do
- if apt?
- stderr.puts "[-] ERROR: msfupdate is not supported on Kali Linux."
- stderr.puts "[-] Please run 'apt update; apt install metasploit-framework' instead."
- elsif binary_install?
- update_binary_install!
- elsif git?
- update_git!
- else
- raise "Cannot determine checkout type: `#{@msfbase_dir}'"
- end
- end
- end
- # We could also do this by running `git config --global user.name` and `git config --global user.email`
- # and check the output of those. (it's a bit quieter)
- def git_globals_okay?
- output = ''
- begin
- output = `git config --list`
- rescue Errno::ENOENT
- stderr.puts '[-] ERROR: Failed to check git settings, git not found'
- return false
- end
- unless output.include? 'user.name'
- stderr.puts '[-] ERROR: user.name is not set in your global git configuration'
- stderr.puts '[-] Set it by running: \'git config --global user.name "NAME HERE"\''
- stderr.puts ''
- return false
- end
- unless output.include? 'user.email'
- stderr.puts '[-] ERROR: user.email is not set in your global git configuration'
- stderr.puts '[-] Set it by running: \'git config --global user.email "email@example.com"\''
- stderr.puts ''
- return false
- end
- true
- end
- def update_git!
- ####### Since we're Git, do it all that way #######
- stdout.puts "[*] Checking for updates via git"
- stdout.puts "[*] Note: Updating from bleeding edge"
- out = `git remote show upstream` # Actually need the output for this one.
- add_git_upstream unless $?.success? &&
- out =~ %r{(https|git|git@github\.com):(//github\.com/)?(rapid7/metasploit-framework\.git)}
- remote = @git_remote || "upstream"
- branch = @git_branch || "master"
- # This will save local changes in a stash, but won't
- # attempt to reapply them. If the user wants them back
- # they can always git stash pop them, and that presumes
- # they know what they're doing when they're editing local
- # checkout, which presumes they're not using msfupdate
- # to begin with.
- #
- # Note, this requires at least user.name and user.email
- # to be configured in the global git config. Installers
- # will be told to set them if they aren't already set.
- # Checks user.name and user.email
- global_status = git_globals_okay?
- maybe_wait_and_exit(1) unless global_status
- # We shouldn't get here if the globals dont check out
- committed = system("git", "diff", "--quiet", "HEAD")
- if committed.nil?
- stderr.puts "[-] ERROR: Failed to run git"
- stderr.puts ""
- stderr.puts "[-] If you used a binary installer, make sure you run the symlink in"
- stderr.puts "[-] /usr/local/bin instead of running this file directly (e.g.: ./msfupdate)"
- stderr.puts "[-] to ensure a proper environment."
- maybe_wait_and_exit 1
- elsif !committed
- system("git", "stash")
- stdout.puts "[*] Stashed local changes to avoid merge conflicts."
- stdout.puts "[*] Run `git stash pop` to reapply local changes."
- end
- system("git", "reset", "HEAD", "--hard")
- system("git", "checkout", branch)
- system("git", "fetch", remote)
- system("git", "merge", "#{remote}/#{branch}")
- stdout.puts "[*] Updating gems..."
- begin
- require 'bundler'
- rescue LoadError
- stderr.puts '[*] Installing bundler'
- system('gem', 'install', 'bundler')
- Gem.clear_paths
- require 'bundler'
- end
- Bundler.with_clean_env do
- if File::exist? "Gemfile.local"
- system("bundle", "install", "--gemfile", "Gemfile.local")
- else
- system("bundle", "install")
- end
- end
- end
- def update_binary_install!
- update_script = File.expand_path(File.join(@msfbase_dir, "..", "engine", "update.rb"))
- product_key = File.expand_path(File.join(@msfbase_dir, "..", "engine", "license", "product.key"))
- if File.exist? product_key
- if File.readable? product_key
- if @offline_file
- system("ruby", update_script, @offline_file)
- else
- system("ruby", update_script)
- end
- else
- stdout.puts "[-] ERROR: Failed to update Metasploit installation"
- stdout.puts ""
- stdout.puts "[-] You must be able to read the product key for the"
- stdout.puts "[-] Metasploit installation in order to run msfupdate."
- stdout.puts "[-] Usually, this means you must be root (EUID 0)."
- maybe_wait_and_exit 10
- end
- else
- stdout.puts "[-] ERROR: Failed to update Metasploit installation"
- stdout.puts ""
- stdout.puts "[-] In order to update your Metasploit installation,"
- stdout.puts "[-] you must first register it through the UI, here:"
- stderr.puts "[-] https://localhost:3790"
- stderr.puts "[-] (Note: Metasploit Community Edition is totally"
- stderr.puts "[-] free and takes just a few seconds to register!)"
- maybe_wait_and_exit 11
- end
- end
- # Adding an upstream enables msfupdate to pull updates from
- # Rapid7's metasploit-framework repo instead of the repo
- # the user originally cloned or forked.
- def add_git_upstream
- stdout.puts "[*] Attempting to add remote 'upstream' to your local git repository."
- system("git", "remote", "add", "upstream", "git://github.com/rapid7/metasploit-framework.git")
- stdout.puts "[*] Added remote 'upstream' to your local git repository."
- end
- # This only exits if you actually pass a wait option, otherwise
- # just returns nil. This is likely unexpected, revisit this.
- def maybe_wait_and_exit(exit_code = 0)
- if @actually_wait
- stdout.puts ""
- stdout.puts "[*] Please hit enter to exit"
- stdout.puts ""
- stdin.readline
- end
- exit exit_code
- end
- def apt_upgrade_available(package)
- require 'open3'
- installed = nil
- upgrade = nil
- ::Open3.popen3({ 'LANG' => 'en_US.UTF-8' }, "apt-cache", "policy", package) do |_stdin, stdout, _stderr|
- stdout.each do |line|
- installed = $1 if line =~ /Installed: ([\w\-+.:~]+)$/
- upgrade = $1 if line =~ /Candidate: ([\w\-+.:~]+)$/
- break if installed && upgrade
- end
- end
- if installed && installed != upgrade
- upgrade
- else
- nil
- end
- end
- end
- if __FILE__ == $PROGRAM_NAME
- cli = Msfupdate.new(File.dirname(msfbase))
- cli.parse_args(ARGV.dup)
- cli.run!
- cli.maybe_wait_and_exit
- end
|