API_Fuzzer 0.1.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.
@@ -0,0 +1,62 @@
1
+ require 'API_Fuzzer/vulnerability'
2
+ require 'API_Fuzzer/error'
3
+ require 'API_Fuzzer/request'
4
+
5
+ module API_Fuzzer
6
+ class IdorCheck
7
+ class << self
8
+ def scan(options = {})
9
+ @url = options[:url]
10
+ @params = options[:params]
11
+ @methods = options[:method]
12
+ @headers = options[:headers] || {}
13
+ @cookies = options[:cookies]
14
+ @vulnerabilities = []
15
+
16
+ fuzz_without_session
17
+ @vulnerabilities.uniq { |vuln| vuln.description }
18
+ end
19
+
20
+ def fuzz_without_session
21
+ @methods.each do |method|
22
+ response = API_Fuzzer::Request.send_api_request(
23
+ url: @url,
24
+ params: @params,
25
+ method: method,
26
+ headers: @headers,
27
+ cookies: @cookies
28
+ )
29
+
30
+ response_without_session = API_Fuzzer::Request.send_api_request(
31
+ url: @url,
32
+ params: @params,
33
+ method: method
34
+ )
35
+
36
+ fuzz_sensitive_files(response, method)
37
+ fuzz_match(response, response_without_session, method)
38
+ end
39
+ end
40
+
41
+ def fuzz_match(resp, resp_without_session, method)
42
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
43
+ type: 'HIGH',
44
+ value: "API doesn't have access control protection",
45
+ description: "Possible IDOR in #{method} #{@url}"
46
+ ) if resp.body.to_s == resp_without_session.body.to_s
47
+ end
48
+
49
+ def fuzz_sensitive_files(response, method)
50
+ file_url = /^((https?:\/\/)?(www\.)?([\da-z\.-]+)\.([a-z\.]{2,6})\/[\w \.-]+?\.(pdf|doc|docs|rtf)([a-zA-Z0-9=?]*?))$/
51
+ flagged_url = response.body.to_s.scan(file_url) || []
52
+ flagged_url.each do |url|
53
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
54
+ type: 'MEDIUM',
55
+ value: "File #{url} can be accessed without proper permissions",
56
+ description: "Access control violation in #{method} #{url}"
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,78 @@
1
+ require 'API_Fuzzer/vulnerability'
2
+ require 'API_Fuzzer/error'
3
+ require 'API_Fuzzer/request'
4
+
5
+ module API_Fuzzer
6
+ class PrivilegeEscalationCheck
7
+ class << self
8
+ def scan(options = {})
9
+ @url = options[:url]
10
+ @params = options[:params] || {}
11
+ @headers = options[:headers] || {}
12
+ @methods = options[:method] || []
13
+ @cookies = options[:cookies] || {}
14
+
15
+ @vulnerabilities = []
16
+ fuzz_privileges
17
+ @vulnerabilities.uniq { |vuln| vuln.description }
18
+ rescue Exception => e
19
+ Rails.logger.info e.message
20
+ end
21
+
22
+ def fuzz_privileges
23
+ id = /\A\d+\z/
24
+ uri = URI(@url)
25
+ path = uri.path
26
+ query = uri.query
27
+ url = @url
28
+ base_uri = query.nil? ? path : [path, query].join("?")
29
+ fragments = base_uri.split(/[\/,?,&]/) - ['']
30
+ fragments.each do |fragment|
31
+ if fragment.match(/\A(\w)+=(\w)*\z/)
32
+ key, value = fragment.split("=")
33
+ if value.match(id)
34
+ value = value.to_i
35
+ value += 1
36
+ url = url.gsub(fragment, [key, value].join("=")).chomp
37
+ fuzz_identity(url, @params)
38
+ end
39
+ elsif fragment.match(id)
40
+ value = fragment.to_i
41
+ value += 1
42
+ url = url.gsub(fragment, value.to_s).chomp if url
43
+ fuzz_identity(url, @params, url)
44
+ end
45
+ end
46
+ return if @params.empty?
47
+
48
+ parameters = @params
49
+ parameters.keys.each do |parameter|
50
+ value = parameters[parameter]
51
+ if value.match(id)
52
+ value = value.to_i
53
+ value += 1
54
+ info = [parameter, value].join(" ")
55
+ fuzz_identity(@url, parameters.merge(parameter, value), info)
56
+ end
57
+ end
58
+ end
59
+
60
+ def fuzz_identity(url, params, value)
61
+ @methods.each do |method|
62
+ response = API_Fuzzer::Request.send_api_request(
63
+ url: url,
64
+ method: method,
65
+ params: @params,
66
+ cookies: @cookies,
67
+ headers: @headers
68
+ )
69
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
70
+ type: 'HIGH',
71
+ value: "ID in #{value} parameter is vulnerable to Privilege Escalation vulnerability.",
72
+ description: "Privilege Escalation vulnerability in #{method} #{url}"
73
+ ) if response.code == 200
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,69 @@
1
+ require 'API_Fuzzer/vulnerability'
2
+ require 'API_Fuzzer/request'
3
+
4
+ module API_Fuzzer
5
+ class RateLimitCheck
6
+ def self.scan(options = {})
7
+ @url = options[:url]
8
+ @params = options[:params] || {}
9
+ @headers = options[:headers] || {}
10
+ @cookies = options[:cookies] || {}
11
+ @vulnerabilities = []
12
+ @limit = options[:limit] || 50
13
+ @methods = options[:method] || [:get]
14
+
15
+ @methods.each { |method| fuzz_api_requests(method) }
16
+ @vulnerabilities.uniq { |vuln| vuln.description }
17
+ end
18
+
19
+ def self.fuzz_api_requests(method)
20
+ initial_response = fetch_initial_response(method)
21
+
22
+ responses = []
23
+ @limit.times do
24
+ responses << API_Fuzzer::Request.send_api_request(
25
+ url: @url,
26
+ method: method,
27
+ cookies: @cookies,
28
+ headers: @headers,
29
+ params: @params
30
+ )
31
+ end
32
+
33
+ vulnerable = true
34
+ responses.each do |response|
35
+ if response.code == initial_response.code
36
+ content_length = response_content_length(response)
37
+ initial_content_length = response_content_length(initial_response)
38
+ if content_length != initial_content_length
39
+ vulnerable = false
40
+ break
41
+ end
42
+ else
43
+ vulnerable = false
44
+ break
45
+ end
46
+ end
47
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
48
+ description: "API is not rate limited for #{method} #{@url}",
49
+ value: "API doesn't have any ratelimiting protection enabled which can be implemented by either throttling request or using captcha",
50
+ type: 'LOW'
51
+ ) if vulnerable
52
+ end
53
+
54
+ private
55
+ def self.fetch_initial_response(method)
56
+ API_Fuzzer::Request.send_api_request(
57
+ url: @url,
58
+ method: method,
59
+ cookies: @cookies,
60
+ headers: @headers,
61
+ params: @params
62
+ )
63
+ end
64
+
65
+ def self.response_content_length(response)
66
+ response.headers['Content-Length'] || response.body.to_s.size
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,106 @@
1
+ require 'API_Fuzzer/vulnerability'
2
+ require 'API_Fuzzer/error'
3
+ require 'API_Fuzzer/request'
4
+ require 'uri'
5
+
6
+ module API_Fuzzer
7
+ class RedirectCheck
8
+ REDIRECT_URL = 'http://127.0.0.1:3000/ping'
9
+ ALLOWED_METHODS = [:get, :post]
10
+ class << self
11
+ def scan(options = {})
12
+ @url = options[:url]
13
+ @params = options[:params] || {}
14
+ @cookies = options[:cookies] || {}
15
+ @json = options[:json] || false
16
+ @headers = options[:headers] || {}
17
+
18
+ @vulnerabilities = []
19
+ fuzz_payload
20
+ return @vulnerabilities.uniq { |vuln| vuln.description }
21
+ rescue Exception => e
22
+ @vulnerabilities << API_Fuzzer::Error.new(
23
+ description: e.message,
24
+ status: 'ERROR',
25
+ value: e.backtrace
26
+ )
27
+ end
28
+
29
+ def fuzz_payload
30
+ uri = URI(@url)
31
+ path = uri.path
32
+ query = uri.query
33
+ # base_uri = query.nil? ? path : [path, query].join("?")
34
+ fragments = path.split(/[\/,?,&]/) - ['']
35
+ fragments << query.split('&') if query
36
+ fragments.flatten!
37
+ fragments.each do |fragment|
38
+ if fragment.match(/\A(\w+)=(.?*)\z/) && valid_url?($2)
39
+ url = @url.gsub($2, REDIRECT_URL).chomp
40
+ fuzz_fragment(url)
41
+ elsif valid_url?(fragment)
42
+ url = @url.gsub(fragment, REDIRECT_URL)
43
+ fuzz_fragment(url)
44
+ end
45
+ end
46
+ return if @params.empty?
47
+
48
+ @params.keys.each do |parameter|
49
+ fuzz_each_parameter(parameter) if valid_url? @params[parameter]
50
+ end
51
+ end
52
+
53
+ def fuzz_fragment(url)
54
+ ALLOWED_METHODS.each do |method|
55
+ begin
56
+ response = API_Fuzzer::Request.send_api_request(
57
+ url: url,
58
+ method: method,
59
+ cookies: @cookies,
60
+ params: @params,
61
+ headers: @headers
62
+ )
63
+
64
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
65
+ description: "Possible Open Redirect vulnerability in #{method} #{url}",
66
+ parameter: "URL: #{url}",
67
+ value: "[PAYLOAD] #{url.gsub(REDIRECT_URL, 'PAYLOAD_URL')}",
68
+ type: 'MEDIUM'
69
+ ) if response.headers['Location'] =~ /#{REDIRECT_URL}/
70
+ rescue Exception => e
71
+ puts e.message
72
+ end
73
+ end
74
+ end
75
+
76
+ def fuzz_each_parameter(parameter)
77
+ params = @params
78
+ params[parameter] = REDIRECT_URL
79
+ ALLOWED_METHODS.each do |method|
80
+ begin
81
+ response = API_Fuzzer::Request.send_api_request(
82
+ url: @url,
83
+ method: method,
84
+ cookies: @cookies,
85
+ params: params,
86
+ headers: @headers
87
+ )
88
+
89
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
90
+ description: "Possible Open Redirect vulnerability in #{method} #{url}",
91
+ parameter: "Parameter: #{parameter}",
92
+ value: "[PAYLOAD] #{params.to_s.gsub(REDIRECT_URL, 'PAYLOAD_URL')}",
93
+ type: 'MEDIUM'
94
+ ) if response.headers['LOCATION'] =~ /#{REDIRECT_URL}/
95
+ rescue Exception => e
96
+ puts e.message
97
+ end
98
+ end
99
+ end
100
+
101
+ def valid_url? url
102
+ url =~ URI.regexp
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,69 @@
1
+ require 'http'
2
+
3
+ module API_Fuzzer
4
+ class Request
5
+ attr_accessor :response, :request
6
+
7
+ class << self
8
+ def send_api_request(options = {})
9
+ @url = options.delete(:url)
10
+ @params = options.delete(:params) || {}
11
+ @method = options.delete(:method) || :get
12
+ @json = options.delete(:json) ? true : false
13
+ @body = options.delete(:body) ? true : false
14
+ @request = set_cookies_headers(options)
15
+ send_request
16
+ end
17
+ end
18
+
19
+ def response
20
+ @response
21
+ end
22
+
23
+ def success?
24
+ @response.code == 200
25
+ end
26
+
27
+ private
28
+
29
+ def self.set_cookies_headers(options = {})
30
+ cookies = options.delete(:cookies) || {}
31
+ headers = options.delete(:headers) || {}
32
+ request_object = HTTP.headers(headers).cookies(cookies)
33
+ request_object
34
+ end
35
+
36
+ def self.send_request
37
+ @response = case @method.to_sym
38
+ when :post
39
+ @request.post(@url, set_params)
40
+ when :put
41
+ @request.put(@url, set_params)
42
+ when :patch
43
+ @request.patch(@url, set_params)
44
+ when :head
45
+ @request.head(@url, set_params)
46
+ when :delete
47
+ @request.delete(@url, set_params)
48
+ else
49
+ @request.get(@url, set_params)
50
+ end
51
+ end
52
+
53
+ def self.set_params
54
+ if @json && !method_get?
55
+ { 'json' => @params }
56
+ elsif method_get?
57
+ { 'params' => @params }
58
+ elsif @body
59
+ { 'body' => @params }
60
+ else
61
+ { 'form' => @params }
62
+ end
63
+ end
64
+
65
+ def self.method_get?
66
+ @method.to_s == 'get'
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ require 'yaml'
2
+ require 'API_Fuzzer/vulnerability'
3
+
4
+ module API_Fuzzer
5
+
6
+ class InvalidResponse < StandardError; end
7
+
8
+ class ResourceInfo
9
+ # Accepts response and performs rules match based on the ruleset
10
+
11
+ class << self
12
+ def scan(response)
13
+ @response = response
14
+ if @response
15
+ fetch_rules
16
+ scan_rules
17
+ else
18
+ raise InvalidResponse, "Invalid response argument has been passed"
19
+ end
20
+ end
21
+
22
+ def fetch_rules
23
+ info_rules = File.expand_path('../../../rules', __FILE__)
24
+ @rules = YAML::load_file(File.join(info_rules, "info.yml"))['rules']
25
+ end
26
+
27
+ def scan_rules
28
+ @vulnerability_info = []
29
+
30
+ if @rules
31
+ headers = @response.headers.keys
32
+
33
+ @rules.each do |rule|
34
+ headers.each do |header|
35
+
36
+ if /#{rule['match'].downcase}/.match(header.downcase)
37
+ @vulnerability_info << API_Fuzzer::Vulnerability.new(
38
+ description: rule['description'],
39
+ value: [header, @response.headers[header].to_s].join(": "),
40
+ type: 'INFORMATIVE'
41
+ )
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ return @vulnerability_info
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ require 'API_Fuzzer/vulnerability'
2
+ require 'API_Fuzzer/error'
3
+ require 'API_Fuzzer/request'
4
+ require 'API_Fuzzer/sql_check'
5
+
6
+ module API_Fuzzer
7
+ class InvalidURLError < StandardError; end
8
+ class SqlBlindCheck < SqlCheck
9
+ PAYLOAD_PATH = '../../../payloads/blind_sql.txt'.freeze
10
+ SQL_ERRORS = []
11
+ SCAN_TIME = '20'
12
+ attr_accessor :payloads
13
+
14
+ def self.fuzz_each_parameter(parameter, payload)
15
+ @params[parameter] << payload
16
+ process_vulnerability(nil, payload)
17
+ end
18
+
19
+ def self.fuzz_each_fragment(url, payload)
20
+ process_vulnerability(url, payload)
21
+ end
22
+
23
+ def self.process_vulnerability(url, payload)
24
+ url = url ? url : @url
25
+ ALLOWED_METHODS.each do |method|
26
+ start_time = Time.now
27
+ response = API_Fuzzer::Request.send_api_request(
28
+ url: @url,
29
+ params: @params,
30
+ method: method,
31
+ cookies: @cookies,
32
+ headers: @headers
33
+ )
34
+ end_time = Time.now
35
+ diff = end_time - start_time
36
+ if diff > 20 && diff < 25
37
+ @vulnerabilities << API_Fuzzer::Vulnerability.new(
38
+ description: "Possible blind SQL injection in #{method} #{@url} parameter: #{parameter}",
39
+ value: "[PAYLOAD] #{payload}"
40
+ )
41
+ end
42
+ end
43
+ end
44
+
45
+ def self.fetch_payloads
46
+ file = File.expand_path(PAYLOAD_PATH, __FILE__)
47
+ File.readlines(file).each do |line|
48
+ @payloads << line.gsub('__TIME__', SCAN_TIME).gsub('__MARK__', '20000000')
49
+ end
50
+ end
51
+ end
52
+ end