phisher_phinder 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +2 -1
  3. data/.gitignore +3 -0
  4. data/Gemfile +0 -11
  5. data/Gemfile.lock +45 -13
  6. data/README.md +108 -2
  7. data/exe/phisher_phinder +61 -0
  8. data/lib/phisher_phinder.rb +11 -2
  9. data/lib/phisher_phinder/command.rb +20 -0
  10. data/lib/phisher_phinder/display.rb +64 -0
  11. data/lib/phisher_phinder/extended_ip.rb +4 -0
  12. data/lib/phisher_phinder/extended_ip_factory.rb +4 -2
  13. data/lib/phisher_phinder/geoip_ip_data.rb +9 -2
  14. data/lib/phisher_phinder/mail.rb +10 -3
  15. data/lib/phisher_phinder/mail_parser.rb +43 -30
  16. data/lib/phisher_phinder/mail_parser/authentication_headers/auth_results_parser.rb +150 -0
  17. data/lib/phisher_phinder/mail_parser/authentication_headers/parser.rb +25 -0
  18. data/lib/phisher_phinder/mail_parser/authentication_headers/received_spf_parser.rb +222 -0
  19. data/lib/phisher_phinder/mail_parser/body/block_classifier.rb +106 -0
  20. data/lib/phisher_phinder/mail_parser/body/block_parser.rb +37 -0
  21. data/lib/phisher_phinder/mail_parser/body_parser.rb +26 -31
  22. data/lib/phisher_phinder/mail_parser/header_value_parser.rb +25 -10
  23. data/lib/phisher_phinder/mail_parser/received_headers/by_parser.rb +35 -5
  24. data/lib/phisher_phinder/mail_parser/received_headers/for_parser.rb +25 -5
  25. data/lib/phisher_phinder/mail_parser/received_headers/from_parser.rb +50 -6
  26. data/lib/phisher_phinder/mail_parser/received_headers/parser.rb +50 -29
  27. data/lib/phisher_phinder/mail_parser/received_headers/starttls_parser.rb +8 -1
  28. data/lib/phisher_phinder/null_lookup_client.rb +9 -0
  29. data/lib/phisher_phinder/null_response.rb +12 -0
  30. data/lib/phisher_phinder/sender_extractor.rb +74 -0
  31. data/lib/phisher_phinder/simple_ip.rb +4 -0
  32. data/lib/phisher_phinder/tracing_report.rb +47 -0
  33. data/lib/phisher_phinder/version.rb +1 -1
  34. data/phisher_phinder.gemspec +15 -1
  35. metadata +208 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee817dd4cbc016b47f7710714c4be4c5b07a81baa89b47d46c00ae9112fc04e0
4
- data.tar.gz: 7fb874d90fbb94f310ccec195ee6901f77c193f92615cdb5d9c8f5764dfaac94
3
+ metadata.gz: c8063750bef71fc06792e7c10440f99e979891612540b06abf4c66f2f368022d
4
+ data.tar.gz: a94cd0e2438359bc626609216a91633deb589121ac92a2567c2a90625c0e30ae
5
5
  SHA512:
6
- metadata.gz: c4d8660b672488b32ce4cb4ba1e3b7652e414b54afff9d2817a8461d570f7db5aac391424287953a4653ef3b00513dfcffa6666dc740aea823231bc4d6b5f685
7
- data.tar.gz: 9e35efd983eb5961fac93c05036bf09e59919ecd1ce6ecd1c338d026e5c3489276f69edd135d2d81e47870daf3422df5ef07f8de570c3d5dfa0f14111c6cdce6
6
+ metadata.gz: 8cfc5618456ab2db9304232da36e6a5ae3e989edd1ba0993f5bd1f07feb27cfb4d1921052176630e002aa7847f0faee646f5f6690cdd650c34fd64ed843d0188
7
+ data.tar.gz: bb5ef0c6f6f601a8c7d42ec2a7a8c8de976ccc9a86e438c56cedf0e4853908edb9efb3dfecfae515d300e0f2671cf321c3eaebe0105dad874a52e9be2fff7050
@@ -1,3 +1,4 @@
1
- DATABASE_URL=sqlite3://example.sqlite3
1
+ DATABASE_URL=sqlite://example.sqlite3
2
2
  MAXMIND_USER_ID=12345
3
3
  MAXMIND_LICENCE_KEY=abcdef
4
+ LINE_ENDING_TYPE=dos # or unix
data/.gitignore CHANGED
@@ -10,9 +10,12 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
 
13
+ *.gem
14
+
13
15
  *.sqlite3
14
16
 
15
17
  *.env
16
18
  *.env.test
17
19
 
18
20
  test_files
21
+ icebox
data/Gemfile CHANGED
@@ -1,14 +1,3 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
-
5
- gem 'dotenv', '~> 2.7.5'
6
- gem 'maxmind-geoip2', '~> 0.4.0'
7
- gem 'nokogiri', '~> 1.10.10'
8
- gem 'sequel', '~> 5.33'
9
- gem 'sqlite3', '~> 1.4.2'
10
-
11
- gem "rake", "~> 12.0"
12
- gem "rspec", "~> 3.0"
13
- gem 'database_cleaner-sequel', '1.8.0'
14
- gem 'webmock', '~> 3.8.3'
@@ -1,14 +1,32 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- phisher_phinder (0.1.0)
4
+ phisher_phinder (0.2.0)
5
+ dotenv (~> 2.7.5)
6
+ maxmind-geoip2 (~> 0.4.0)
7
+ nokogiri (~> 1.11.0)
8
+ sequel (~> 5.33)
9
+ sqlite3 (~> 1.4.2)
10
+ terminal-table (~> 2.0.0)
11
+ whois (~> 5.0.1)
12
+ whois-parser (~> 1.2.0)
5
13
 
6
14
  GEM
7
15
  remote: https://rubygems.org/
8
16
  specs:
17
+ activesupport (6.1.0)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 1.6, < 2)
20
+ minitest (>= 5.1)
21
+ tzinfo (~> 2.0)
22
+ zeitwerk (~> 2.3)
9
23
  addressable (2.7.0)
10
24
  public_suffix (>= 2.0.2, < 5.0)
25
+ bundler-audit (0.7.0.1)
26
+ bundler (>= 1.2.0, < 3)
27
+ thor (>= 0.18, < 2)
11
28
  coderay (1.1.2)
29
+ concurrent-ruby (1.1.7)
12
30
  connection_pool (2.2.3)
13
31
  crack (0.4.3)
14
32
  safe_yaml (~> 1.0.0)
@@ -19,8 +37,8 @@ GEM
19
37
  diff-lcs (1.3)
20
38
  domain_name (0.5.20190701)
21
39
  unf (>= 0.0.5, < 1.0.0)
22
- dotenv (2.7.5)
23
- ffi (1.13.1)
40
+ dotenv (2.7.6)
41
+ ffi (1.14.2)
24
42
  ffi-compiler (1.0.1)
25
43
  ffi (>= 1.0.0)
26
44
  rake
@@ -33,21 +51,26 @@ GEM
33
51
  http-cookie (1.0.3)
34
52
  domain_name (~> 0.5)
35
53
  http-form_data (2.3.0)
36
- http-parser (1.2.1)
37
- ffi-compiler (>= 1.0, < 2.0)
54
+ http-parser (1.2.2)
55
+ ffi-compiler
56
+ i18n (1.8.7)
57
+ concurrent-ruby (~> 1.0)
38
58
  maxmind-db (1.1.1)
39
59
  maxmind-geoip2 (0.4.0)
40
60
  connection_pool (~> 2.2)
41
61
  http (~> 4.3)
42
62
  maxmind-db (~> 1.1)
43
63
  method_source (1.0.0)
44
- mini_portile2 (2.4.0)
45
- nokogiri (1.10.10)
46
- mini_portile2 (~> 2.4.0)
64
+ mini_portile2 (2.5.0)
65
+ minitest (5.14.2)
66
+ nokogiri (1.11.0)
67
+ mini_portile2 (~> 2.5.0)
68
+ racc (~> 1.4)
47
69
  pry (0.13.1)
48
70
  coderay (~> 1.1)
49
71
  method_source (~> 1.0)
50
72
  public_suffix (4.0.5)
73
+ racc (1.5.2)
51
74
  rake (12.3.3)
52
75
  rspec (3.9.0)
53
76
  rspec-core (~> 3.9.0)
@@ -65,28 +88,37 @@ GEM
65
88
  safe_yaml (1.0.5)
66
89
  sequel (5.33.0)
67
90
  sqlite3 (1.4.2)
91
+ terminal-table (2.0.0)
92
+ unicode-display_width (~> 1.1, >= 1.1.1)
93
+ thor (1.0.1)
94
+ timecop (0.9.2)
95
+ tzinfo (2.0.4)
96
+ concurrent-ruby (~> 1.0)
68
97
  unf (0.1.4)
69
98
  unf_ext
70
99
  unf_ext (0.0.7.7)
100
+ unicode-display_width (1.7.0)
71
101
  webmock (3.8.3)
72
102
  addressable (>= 2.3.6)
73
103
  crack (>= 0.3.2)
74
104
  hashdiff (>= 0.4.0, < 2.0.0)
105
+ whois (5.0.1)
106
+ whois-parser (1.2.0)
107
+ activesupport (>= 4)
108
+ whois (>= 4.0.7)
109
+ zeitwerk (2.4.2)
75
110
 
76
111
  PLATFORMS
77
112
  ruby
78
113
 
79
114
  DEPENDENCIES
115
+ bundler-audit (~> 0.7.0.1)
80
116
  database_cleaner-sequel (= 1.8.0)
81
- dotenv (~> 2.7.5)
82
- maxmind-geoip2 (~> 0.4.0)
83
- nokogiri (~> 1.10.10)
84
117
  phisher_phinder!
85
118
  pry
86
119
  rake (~> 12.0)
87
120
  rspec (~> 3.0)
88
- sequel (~> 5.33)
89
- sqlite3 (~> 1.4.2)
121
+ timecop (~> 0.9.2)
90
122
  webmock (~> 3.8.3)
91
123
 
92
124
  BUNDLED WITH
data/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # PhisherPhinder
2
2
 
3
- A simple gem to explore phishing emails
3
+ PhisherPhinder is a small utility that extracts data from an email that can be useful when trying to determine the
4
+ source of a phishing email.
5
+
6
+ As of version 0.2.0 this functionality is very limited. There is a good chance that you will encounter sharp edges - if
7
+ you do, feel free to file a GH issue and I will try to fix this.
8
+
9
+ ## Caution
10
+
11
+ **When downloading an email for parsing, DO NOT CLICK on any links contained within the email. Rather download the
12
+ message source as text. As an example, here are the instructions for doing it using
13
+ [GMail](https://www.lifewire.com/how-to-view-the-source-of-a-message-in-gmail-1172105 'Gmail Download Instructions')**
4
14
 
5
15
  ## Installation
6
16
 
17
+ Note: Currently, Windows support is not guaranteed. If you would like to test it on Windows and tell me what does not
18
+ work, I will try my best to address these issues.
19
+
7
20
  Add this line to your application's Gemfile:
8
21
 
9
22
  ```ruby
@@ -20,9 +33,102 @@ Or install it yourself as:
20
33
 
21
34
  ## Usage
22
35
 
23
- TODO: Write usage instructions here
36
+ At it's most simple - download the email as text to a location on th emahcine running PhihserPhinder
37
+ (`/path/to/content.eml`), then:
38
+
39
+ ```ruby
40
+ phisher_phinder /path/to/content.eml
41
+ ```
42
+
43
+ By default, PhisherPhinder will assume that the file uses the dos-style line endings (`\r\n`), but you can specify the
44
+ line ending type:
45
+
46
+ ```bash
47
+ phisher_phinder -l unix /path/to/content.eml # unix line endings \n
48
+ phisher_phinder -l dos /path/to/content.eml # windows line endings \r\n
49
+ ```
50
+
51
+ PhisherPhinder can lookup IPV4 address data from the MaxMind GeoIP service which requires a Maxmind GeoIP account.
52
+ Lookup results are cached locally in a database (SQLITE will suffice). The URL for the database needs to be provided as
53
+ an environment variable:
54
+
55
+ ```bash
56
+ DATABASE_URL=sqlite://development.sqlite3 phisher_phinder -a foo -k bar -g /path/to/content.eml
57
+ ```
58
+
59
+ Note: As of version 0.2.0, the GeoIP data is not used for much and the functionality may be removed in future releases.
60
+
61
+ To see help instructions for the CLI usage:
62
+
63
+ ```bash
64
+ phisher_phinder -h
65
+ ```
66
+
67
+ ```
68
+ Usage: phisher_phinder [options] /path/to/email/contents
69
+ -a, --account_id ACCOUNT_ID GeoIP account id
70
+ -k, --license_key LICENSE_KEY GeoIP license key
71
+ -g, --geoip Enable lookup of GeoIP data for IP addresses (requires `DATABASE_URL` env variable to be defined)
72
+ -l, --line-ending TYPE Select line ending type for file
73
+ -h, --help Prints help text
74
+
75
+ ```
76
+
77
+ ## Output
78
+
79
+ The output of PhisherPhinder will produce something similar to the below:
80
+
81
+ ```
82
+ +-------------+---------------------------------------------+
83
+ | Origin |
84
+ +-------------+---------------------------------------------+
85
+ | From | "Email Security Gateway" <test@test.zzz> |
86
+ | Message ID | <ed1770$bbsq7@test.zzz> |
87
+ | Return Path | <test@test.zzz> |
88
+ +-------------+---------------------------------------------+
89
+
90
+
91
+ +-----------+---------------+------------------+
92
+ | SPF |
93
+ +-----------+---------------+------------------+
94
+ | SPF Pass? | Sender Host | From Address |
95
+ +-----------+---------------+------------------+
96
+ | Yes | 10.0.0.1 | test@test.zzz |
97
+ +-----------+---------------+------------------+
98
+
99
+
100
+ +---------------+------------------------------+------------------------------+--------------------------+
101
+ | Trace |
102
+ +---------------+------------------------------+------------------------------+--------------------------+
103
+ | Sender IP | Sender Host | Advertised Sender | Recipient |
104
+ +---------------+------------------------------+------------------------------+--------------------------+
105
+ | 10.0.0.1 | host1.test.zzz | dodgyname.test.zzz | mx.google.com |
106
+ | 10.0.0.2 | | othersdodgyname.text.zzz | host1.test.zzz |
107
+ +---------------+------------------------------+------------------------------+--------------------------+
108
+
109
+ ```
110
+
111
+ The `Origin` section contains data relating to the email origin.
112
+
113
+ With regard to the `SPF` and `Trace` sections, they are based on the assumption that the most recent SPF details
114
+ provided in the headers can be trusted as they have been provided by the host of the recipient email and can, hopefully,
115
+ be trusted.
116
+
117
+ The `Trace` secion shows a subset of the `Received` headers from the original (advertised, but not necessarily actual)
118
+ origin (the last entry in the table) to the last external server to process the email before the recipient's mail host
119
+ received the email.
120
+
121
+ ## Dependencies
122
+ 1. [Maxmind GeoIP2 User Account](https://dev.maxmind.com/geoip/geoip2/web-services/) - pay as you go
123
+ 2. [Database Cleaner](https://github.com/DatabaseCleaner/database_cleaner#safeguards) - whitelist for database urls
24
124
 
25
125
  ## Development
126
+ .env.example contains dependent environment variables for the .env.test file. You will need to change: DATABASE_URL=sqlite://test.sqlite3.
127
+ Prior to running the specs, run the 0001 migration to create the geo_ip_cache table.
128
+
129
+ ```
130
+ # bundle exec rake db:migrate
131
+ ```
26
132
 
27
133
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
134
 
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'phisher_phinder'
5
+
6
+ options = {
7
+ line_ending: "\r\n",
8
+ geoip_lookup: false,
9
+ geoip_settings: {
10
+ account_id: nil,
11
+ license_key: nil
12
+ }
13
+ }
14
+
15
+ OptionParser.new do |opts|
16
+ opts.banner = 'Usage: phisher_phinder [options] /path/to/email/contents'
17
+ line_endings = {
18
+ 'windows' => "\r\n",
19
+ 'dos' => "\r\n",
20
+ 'unix' => "\n",
21
+ }
22
+
23
+ def geoip_credentials?(opts)
24
+ opts[:geoip_settings][:account_id] && opts[:geoip_settings][:license_key]
25
+ end
26
+
27
+ opts.on('-a ACCOUNT_ID', '--account_id ACCOUNT_ID', 'GeoIP account id') do |account_id|
28
+ options[:geoip_settings][:account_id] = account_id
29
+ end
30
+
31
+ opts.on('-k LICENSE_KEY', '--license_key LICENSE_KEY', 'GeoIP license key') do |license_key|
32
+ options[:geoip_settings][:license_key] = license_key
33
+ end
34
+
35
+ opts.on(
36
+ '-g',
37
+ '--geoip',
38
+ 'Enable lookup of GeoIP data for IP addresses (requires `DATABASE_URL` env variable to be defined)'
39
+ ) do |geoip|
40
+ raise 'Please provide the GeoIP account id and license key' unless geoip_credentials?(options)
41
+ raise 'Please set the DATABASE_URL ENV variable' unless ENV['DATABASE_URL']
42
+ options[:geoip_lookup] = geoip
43
+ end
44
+
45
+ opts.on('-l TYPE', '--line-ending TYPE', line_endings, 'Select line ending type for file') do |ending|
46
+ options[:line_ending] = ending
47
+ end
48
+
49
+ opts.on('-h', '--help', 'Prints help text') do
50
+ puts opts
51
+ exit
52
+ end
53
+ end.parse!
54
+
55
+ file_contents = IO.read(ARGV.last)
56
+
57
+ command = PhisherPhinder::Command.new
58
+
59
+ PhisherPhinder::Display.new.display_report(
60
+ command.report(file_contents, **options)
61
+ )
@@ -2,11 +2,13 @@ require "phisher_phinder/version"
2
2
 
3
3
  require 'maxmind/geoip2'
4
4
 
5
- require 'sequel'
6
- Sequel::Model.plugin :timestamps, update_on_create: true
5
+ require_relative './phisher_phinder/command'
6
+ require_relative './phisher_phinder/display'
7
7
 
8
8
  require_relative './phisher_phinder/body_hyperlink'
9
9
  require_relative './phisher_phinder/cached_geoip_client'
10
+ require_relative './phisher_phinder/null_lookup_client'
11
+ require_relative './phisher_phinder/null_response'
10
12
  require_relative './phisher_phinder/geoip_ip_data'
11
13
  require_relative './phisher_phinder/expanded_data_processor'
12
14
  require_relative './phisher_phinder/extended_ip'
@@ -14,6 +16,11 @@ require_relative './phisher_phinder/extended_ip_factory'
14
16
  require_relative './phisher_phinder/mail_parser'
15
17
  require_relative './phisher_phinder/mail'
16
18
  require_relative './phisher_phinder/simple_ip'
19
+ require_relative './phisher_phinder/mail_parser/authentication_headers/parser'
20
+ require_relative './phisher_phinder/mail_parser/authentication_headers/auth_results_parser'
21
+ require_relative './phisher_phinder/mail_parser/authentication_headers/received_spf_parser'
22
+ require_relative './phisher_phinder/mail_parser/body/block_classifier'
23
+ require_relative './phisher_phinder/mail_parser/body/block_parser'
17
24
  require_relative './phisher_phinder/mail_parser/received_headers/parser'
18
25
  require_relative './phisher_phinder/mail_parser/received_headers/by_parser'
19
26
  require_relative './phisher_phinder/mail_parser/received_headers/classifier'
@@ -21,6 +28,8 @@ require_relative './phisher_phinder/mail_parser/received_headers/for_parser'
21
28
  require_relative './phisher_phinder/mail_parser/received_headers/from_parser'
22
29
  require_relative './phisher_phinder/mail_parser/received_headers/starttls_parser'
23
30
  require_relative './phisher_phinder/mail_parser/received_headers/timestamp_parser'
31
+ require_relative './phisher_phinder/sender_extractor'
32
+ require_relative './phisher_phinder/tracing_report'
24
33
 
25
34
  module PhisherPhinder
26
35
  class Error < StandardError; end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ class Command
5
+ def report(contents, line_ending:, geoip_lookup:, geoip_settings: {})
6
+ lookup_client = if geoip_lookup
7
+ PhisherPhinder::CachedGeoipClient.new(
8
+ MaxMind::GeoIP2::Client.new(**geoip_settings),
9
+ Time.now - 86400
10
+ )
11
+ else
12
+ PhisherPhinder::NullLookupClient.new
13
+ end
14
+ ip_factory = PhisherPhinder::ExtendedIpFactory.new(geoip_client: lookup_client)
15
+ mail_parser = PhisherPhinder::MailParser::Parser.new(ip_factory, line_ending)
16
+ tracing_report = PhisherPhinder::TracingReport.new(mail_parser.parse(contents))
17
+ tracing_report.report
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require 'terminal-table'
3
+
4
+ module PhisherPhinder
5
+ class Display
6
+ def display_report(input_data)
7
+ origin_table = Terminal::Table.new(
8
+ title: 'Origin',
9
+ rows: format_origin_data(input_data)
10
+ )
11
+
12
+ puts origin_table
13
+
14
+ puts "\n\n"
15
+
16
+ spf_table = Terminal::Table.new(
17
+ headings: ['SPF Pass?', 'Sender Host', 'From Address'],
18
+ title: 'SPF',
19
+ rows: [
20
+ [
21
+ input_data[:authentication][:spf][:success] ? 'Yes' : 'No',
22
+ input_data[:authentication][:spf][:ip],
23
+ input_data[:authentication][:spf][:from_address]
24
+ ]
25
+ ]
26
+ )
27
+
28
+ puts spf_table
29
+
30
+ puts "\n\n"
31
+
32
+ data = input_data[:tracing].map do |entry|
33
+ [
34
+ entry[:sender][:ip],
35
+ entry[:sender][:host],
36
+ entry[:advertised_sender] || entry[:helo],
37
+ entry[:recipient]
38
+ ]
39
+ end
40
+
41
+ trace_table = Terminal::Table.new(
42
+ headings: ['Sender IP', 'Sender Host', 'Advertised Sender', 'Recipient'],
43
+ title: 'Trace',
44
+ rows: data
45
+ )
46
+
47
+ puts trace_table
48
+ end
49
+
50
+ private
51
+
52
+ def format_origin_data(input_data)
53
+ types = [
54
+ ['From', :from],
55
+ ['Message ID', :message_id],
56
+ ['Return Path', :return_path],
57
+ ]
58
+
59
+ types.inject([]) do |output, (description, type)|
60
+ output << [description, input_data[:origin][type].join(', ')]
61
+ end
62
+ end
63
+ end
64
+ end