API_Fuzzer 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|