gophish-ruby 0.1.0 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +342 -2
- data/Rakefile +1 -1
- data/docs/API_REFERENCE.md +572 -4
- data/docs/EXAMPLES.md +984 -0
- data/docs/GETTING_STARTED.md +239 -0
- data/lib/gophish/base.rb +6 -24
- data/lib/gophish/group.rb +2 -0
- data/lib/gophish/page.rb +56 -0
- data/lib/gophish/template.rb +94 -0
- data/lib/gophish/version.rb +1 -1
- data/lib/gophish-ruby.rb +2 -1
- metadata +3 -1
data/docs/GETTING_STARTED.md
CHANGED
@@ -84,6 +84,72 @@ else
|
|
84
84
|
end
|
85
85
|
```
|
86
86
|
|
87
|
+
### 4. Create Your First Template
|
88
|
+
|
89
|
+
Templates define the email content for your phishing campaigns:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Create a basic email template
|
93
|
+
template = Gophish::Template.new(
|
94
|
+
name: "Security Awareness Test",
|
95
|
+
subject: "Important Security Update Required",
|
96
|
+
html: "<h1>Security Update</h1><p>Please click <a href='{{.URL}}'>here</a> to update your password.</p>",
|
97
|
+
text: "Security Update\n\nPlease visit {{.URL}} to update your password."
|
98
|
+
)
|
99
|
+
|
100
|
+
if template.save
|
101
|
+
puts "✓ Template created successfully with ID: #{template.id}"
|
102
|
+
else
|
103
|
+
puts "✗ Failed to create template:"
|
104
|
+
template.errors.full_messages.each { |error| puts " - #{error}" }
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
### 5. Create Your First Landing Page
|
109
|
+
|
110
|
+
Landing pages are what users see when they click phishing links:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# Create a basic landing page
|
114
|
+
page = Gophish::Page.new(
|
115
|
+
name: "Microsoft Login Page",
|
116
|
+
html: <<~HTML
|
117
|
+
<!DOCTYPE html>
|
118
|
+
<html>
|
119
|
+
<head>
|
120
|
+
<title>Microsoft Account</title>
|
121
|
+
<style>
|
122
|
+
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
|
123
|
+
.container { max-width: 400px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; }
|
124
|
+
input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; }
|
125
|
+
button { width: 100%; padding: 12px; background: #0078d4; color: white; border: none; border-radius: 4px; }
|
126
|
+
</style>
|
127
|
+
</head>
|
128
|
+
<body>
|
129
|
+
<div class="container">
|
130
|
+
<h2>Sign in to your account</h2>
|
131
|
+
<form method="post">
|
132
|
+
<input type="email" name="username" placeholder="Email" required>
|
133
|
+
<input type="password" name="password" placeholder="Password" required>
|
134
|
+
<button type="submit">Sign in</button>
|
135
|
+
</form>
|
136
|
+
</div>
|
137
|
+
</body>
|
138
|
+
</html>
|
139
|
+
HTML,
|
140
|
+
capture_credentials: true,
|
141
|
+
redirect_url: "https://www.microsoft.com"
|
142
|
+
)
|
143
|
+
|
144
|
+
if page.save
|
145
|
+
puts "✓ Landing page created successfully with ID: #{page.id}"
|
146
|
+
puts " Captures credentials: #{page.captures_credentials?}"
|
147
|
+
else
|
148
|
+
puts "✗ Failed to create page:"
|
149
|
+
page.errors.full_messages.each { |error| puts " - #{error}" }
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
87
153
|
## Common Workflows
|
88
154
|
|
89
155
|
### Importing Targets from CSV
|
@@ -125,6 +191,179 @@ else
|
|
125
191
|
end
|
126
192
|
```
|
127
193
|
|
194
|
+
### Working with Templates
|
195
|
+
|
196
|
+
#### Creating Templates with Attachments
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
# Create template with file attachments
|
200
|
+
template = Gophish::Template.new(
|
201
|
+
name: "Invoice Template",
|
202
|
+
subject: "Your Invoice #{{.RId}}",
|
203
|
+
html: "<p>Dear {{.FirstName}},</p><p>Please find your invoice attached.</p>"
|
204
|
+
)
|
205
|
+
|
206
|
+
# Add PDF attachment
|
207
|
+
pdf_content = File.read("sample_invoice.pdf")
|
208
|
+
template.add_attachment(pdf_content, "application/pdf", "invoice.pdf")
|
209
|
+
|
210
|
+
# Check attachments
|
211
|
+
puts "Template has #{template.attachment_count} attachments" if template.has_attachments?
|
212
|
+
|
213
|
+
template.save
|
214
|
+
```
|
215
|
+
|
216
|
+
#### Importing Email Templates
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
# Import an existing email (.eml file)
|
220
|
+
email_content = File.read("phishing_template.eml")
|
221
|
+
|
222
|
+
imported_data = Gophish::Template.import_email(
|
223
|
+
email_content,
|
224
|
+
convert_links: true # Convert links for Gophish tracking
|
225
|
+
)
|
226
|
+
|
227
|
+
template = Gophish::Template.new(imported_data)
|
228
|
+
template.name = "Imported Email Template"
|
229
|
+
template.save
|
230
|
+
|
231
|
+
puts "Imported template: #{template.name}"
|
232
|
+
```
|
233
|
+
|
234
|
+
#### Managing Existing Templates
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
# List all templates
|
238
|
+
puts "Existing templates:"
|
239
|
+
Gophish::Template.all.each do |template|
|
240
|
+
attachment_info = template.has_attachments? ? " (#{template.attachment_count} attachments)" : ""
|
241
|
+
puts " #{template.id}: #{template.name}#{attachment_info}"
|
242
|
+
end
|
243
|
+
|
244
|
+
# Update a template
|
245
|
+
template = Gophish::Template.find(1)
|
246
|
+
template.subject = "Updated Subject Line"
|
247
|
+
template.html = "<h1>Updated Content</h1><p>New message content here.</p>"
|
248
|
+
|
249
|
+
# Add or remove attachments
|
250
|
+
template.add_attachment(File.read("new_file.pdf"), "application/pdf", "new_file.pdf")
|
251
|
+
template.remove_attachment("old_file.pdf")
|
252
|
+
|
253
|
+
template.save
|
254
|
+
```
|
255
|
+
|
256
|
+
### Working with Landing Pages
|
257
|
+
|
258
|
+
#### Creating Pages with Different Features
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
# Simple page without credential capture
|
262
|
+
basic_page = Gophish::Page.new(
|
263
|
+
name: "Generic Landing Page",
|
264
|
+
html: "<html><body><h1>Thank you!</h1><p>Your action has been completed.</p></body></html>"
|
265
|
+
)
|
266
|
+
|
267
|
+
# Page with credential capture and redirect
|
268
|
+
login_page = Gophish::Page.new(
|
269
|
+
name: "Banking Login Clone",
|
270
|
+
html: <<~HTML
|
271
|
+
<html>
|
272
|
+
<head>
|
273
|
+
<title>Secure Banking</title>
|
274
|
+
<style>
|
275
|
+
body { font-family: Arial, sans-serif; background: #003366; color: white; padding: 50px; }
|
276
|
+
.form-container { max-width: 350px; margin: 0 auto; background: white; color: black; padding: 30px; border-radius: 8px; }
|
277
|
+
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; }
|
278
|
+
button { width: 100%; padding: 12px; background: #003366; color: white; border: none; cursor: pointer; }
|
279
|
+
</style>
|
280
|
+
</head>
|
281
|
+
<body>
|
282
|
+
<div class="form-container">
|
283
|
+
<h2>Online Banking Login</h2>
|
284
|
+
<form method="post">
|
285
|
+
<input type="text" name="username" placeholder="Username" required>
|
286
|
+
<input type="password" name="password" placeholder="Password" required>
|
287
|
+
<button type="submit">Login</button>
|
288
|
+
</form>
|
289
|
+
</div>
|
290
|
+
</body>
|
291
|
+
</html>
|
292
|
+
HTML,
|
293
|
+
capture_credentials: true,
|
294
|
+
capture_passwords: true,
|
295
|
+
redirect_url: "https://www.realbank.com/login"
|
296
|
+
)
|
297
|
+
|
298
|
+
# Save both pages
|
299
|
+
[basic_page, login_page].each do |page|
|
300
|
+
if page.save
|
301
|
+
puts "✓ Created page: #{page.name} (ID: #{page.id})"
|
302
|
+
puts " Captures credentials: #{page.captures_credentials?}"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
```
|
306
|
+
|
307
|
+
#### Importing Pages from Existing Websites
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
# Import a real website as a landing page template
|
311
|
+
begin
|
312
|
+
imported_data = Gophish::Page.import_site(
|
313
|
+
"https://login.live.com",
|
314
|
+
include_resources: true # Include CSS, JS, and images
|
315
|
+
)
|
316
|
+
|
317
|
+
page = Gophish::Page.new(imported_data)
|
318
|
+
page.name = "Microsoft Live Login Clone"
|
319
|
+
page.capture_credentials = true
|
320
|
+
|
321
|
+
if page.save
|
322
|
+
puts "✓ Successfully imported website as landing page"
|
323
|
+
puts " Page ID: #{page.id}"
|
324
|
+
puts " HTML size: #{page.html.length} characters"
|
325
|
+
end
|
326
|
+
|
327
|
+
rescue StandardError => e
|
328
|
+
puts "✗ Failed to import website: #{e.message}"
|
329
|
+
puts " Falling back to manual page creation"
|
330
|
+
|
331
|
+
# Create a manual fallback page
|
332
|
+
fallback_page = Gophish::Page.new(
|
333
|
+
name: "Manual Microsoft Login Clone",
|
334
|
+
html: "<html><body><h1>Microsoft</h1><form method='post'><input name='email' type='email' placeholder='Email'><input name='password' type='password' placeholder='Password'><button type='submit'>Sign in</button></form></body></html>",
|
335
|
+
capture_credentials: true
|
336
|
+
)
|
337
|
+
fallback_page.save
|
338
|
+
end
|
339
|
+
```
|
340
|
+
|
341
|
+
#### Managing Existing Pages
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
# List all pages
|
345
|
+
puts "Existing pages:"
|
346
|
+
Gophish::Page.all.each do |page|
|
347
|
+
credential_info = page.captures_credentials? ? " [Captures Credentials]" : ""
|
348
|
+
redirect_info = page.has_redirect? ? " → #{page.redirect_url}" : ""
|
349
|
+
puts " #{page.id}: #{page.name}#{credential_info}#{redirect_info}"
|
350
|
+
end
|
351
|
+
|
352
|
+
# Update a page
|
353
|
+
page = Gophish::Page.find(1)
|
354
|
+
page.name = "Updated Page Name"
|
355
|
+
page.capture_credentials = true
|
356
|
+
page.redirect_url = "https://example.com/success"
|
357
|
+
|
358
|
+
# Modify the HTML content
|
359
|
+
page.html = page.html.gsub("Sign in", "Login")
|
360
|
+
|
361
|
+
if page.save
|
362
|
+
puts "✓ Page updated successfully"
|
363
|
+
puts " Now captures credentials: #{page.captures_credentials?}"
|
364
|
+
end
|
365
|
+
```
|
366
|
+
|
128
367
|
### Managing Existing Groups
|
129
368
|
|
130
369
|
```ruby
|
data/lib/gophish/base.rb
CHANGED
@@ -13,12 +13,13 @@ module Gophish
|
|
13
13
|
include ActiveModel::Model
|
14
14
|
include ActiveModel::Attributes
|
15
15
|
include ActiveModel::Validations
|
16
|
+
include ActiveModel::Dirty
|
16
17
|
include ActiveRecord::Callbacks
|
17
18
|
|
18
19
|
def initialize(attributes = {})
|
19
20
|
@persisted = false
|
20
|
-
@changed_attributes = {}
|
21
21
|
super(attributes)
|
22
|
+
clear_changes_information
|
22
23
|
end
|
23
24
|
|
24
25
|
def self.configuration
|
@@ -125,7 +126,7 @@ module Gophish
|
|
125
126
|
send "#{key}=", value if respond_to? "#{key}="
|
126
127
|
end
|
127
128
|
@persisted = true
|
128
|
-
|
129
|
+
clear_changes_information
|
129
130
|
end
|
130
131
|
|
131
132
|
def handle_error_response(response)
|
@@ -145,9 +146,9 @@ module Gophish
|
|
145
146
|
|
146
147
|
def update_record
|
147
148
|
return false if id.nil?
|
148
|
-
return true
|
149
|
+
return true unless changed?
|
149
150
|
|
150
|
-
response = self.class.put "#{self.class.resource_path}/#{id}
|
151
|
+
response = self.class.put "#{self.class.resource_path}/#{id}", request_options(body_for_update)
|
151
152
|
return handle_error_response response unless response.success?
|
152
153
|
|
153
154
|
update_attributes_from_response response.parsed_response
|
@@ -177,29 +178,10 @@ module Gophish
|
|
177
178
|
end
|
178
179
|
|
179
180
|
def body_for_update
|
180
|
-
body_for_create
|
181
|
-
end
|
182
|
-
|
183
|
-
def attribute_changed?(attribute)
|
184
|
-
@changed_attributes.key? attribute.to_s
|
185
|
-
end
|
186
|
-
|
187
|
-
def changed_attributes
|
188
|
-
@changed_attributes.keys
|
189
|
-
end
|
190
|
-
|
191
|
-
def attribute_was(attribute)
|
192
|
-
@changed_attributes[attribute.to_s]
|
181
|
+
body_for_create.merge id:
|
193
182
|
end
|
194
183
|
|
195
184
|
def []=(attribute, value)
|
196
|
-
attribute_str = attribute.to_s
|
197
|
-
current_value = send attribute if respond_to? attribute
|
198
|
-
|
199
|
-
unless current_value == value
|
200
|
-
@changed_attributes[attribute_str] = current_value
|
201
|
-
end
|
202
|
-
|
203
185
|
send "#{attribute}=", value if respond_to? "#{attribute}="
|
204
186
|
end
|
205
187
|
end
|
data/lib/gophish/group.rb
CHANGED
data/lib/gophish/page.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
module Gophish
|
5
|
+
class Page < Base
|
6
|
+
attribute :id, :integer
|
7
|
+
attribute :name, :string
|
8
|
+
attribute :html, :string
|
9
|
+
attribute :capture_credentials, :boolean, default: false
|
10
|
+
attribute :capture_passwords, :boolean, default: false
|
11
|
+
attribute :modified_date, :string
|
12
|
+
attribute :redirect_url, :string
|
13
|
+
|
14
|
+
define_attribute_methods :id, :name, :html, :capture_credentials, :capture_passwords, :modified_date, :redirect_url
|
15
|
+
|
16
|
+
validates :name, presence: true
|
17
|
+
validates :html, presence: true
|
18
|
+
|
19
|
+
def body_for_create
|
20
|
+
{
|
21
|
+
name:,
|
22
|
+
html:,
|
23
|
+
capture_credentials:,
|
24
|
+
capture_passwords:,
|
25
|
+
redirect_url:
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.import_site(url, include_resources: false)
|
30
|
+
options = build_import_options url, include_resources
|
31
|
+
response = post '/import/site', options
|
32
|
+
raise StandardError, 'Failed to import site' unless response.success?
|
33
|
+
|
34
|
+
response.parsed_response
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.build_import_options(url, include_resources)
|
38
|
+
{
|
39
|
+
body: { url:, include_resources: }.to_json,
|
40
|
+
headers: { 'Content-Type' => 'application/json' }
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def captures_credentials?
|
45
|
+
capture_credentials == true
|
46
|
+
end
|
47
|
+
|
48
|
+
def captures_passwords?
|
49
|
+
capture_passwords == true
|
50
|
+
end
|
51
|
+
|
52
|
+
def has_redirect?
|
53
|
+
!redirect_url.blank?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Gophish
|
6
|
+
class Template < Base
|
7
|
+
attribute :id, :integer
|
8
|
+
attribute :name, :string
|
9
|
+
attribute :subject, :string
|
10
|
+
attribute :text, :string
|
11
|
+
attribute :html, :string
|
12
|
+
attribute :modified_date, :string
|
13
|
+
attribute :attachments, default: -> { [] }
|
14
|
+
|
15
|
+
define_attribute_methods :id, :name, :subject, :text, :html, :modified_date, :attachments
|
16
|
+
|
17
|
+
validates :name, presence: true
|
18
|
+
validate :validate_content_presence
|
19
|
+
validate :validate_attachments_structure
|
20
|
+
|
21
|
+
def body_for_create
|
22
|
+
{ name:, subject:, text:, html:, attachments: }
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.import_email(content, convert_links: false)
|
26
|
+
options = build_import_options content, convert_links
|
27
|
+
response = post '/import/email', options
|
28
|
+
raise StandardError, 'Failed to import email' unless response.success?
|
29
|
+
|
30
|
+
response.parsed_response
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.build_import_options(content, convert_links)
|
34
|
+
{
|
35
|
+
body: { content:, convert_links: }.to_json,
|
36
|
+
headers: { 'Content-Type' => 'application/json' }
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_attachment(content, type, name)
|
41
|
+
encoded_content = encode_content content
|
42
|
+
attachments << { content: encoded_content, type:, name: }
|
43
|
+
attachments_will_change!
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_attachment(name)
|
47
|
+
original_size = attachments.size
|
48
|
+
attachments.reject! { |attachment| attachment[:name] == name || attachment['name'] == name }
|
49
|
+
attachments_will_change! if attachments.size != original_size
|
50
|
+
end
|
51
|
+
|
52
|
+
def has_attachments?
|
53
|
+
!attachments.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def attachment_count
|
57
|
+
attachments.length
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def encode_content(content)
|
63
|
+
content.is_a?(String) ? Base64.strict_encode64(content) : content
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_content_presence
|
67
|
+
return unless text.blank? && html.blank?
|
68
|
+
|
69
|
+
errors.add :base, 'Need to specify at least plaintext or HTML content'
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_attachments_structure
|
73
|
+
return if attachments.blank?
|
74
|
+
return errors.add :attachments, 'must be an array' unless attachments.is_a? Array
|
75
|
+
|
76
|
+
attachments.each_with_index { |attachment, index| validate_attachment attachment, index }
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_attachment(attachment, index)
|
80
|
+
return errors.add :attachments, "item at index #{index} must be a hash" unless attachment.is_a? Hash
|
81
|
+
|
82
|
+
validate_attachment_field attachment, index, :content
|
83
|
+
validate_attachment_field attachment, index, :type
|
84
|
+
validate_attachment_field attachment, index, :name
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_attachment_field(attachment, index, field)
|
88
|
+
value = attachment[field] || attachment[field.to_s]
|
89
|
+
return unless value.blank?
|
90
|
+
|
91
|
+
errors.add :attachments, "item at index #{index} must have a #{field}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/gophish/version.rb
CHANGED
data/lib/gophish-ruby.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gophish-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eli Sebastian Herrera Aguilar
|
@@ -105,6 +105,8 @@ files:
|
|
105
105
|
- lib/gophish/base.rb
|
106
106
|
- lib/gophish/configuration.rb
|
107
107
|
- lib/gophish/group.rb
|
108
|
+
- lib/gophish/page.rb
|
109
|
+
- lib/gophish/template.rb
|
108
110
|
- lib/gophish/version.rb
|
109
111
|
- sig/gophish/ruby.rbs
|
110
112
|
homepage: https://github.com/EliSebastian/gopish-ruby
|