msfupdate 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #!/usr/bin/env ruby
  2. # -*- coding: binary -*-
  3. #
  4. # $Id$
  5. #
  6. # This keeps the framework up-to-date
  7. #
  8. # $Revision$
  9. #
  10. msfbase = __FILE__
  11. while File.symlink?(msfbase)
  12. msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
  13. end
  14. class Msfupdate
  15. attr_reader :stdin
  16. attr_reader :stdout
  17. attr_reader :stderr
  18. def initialize(msfbase_dir, stdin = $stdin, stdout = $stdout, stderr = $stderr)
  19. @msfbase_dir = msfbase_dir
  20. @stdin = stdin
  21. @stdout = stdout
  22. @stderr = stderr
  23. end
  24. def usage(io = stdout)
  25. help = "usage: msfupdate [options...]\n"
  26. help << "Options:\n"
  27. help << "-h, --help show help\n"
  28. help << " --git-remote REMOTE git remote to use (default upstream)\n" if git?
  29. help << " --git-branch BRANCH git branch to use (default master)\n" if git?
  30. help << " --offline-file FILE offline update file to use\n" if binary_install?
  31. io.print help
  32. end
  33. def parse_args(args)
  34. begin
  35. # GetoptLong uses ARGV, but we want to use the args parameter
  36. # Copy args into ARGV, then restore ARGV after GetoptLong
  37. real_args = ARGV.clone
  38. ARGV.clear
  39. args.each { |arg| ARGV << arg }
  40. require 'getoptlong'
  41. opts = GetoptLong.new(
  42. ['--help', '-h', GetoptLong::NO_ARGUMENT],
  43. ['--git-remote', GetoptLong::REQUIRED_ARGUMENT],
  44. ['--git-branch', GetoptLong::REQUIRED_ARGUMENT],
  45. ['--offline-file', GetoptLong::REQUIRED_ARGUMENT]
  46. )
  47. begin
  48. opts.each do |opt, arg|
  49. case opt
  50. when '--help'
  51. usage
  52. maybe_wait_and_exit
  53. when '--git-remote'
  54. @git_remote = arg
  55. when '--git-branch'
  56. @git_branch = arg
  57. when '--offline-file'
  58. @offline_file = File.expand_path(arg)
  59. end
  60. end
  61. rescue GetoptLong::Error
  62. stderr.puts "#{$PROGRAM_NAME}: try 'msfupdate --help' for more information"
  63. maybe_wait_and_exit 0x20
  64. end
  65. # Handle the old wait/nowait argument behavior
  66. if ARGV[0] == 'wait' || ARGV[0] == 'nowait'
  67. @actually_wait = (ARGV.shift == 'wait')
  68. end
  69. ensure
  70. # Restore the original ARGV value
  71. ARGV.clear
  72. real_args.each { |arg| ARGV << arg }
  73. end
  74. end
  75. def validate_args
  76. valid = true
  77. if binary_install? || apt?
  78. if @git_branch
  79. stderr.puts "[-] ERROR: git-branch is not supported on this installation"
  80. valid = false
  81. end
  82. if @git_remote
  83. stderr.puts "[-] ERROR: git-remote is not supported on this installation"
  84. valid = false
  85. end
  86. end
  87. if apt? || git?
  88. if @offline_file
  89. stderr.puts "[-] ERROR: offline-file option is not supported on this installation"
  90. valid = false
  91. end
  92. end
  93. valid
  94. end
  95. def apt?
  96. File.exist?(File.expand_path(File.join(@msfbase_dir, '.apt')))
  97. end
  98. # Are you an installer, or did you get here via a source checkout?
  99. def binary_install?
  100. File.exist?(File.expand_path(File.join(@msfbase_dir, "..", "engine", "update.rb"))) && !apt?
  101. end
  102. def git?
  103. File.directory?(File.join(@msfbase_dir, ".git"))
  104. end
  105. def run!
  106. validate_args || maybe_wait_and_exit(0x13)
  107. stderr.puts "[*]"
  108. stderr.puts "[*] Attempting to update the Metasploit Framework..."
  109. stderr.puts "[*]"
  110. stderr.puts ""
  111. # Bail right away, no waiting around for consoles.
  112. unless Process.uid.zero? || File.stat(@msfbase_dir).owned?
  113. stderr.puts "[-] ERROR: User running msfupdate does not own the Metasploit installation"
  114. stderr.puts "[-] Please run msfupdate as the same user who installed Metasploit."
  115. maybe_wait_and_exit 0x10
  116. end
  117. Dir.chdir(@msfbase_dir) do
  118. if apt?
  119. stderr.puts "[-] ERROR: msfupdate is not supported on Kali Linux."
  120. stderr.puts "[-] Please run 'apt update; apt install metasploit-framework' instead."
  121. elsif binary_install?
  122. update_binary_install!
  123. elsif git?
  124. update_git!
  125. else
  126. raise "Cannot determine checkout type: `#{@msfbase_dir}'"
  127. end
  128. end
  129. end
  130. # We could also do this by running `git config --global user.name` and `git config --global user.email`
  131. # and check the output of those. (it's a bit quieter)
  132. def git_globals_okay?
  133. output = ''
  134. begin
  135. output = `git config --list`
  136. rescue Errno::ENOENT
  137. stderr.puts '[-] ERROR: Failed to check git settings, git not found'
  138. return false
  139. end
  140. unless output.include? 'user.name'
  141. stderr.puts '[-] ERROR: user.name is not set in your global git configuration'
  142. stderr.puts '[-] Set it by running: \'git config --global user.name "NAME HERE"\''
  143. stderr.puts ''
  144. return false
  145. end
  146. unless output.include? 'user.email'
  147. stderr.puts '[-] ERROR: user.email is not set in your global git configuration'
  148. stderr.puts '[-] Set it by running: \'git config --global user.email "email@example.com"\''
  149. stderr.puts ''
  150. return false
  151. end
  152. true
  153. end
  154. def update_git!
  155. ####### Since we're Git, do it all that way #######
  156. stdout.puts "[*] Checking for updates via git"
  157. stdout.puts "[*] Note: Updating from bleeding edge"
  158. out = `git remote show upstream` # Actually need the output for this one.
  159. add_git_upstream unless $?.success? &&
  160. out =~ %r{(https|git|git@github\.com):(//github\.com/)?(rapid7/metasploit-framework\.git)}
  161. remote = @git_remote || "upstream"
  162. branch = @git_branch || "master"
  163. # This will save local changes in a stash, but won't
  164. # attempt to reapply them. If the user wants them back
  165. # they can always git stash pop them, and that presumes
  166. # they know what they're doing when they're editing local
  167. # checkout, which presumes they're not using msfupdate
  168. # to begin with.
  169. #
  170. # Note, this requires at least user.name and user.email
  171. # to be configured in the global git config. Installers
  172. # will be told to set them if they aren't already set.
  173. # Checks user.name and user.email
  174. global_status = git_globals_okay?
  175. maybe_wait_and_exit(1) unless global_status
  176. # We shouldn't get here if the globals dont check out
  177. committed = system("git", "diff", "--quiet", "HEAD")
  178. if committed.nil?
  179. stderr.puts "[-] ERROR: Failed to run git"
  180. stderr.puts ""
  181. stderr.puts "[-] If you used a binary installer, make sure you run the symlink in"
  182. stderr.puts "[-] /usr/local/bin instead of running this file directly (e.g.: ./msfupdate)"
  183. stderr.puts "[-] to ensure a proper environment."
  184. maybe_wait_and_exit 1
  185. elsif !committed
  186. system("git", "stash")
  187. stdout.puts "[*] Stashed local changes to avoid merge conflicts."
  188. stdout.puts "[*] Run `git stash pop` to reapply local changes."
  189. end
  190. system("git", "reset", "HEAD", "--hard")
  191. system("git", "checkout", branch)
  192. system("git", "fetch", remote)
  193. system("git", "merge", "#{remote}/#{branch}")
  194. stdout.puts "[*] Updating gems..."
  195. begin
  196. require 'bundler'
  197. rescue LoadError
  198. stderr.puts '[*] Installing bundler'
  199. system('gem', 'install', 'bundler')
  200. Gem.clear_paths
  201. require 'bundler'
  202. end
  203. Bundler.with_clean_env do
  204. if File::exist? "Gemfile.local"
  205. system("bundle", "install", "--gemfile", "Gemfile.local")
  206. else
  207. system("bundle", "install")
  208. end
  209. end
  210. end
  211. def update_binary_install!
  212. update_script = File.expand_path(File.join(@msfbase_dir, "..", "engine", "update.rb"))
  213. product_key = File.expand_path(File.join(@msfbase_dir, "..", "engine", "license", "product.key"))
  214. if File.exist? product_key
  215. if File.readable? product_key
  216. if @offline_file
  217. system("ruby", update_script, @offline_file)
  218. else
  219. system("ruby", update_script)
  220. end
  221. else
  222. stdout.puts "[-] ERROR: Failed to update Metasploit installation"
  223. stdout.puts ""
  224. stdout.puts "[-] You must be able to read the product key for the"
  225. stdout.puts "[-] Metasploit installation in order to run msfupdate."
  226. stdout.puts "[-] Usually, this means you must be root (EUID 0)."
  227. maybe_wait_and_exit 10
  228. end
  229. else
  230. stdout.puts "[-] ERROR: Failed to update Metasploit installation"
  231. stdout.puts ""
  232. stdout.puts "[-] In order to update your Metasploit installation,"
  233. stdout.puts "[-] you must first register it through the UI, here:"
  234. stderr.puts "[-] https://localhost:3790"
  235. stderr.puts "[-] (Note: Metasploit Community Edition is totally"
  236. stderr.puts "[-] free and takes just a few seconds to register!)"
  237. maybe_wait_and_exit 11
  238. end
  239. end
  240. # Adding an upstream enables msfupdate to pull updates from
  241. # Rapid7's metasploit-framework repo instead of the repo
  242. # the user originally cloned or forked.
  243. def add_git_upstream
  244. stdout.puts "[*] Attempting to add remote 'upstream' to your local git repository."
  245. system("git", "remote", "add", "upstream", "git://github.com/rapid7/metasploit-framework.git")
  246. stdout.puts "[*] Added remote 'upstream' to your local git repository."
  247. end
  248. # This only exits if you actually pass a wait option, otherwise
  249. # just returns nil. This is likely unexpected, revisit this.
  250. def maybe_wait_and_exit(exit_code = 0)
  251. if @actually_wait
  252. stdout.puts ""
  253. stdout.puts "[*] Please hit enter to exit"
  254. stdout.puts ""
  255. stdin.readline
  256. end
  257. exit exit_code
  258. end
  259. def apt_upgrade_available(package)
  260. require 'open3'
  261. installed = nil
  262. upgrade = nil
  263. ::Open3.popen3({ 'LANG' => 'en_US.UTF-8' }, "apt-cache", "policy", package) do |_stdin, stdout, _stderr|
  264. stdout.each do |line|
  265. installed = $1 if line =~ /Installed: ([\w\-+.:~]+)$/
  266. upgrade = $1 if line =~ /Candidate: ([\w\-+.:~]+)$/
  267. break if installed && upgrade
  268. end
  269. end
  270. if installed && installed != upgrade
  271. upgrade
  272. else
  273. nil
  274. end
  275. end
  276. end
  277. if __FILE__ == $PROGRAM_NAME
  278. cli = Msfupdate.new(File.dirname(msfbase))
  279. cli.parse_args(ARGV.dup)
  280. cli.run!
  281. cli.maybe_wait_and_exit
  282. end