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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/API_Fuzzer.gemspec +31 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +10 -0
- data/app/controllers/ping_controller.rb +22 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/routes.rb +4 -0
- data/lib/API_Fuzzer.rb +43 -0
- data/lib/API_Fuzzer/csrf_check.rb +41 -0
- data/lib/API_Fuzzer/engine.rb +5 -0
- data/lib/API_Fuzzer/error.rb +11 -0
- data/lib/API_Fuzzer/header_info.rb +50 -0
- data/lib/API_Fuzzer/idor_check.rb +62 -0
- data/lib/API_Fuzzer/privilege_escalation_check.rb +78 -0
- data/lib/API_Fuzzer/rate_limit_check.rb +69 -0
- data/lib/API_Fuzzer/redirect_check.rb +106 -0
- data/lib/API_Fuzzer/request.rb +69 -0
- data/lib/API_Fuzzer/resource_info.rb +51 -0
- data/lib/API_Fuzzer/sql_blind_check.rb +52 -0
- data/lib/API_Fuzzer/sql_check.rb +156 -0
- data/lib/API_Fuzzer/version.rb +3 -0
- data/lib/API_Fuzzer/vulnerability.rb +14 -0
- data/lib/API_Fuzzer/xss_check.rb +92 -0
- data/lib/API_Fuzzer/xxe_check.rb +47 -0
- data/payloads/blind_sql.txt +3 -0
- data/payloads/detect/sql.txt +89 -0
- data/payloads/sql.txt +196 -0
- data/payloads/xss.txt +58 -0
- data/rules/headers.yml +17 -0
- data/rules/info.yml +21 -0
- metadata +163 -0
@@ -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
|