pwn 0.5.199 → 0.5.200

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.
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+
6
+ module PWN
7
+ module Plugins
8
+ # This plugin is used for interacting w/ Hunter's REST API using
9
+ # the 'rest' browser type of PWN::Plugins::TransparentBrowser.
10
+ # This is based on the following Hunter API Specification:
11
+ # https://hunter.how/search-api
12
+ module Hunter
13
+ @@logger = PWN::Plugins::PWNLogger.create
14
+
15
+ # Supported Method Parameters::
16
+ # hunter_rest_call(
17
+ # http_method: 'optional HTTP method (defaults to GET)
18
+ # rest_call: 'required rest call to make per the schema',
19
+ # params: 'optional params passed in the URI or HTTP Headers',
20
+ # http_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST'
21
+ # )
22
+
23
+ private_class_method def self.hunter_rest_call(opts = {})
24
+ hunter_obj = opts[:hunter_obj]
25
+ http_method = if opts[:http_method].nil?
26
+ :get
27
+ else
28
+ opts[:http_method].to_s.scrub.to_sym
29
+ end
30
+ rest_call = opts[:rest_call].to_s.scrub
31
+ params = opts[:params]
32
+ http_body = opts[:http_body].to_s.scrub
33
+ base_hunter_api_uri = 'https://api.hunter.how'
34
+
35
+ browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
36
+ rest_client = browser_obj[:browser]::Request
37
+
38
+ case http_method
39
+ when :get
40
+ response = rest_client.execute(
41
+ method: :get,
42
+ url: "#{base_hunter_api_uri}/#{rest_call}",
43
+ headers: {
44
+ content_type: 'application/json; charset=UTF-8',
45
+ params: params
46
+ },
47
+ verify_ssl: false
48
+ )
49
+
50
+ when :post
51
+ response = rest_client.execute(
52
+ method: :post,
53
+ url: "#{base_hunter_api_uri}/#{rest_call}",
54
+ headers: {
55
+ content_type: 'application/json; charset=UTF-8',
56
+ params: params
57
+ },
58
+ payload: http_body,
59
+ verify_ssl: false
60
+ )
61
+
62
+ else
63
+ raise @@logger.error("Unsupported HTTP Method #{http_method} for #{self} Plugin")
64
+ end
65
+ JSON.parse(response.scrub, symbolize_names: true)
66
+ rescue JSON::ParserError => e
67
+ {
68
+ total: 0,
69
+ matches: [],
70
+ error: "JSON::ParserError #{e.message}",
71
+ rest_call: rest_call,
72
+ params: params
73
+ }
74
+ rescue RestClient::TooManyRequests
75
+ print 'Too many requests. Sleeping 10s...'
76
+ sleep 10
77
+ retry
78
+ rescue StandardError => e
79
+ case e.message
80
+ when '400 Bad Request', '404 Resource Not Found'
81
+ "#{e.message}: #{e.response}"
82
+ else
83
+ raise e
84
+ end
85
+ end
86
+
87
+ # Supported Method Parameters::
88
+ # search_results = PWN::Plugins::Hunter.search(
89
+ # api_key: 'required hunter api key',
90
+ # query: 'required - hunter search query',
91
+ # start_time: 'required - start date for the search (format is yyyy-mm-dd)',
92
+ # end_time: 'required - end date for the search (format is yyyy-mm-dd)',
93
+ # start_page: 'optional - starting page number for pagination (default is 1)',
94
+ # page_size: 'optional - number of results per page (default is 10)',
95
+ # fields: 'optional - comma-separated list of fields 'product,transport_protocol,protocol,banner,country,province,city,asn,org,web,updated_at' (default is nil)'
96
+ # )
97
+
98
+ public_class_method def self.search(opts = {})
99
+ api_key = opts[:api_key].to_s.scrub
100
+ raise "ERROR: #{self} requires a valid Hunter API Key" if api_key.empty?
101
+
102
+ query = opts[:query].to_s.scrub
103
+ raise "ERROR: #{self} requires a valid query" if query.empty?
104
+
105
+ start_time = opts[:start_time]
106
+ raise "ERROR: #{self} requires a valid start time" if start_time.nil?
107
+
108
+ end_time = opts[:end_time]
109
+ raise "ERROR: #{self} requires a valid end time" if end_time.nil?
110
+
111
+ start_page = opts[:start_page] ||= 1
112
+ page_size = opts[:page_size] ||= 10
113
+ fields = opts[:fields]
114
+
115
+ params = {}
116
+ params[:'api-key'] = api_key
117
+ base64_query = Base64.urlsafe_encode64(query)
118
+ params[:query] = base64_query
119
+ params[:page] = start_page
120
+ params[:page_size] = page_size
121
+ params[:start_time] = start_time
122
+ params[:end_time] = end_time
123
+ params[:fields] = fields
124
+
125
+ hunter_rest_call(
126
+ rest_call: 'search',
127
+ params: params
128
+ )
129
+ rescue StandardError => e
130
+ raise e
131
+ end
132
+
133
+ # Author(s):: 0day Inc. <support@0dayinc.com>
134
+
135
+ public_class_method def self.authors
136
+ "AUTHOR(S):
137
+ 0day Inc. <support@0dayinc.com>
138
+ "
139
+ end
140
+
141
+ # Display Usage for this Module
142
+
143
+ public_class_method def self.help
144
+ puts "USAGE:
145
+ search_results = #{self}.query(
146
+ api_key: 'required hunter api key',
147
+ query: 'required - hunter search query',
148
+ start_time: 'required - start date for the search (format is yyyy-mm-dd)',
149
+ end_time: 'required - end date for the search (format is yyyy-mm-dd)',
150
+ start_page: 'optional - starting page number for pagination (default is 1)',
151
+ page_size: 'optional - number of results per page (default is 10)',
152
+ fields: 'optional - comma-separated list of fields 'product,transport_protocol,protocol,banner,country,province,city,asn,org,web,updated_at' (default is nil)'
153
+ )
154
+
155
+ #{self}.authors
156
+ "
157
+ end
158
+ end
159
+ end
160
+ end
@@ -542,6 +542,9 @@ module PWN
542
542
  pi.config.pwn_irc = pi.config.p[:irc]
543
543
  Pry.config.pwn_irc = pi.config.pwn_irc
544
544
 
545
+ pi.config.pwn_hunter = pi.config.p[:hunter][:api_key]
546
+ Pry.config.pwn_hunter = pi.config.pwn_hunter
547
+
545
548
  pi.config.pwn_shodan = pi.config.p[:shodan][:api_key]
546
549
  Pry.config.pwn_shodan = pi.config.pwn_shodan
547
550
 
@@ -110,7 +110,7 @@ module PWN
110
110
  # DevTools ToolBox Settings in Firefox about:config
111
111
  this_profile['devtools.f12.enabled'] = true
112
112
  this_profile['devtools.toolbox.host'] = 'right'
113
- this_profile['devtools.toolbox.sidebar.width'] = 1400
113
+ this_profile['devtools.toolbox.sidebar.width'] = 1700
114
114
  this_profile['devtools.toolbox.splitconsoleHeight'] = 200
115
115
 
116
116
  # DevTools Debugger Settings in Firefox about:config
@@ -118,8 +118,7 @@ module PWN
118
118
  this_profile['devtools.debugger.start-panel-size'] = 200
119
119
  this_profile['devtools.debugger.end-panel-size'] = 200
120
120
  this_profile['devtools.debugger.auto-pretty-print'] = true
121
- # Re-enable once syntax highlighting is fixed
122
- # this_profile['devtools.debugger.ui.editor-wrapping'] = true
121
+ this_profile['devtools.debugger.ui.editor-wrapping'] = true
123
122
  this_profile['devtools.debugger.features.javascript-tracing'] = true
124
123
  this_profile['devtools.debugger.xhr-breakpoints-visible'] = true
125
124
  this_profile['devtools.debugger.expressions-visible'] = true
@@ -333,6 +332,14 @@ module PWN
333
332
  browser_obj[:devtools].send_cmd('DOMSnapshot.enable')
334
333
  end
335
334
 
335
+ firefox_browser_types = %i[firefox headless_firefox]
336
+ if firefox_browser_types.include?(browser_type)
337
+ # browser_obj[:devtools].send_cmd(
338
+ # 'EventBreakpoints.setInstrumentationBreakpoint',
339
+ # eventName: 'script'
340
+ # )
341
+ end
342
+
336
343
  # Future BiDi API that's more universally supported across browsers
337
344
  # browser_obj[:bidi] = driver.bidi
338
345
 
@@ -450,16 +457,22 @@ module PWN
450
457
 
451
458
  browser = browser_obj[:browser]
452
459
  case js
453
- when 'debugger', 'debugger;', 'debugger()', 'debugger();'
454
- Timeout.timeout(1) { console_resp = browser.execute_script('debugger') }
455
460
  when 'clear', 'clear;', 'clear()', 'clear();'
456
- console_resp = browser.execute_script('console.clear()')
461
+ script = 'console.clear()'
457
462
  else
458
- console_resp = browser.execute_script("console.log(#{js})")
463
+ script = "console.log(#{js})"
464
+ end
465
+
466
+ console_resp = nil
467
+ begin
468
+ Timeout.timeout(1) { console_resp = browser.execute_script(script) }
469
+ rescue Timeout::Error, Timeout::ExitException
470
+ console_resp
471
+ rescue Selenium::WebDriver::Error::JavascriptError
472
+ script = js
473
+ retry
459
474
  end
460
475
 
461
- console_resp
462
- rescue Timeout::Error, Timeout::ExitException
463
476
  console_resp
464
477
  rescue StandardError => e
465
478
  raise e
@@ -637,8 +650,10 @@ module PWN
637
650
 
638
651
  case action.to_s.downcase.to_sym
639
652
  when :pause
640
- console(browser_obj: browser_obj, js: 'debugger')
641
-
653
+ devtools.send_cmd(
654
+ 'EventBreakpoints.setInstrumentationBreakpoint',
655
+ eventName: 'scriptFirstStatement'
656
+ )
642
657
  # devtools.send_cmd('Debugger.enable')
643
658
  # devtools.send_cmd(
644
659
  # 'Debugger.setInstrumentationBreakpoint',
@@ -664,6 +679,10 @@ module PWN
664
679
  url
665
680
  end
666
681
  when :resume
682
+ devtools.send_cmd(
683
+ 'EventBreakpoints.removeInstrumentationBreakpoint',
684
+ eventName: 'scriptFirstStatement'
685
+ )
667
686
  devtools.send_cmd('Debugger.resume')
668
687
  else
669
688
  raise 'ERROR: action parameter must be :pause or :resume'
@@ -673,24 +692,69 @@ module PWN
673
692
  end
674
693
 
675
694
  # Supported Method Parameters::
676
- # PWN::Plugins::TransparentBrowser.step_into(
695
+ # current_dom = PWN::Plugins::TransparentBrowser.dom(
677
696
  # browser_obj: 'required - browser_obj returned from #open method)'
678
697
  # )
679
698
 
699
+ public_class_method def self.dom(opts = {})
700
+ browser_obj = opts[:browser_obj]
701
+ supported = %i[chrome headless_chrome]
702
+ verify_devtools_browser(browser_obj: browser_obj, supported: supported)
703
+
704
+ devtools = browser_obj[:devtools]
705
+ computed_styles = %i[display color font-size font-family]
706
+ devtools.send_cmd(
707
+ 'DOMSnapshot.captureSnapshot',
708
+ computedStyles: computed_styles
709
+ ).transform_keys(&:to_sym)
710
+ rescue StandardError => e
711
+ raise e
712
+ end
713
+
714
+ # Supported Method Parameters::
715
+ # PWN::Plugins::TransparentBrowser.step_into(
716
+ # browser_obj: 'required - browser_obj returned from #open method)',
717
+ # steps: 'optional - number of steps taken (Defaults to 1)'
718
+ # )
719
+
680
720
  public_class_method def self.step_into(opts = {})
681
721
  browser_obj = opts[:browser_obj]
682
722
  supported = %i[chrome headless_chrome]
683
723
  verify_devtools_browser(browser_obj: browser_obj, supported: supported)
684
724
 
725
+ steps = opts[:steps].to_i
726
+ steps = 1 if steps.zero? || steps.negative?
727
+
728
+ diff_arr = []
685
729
  devtools = browser_obj[:devtools]
686
- devtools.send_cmd('Debugger.stepInto')
730
+ steps.times do |s|
731
+ diff_hash = {}
732
+ step = s + 1
733
+ diff_hash[:step] = step
734
+
735
+ dom_before = dom(browser_obj: browser_obj)
736
+ diff_hash[:dom_before_step] = dom_before
737
+
738
+ devtools.send_cmd('Debugger.stepInto')
739
+
740
+ dom_after = dom(browser_obj: browser_obj)
741
+ diff_hash[:dom_after_step] = dom_after
742
+
743
+ da = dom_before.to_a - dom_after.to_a
744
+ diff_hash[:diff_dom] = da.to_h.transform_keys(&:to_sym)
745
+
746
+ diff_arr.push(diff_hash)
747
+ end
748
+
749
+ diff_arr
687
750
  rescue StandardError => e
688
751
  raise e
689
752
  end
690
753
 
691
754
  # Supported Method Parameters::
692
755
  # PWN::Plugins::TransparentBrowser.step_out(
693
- # browser_obj: 'required - browser_obj returned from #open method)'
756
+ # browser_obj: 'required - browser_obj returned from #open method)',
757
+ # steps: 'optional - number of steps taken (Defaults to 1)'
694
758
  # )
695
759
 
696
760
  public_class_method def self.step_out(opts = {})
@@ -698,15 +762,39 @@ module PWN
698
762
  supported = %i[chrome headless_chrome]
699
763
  verify_devtools_browser(browser_obj: browser_obj, supported: supported)
700
764
 
765
+ steps = opts[:steps].to_i
766
+ steps = 1 if steps.zero? || steps.negative?
767
+
768
+ diff_arr = []
701
769
  devtools = browser_obj[:devtools]
702
- devtools.send_cmd('Debugger.stepOut')
770
+ steps.times do |s|
771
+ diff_hash = {}
772
+ step = s + 1
773
+ diff_hash[:step] = step
774
+
775
+ dom_before = dom(browser_obj: browser_obj)
776
+ diff_hash[:pre_step] = dom_before
777
+
778
+ devtools.send_cmd('Debugger.stepOut')
779
+
780
+ dom_after = dom(browser_obj: browser_obj, step_sum: step_sum)
781
+ diff_hash[:post_step] = dom_after
782
+
783
+ da = dom_before.to_a - dom_after.to_a
784
+ diff_hash[:diff] = da.to_h.transform_keys(&:to_sym)
785
+
786
+ diff_arr.push(diff_hash)
787
+ end
788
+
789
+ diff_arr
703
790
  rescue StandardError => e
704
791
  raise e
705
792
  end
706
793
 
707
794
  # Supported Method Parameters::
708
795
  # PWN::Plugins::TransparentBrowser.step_over(
709
- # browser_obj: 'required - browser_obj returned from #open method)'
796
+ # browser_obj: 'required - browser_obj returned from #open method)',
797
+ # steps: 'optional - number of steps taken (Defaults to 1)'
710
798
  # )
711
799
 
712
800
  public_class_method def self.step_over(opts = {})
@@ -714,8 +802,31 @@ module PWN
714
802
  supported = %i[chrome headless_chrome]
715
803
  verify_devtools_browser(browser_obj: browser_obj, supported: supported)
716
804
 
805
+ steps = opts[:steps].to_i
806
+ steps = 1 if steps.zero? || steps.negative?
807
+
808
+ diff_arr = []
717
809
  devtools = browser_obj[:devtools]
718
- devtools.send_cmd('Debugger.stepOver')
810
+ steps.times do |s|
811
+ diff_hash = {}
812
+ step = s + 1
813
+ diff_hash[:step] = step
814
+
815
+ dom_before = dom(browser_obj: browser_obj)
816
+ diff_hash[:dom_before_step] = dom_before
817
+
818
+ devtools.send_cmd('Debugger.stepOver')
819
+
820
+ dom_after = dom(browser_obj: browser_obj, step_sum: step_sum)
821
+ diff_hash[:dom_after_step] = dom_after
822
+
823
+ da = dom_before.to_a - dom_after.to_a
824
+ diff_hash[:diff_dom] = da.to_h.transform_keys(&:to_sym)
825
+
826
+ diff_arr.push(diff_hash)
827
+ end
828
+
829
+ diff_arr
719
830
  rescue StandardError => e
720
831
  raise e
721
832
  end
@@ -986,16 +1097,23 @@ module PWN
986
1097
  url: 'optional - URL to navigate to after pausing debugger (Defaults to nil)'
987
1098
  )
988
1099
 
989
- #{self}.step_into(
1100
+ current_dom = #{self}.dom(
990
1101
  browser_obj: 'required - browser_obj returned from #open method)'
991
1102
  )
992
1103
 
1104
+ #{self}.step_into(
1105
+ browser_obj: 'required - browser_obj returned from #open method)',
1106
+ steps: 'optional - number of steps taken (Defaults to 1)'
1107
+ )
1108
+
993
1109
  #{self}.step_out(
994
- browser_obj: 'required - browser_obj returned from #open method)'
1110
+ browser_obj: 'required - browser_obj returned from #open method)',
1111
+ steps: 'optional - number of steps taken (Defaults to 1)'
995
1112
  )
996
1113
 
997
1114
  #{self}.step_over(
998
- browser_obj: 'required - browser_obj returned from #open method)'
1115
+ browser_obj: 'required - browser_obj returned from #open method)',
1116
+ steps: 'optional - number of steps taken (Defaults to 1)'
999
1117
  )
1000
1118
 
1001
1119
  #{self}.toggle_devtools(
data/lib/pwn/plugins.rb CHANGED
@@ -30,6 +30,7 @@ module PWN
30
30
  autoload :Github, 'pwn/plugins/github'
31
31
  autoload :GQRX, 'pwn/plugins/gqrx'
32
32
  autoload :HackerOne, 'pwn/plugins/hacker_one'
33
+ autoload :Hunter, 'pwn/plugins/hunter'
33
34
  autoload :IPInfo, 'pwn/plugins/ip_info'
34
35
  autoload :IRC, 'pwn/plugins/irc'
35
36
  autoload :Jenkins, 'pwn/plugins/jenkins'
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'socket'
4
+
5
+ module PWN
6
+ module SAST
7
+ # SAST Module used to identify any localStorage function/method
8
+ # declarations within source code in an effort to
9
+ # determine if XSS is possible
10
+ module LocalStorage
11
+ @@logger = PWN::Plugins::PWNLogger.create
12
+
13
+ # Supported Method Parameters::
14
+ # PWN::SAST::LocalStorage.scan(
15
+ # dir_path: 'optional path to dir defaults to .'
16
+ # git_repo_root_uri: 'optional http uri of git repo scanned'
17
+ # )
18
+
19
+ public_class_method def self.scan(opts = {})
20
+ dir_path = opts[:dir_path]
21
+ git_repo_root_uri = opts[:git_repo_root_uri].to_s.scrub
22
+ result_arr = []
23
+ logger_results = ''
24
+
25
+ PWN::Plugins::FileFu.recurse_dir(dir_path: dir_path) do |entry|
26
+ if File.file?(entry) && File.basename(entry) !~ /^pwn.+(html|json|db)$/ && File.basename(entry) !~ /\.JS-BEAUTIFIED$/ && entry !~ /test/i
27
+ line_no_and_contents_arr = []
28
+ entry_beautified = false
29
+
30
+ if File.extname(entry) == '.js' && (`wc -l #{entry}`.split.first.to_i < 20 || entry.include?('.min.js') || entry.include?('-all.js'))
31
+ js_beautify = `js-beautify #{entry} > #{entry}.JS-BEAUTIFIED`.to_s.scrub
32
+ entry = "#{entry}.JS-BEAUTIFIED"
33
+ entry_beautified = true
34
+ end
35
+
36
+ test_case_filter = "
37
+ grep -n \
38
+ -e 'localStorage.getItem(' \
39
+ -e 'localStorage.setItem(' #{entry}
40
+ "
41
+
42
+ str = `#{test_case_filter}`.to_s.scrub
43
+
44
+ if str.to_s.empty?
45
+ # If str length is >= 64 KB do not include results. (Due to Mongo Document Size Restrictions)
46
+ logger_results = "#{logger_results}~" # Catching bugs is good :)
47
+ else
48
+ str = "1:Result larger than 64KB -> Size: #{str.to_s.length}. Please click the \"Path\" link for more details." if str.to_s.length >= 64_000
49
+
50
+ hash_line = {
51
+ timestamp: Time.now.strftime('%Y-%m-%d %H:%M:%S.%9N %z').to_s,
52
+ security_references: security_references,
53
+ filename: { git_repo_root_uri: git_repo_root_uri, entry: entry },
54
+ line_no_and_contents: '',
55
+ raw_content: str,
56
+ test_case_filter: test_case_filter
57
+ }
58
+
59
+ # COMMMENT: Must be a better way to implement this (regex is kinda funky)
60
+ line_contents_split = str.split(/^(\d{1,}):|\n(\d{1,}):/)[1..-1]
61
+ line_no_count = line_contents_split.length # This should always be an even number
62
+ current_count = 0
63
+ while line_no_count > current_count
64
+ line_no = line_contents_split[current_count]
65
+ contents = line_contents_split[current_count + 1]
66
+ if Dir.exist?("#{dir_path}/.git") ||
67
+ Dir.exist?('.git')
68
+
69
+ repo_root = dir_path
70
+ repo_root = '.' if Dir.exist?('.git')
71
+
72
+ author = PWN::Plugins::Git.get_author(
73
+ repo_root: repo_root,
74
+ from_line: line_no,
75
+ to_line: line_no,
76
+ target_file: entry,
77
+ entry_beautified: entry_beautified
78
+ )
79
+ else
80
+ author = 'N/A'
81
+ end
82
+ hash_line[:line_no_and_contents] = line_no_and_contents_arr.push(
83
+ line_no: line_no,
84
+ contents: contents,
85
+ author: author
86
+ )
87
+
88
+ current_count += 2
89
+ end
90
+ result_arr.push(hash_line)
91
+ logger_results = "#{logger_results}x" # Seeing progress is good :)
92
+ end
93
+ end
94
+ end
95
+ logger_banner = "http://#{Socket.gethostname}:8808/doc_root/pwn-#{PWN::VERSION.to_s.scrub}/#{to_s.scrub.gsub('::', '/')}.html"
96
+ if logger_results.empty?
97
+ @@logger.info("#{logger_banner}: No files applicable to this test case.\n")
98
+ else
99
+ @@logger.info("#{logger_banner} => #{logger_results}complete.\n")
100
+ end
101
+ result_arr
102
+ rescue StandardError => e
103
+ raise e
104
+ end
105
+
106
+ # Used primarily to map NIST 800-53 Revision 4 Security Controls
107
+ # https://web.nvd.nist.gov/view/800-53/Rev4/impact?impactName=HIGH
108
+ # to PWN Exploit & Static Code Anti-Pattern Matching Modules to
109
+ # Determine the level of Testing Coverage w/ PWN.
110
+
111
+ public_class_method def self.security_references
112
+ {
113
+ sast_module: self,
114
+ section: 'MALICIOUS CODE PROTECTION',
115
+ nist_800_53_uri: 'https://csrc.nist.gov/Projects/risk-management/sp800-53-controls/release-search#/control/?version=5.1&number=SI-3',
116
+ cwe_id: '79',
117
+ cwe_uri: 'https://cwe.mitre.org/data/definitions/79.html'
118
+ }
119
+ rescue StandardError => e
120
+ raise e
121
+ end
122
+
123
+ # Author(s):: 0day Inc. <support@0dayinc.com>
124
+
125
+ public_class_method def self.authors
126
+ "AUTHOR(S):
127
+ 0day Inc. <support@0dayinc.com>
128
+ "
129
+ end
130
+
131
+ # Display Usage for this Module
132
+
133
+ public_class_method def self.help
134
+ puts "USAGE:
135
+ sast_arr = #{self}.scan(
136
+ dir_path: 'optional path to dir defaults to .',
137
+ git_repo_root_uri: 'optional http uri of git repo scanned'
138
+ )
139
+
140
+ #{self}.authors
141
+ "
142
+ end
143
+ end
144
+ end
145
+ end