aikido-zen 1.0.2-aarch64-linux
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/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +32 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +148 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +22 -0
- data/benchmarks/rails7.1_benchmark.js +1 -0
- data/benchmarks/rails7.1_sql_injection.js +102 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +133 -0
- data/docs/proxy.md +10 -0
- data/docs/rails.md +112 -0
- data/docs/troubleshooting.md +62 -0
- data/lib/aikido/zen/actor.rb +146 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +181 -0
- data/lib/aikido/zen/api_client.rb +145 -0
- data/lib/aikido/zen/attack.rb +217 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
- data/lib/aikido/zen/attack_wave.rb +88 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/cache.rb +91 -0
- data/lib/aikido/zen/capped_collections.rb +86 -0
- data/lib/aikido/zen/collector/event.rb +238 -0
- data/lib/aikido/zen/collector/hosts.rb +30 -0
- data/lib/aikido/zen/collector/routes.rb +71 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +122 -0
- data/lib/aikido/zen/collector/users.rb +32 -0
- data/lib/aikido/zen/collector.rb +223 -0
- data/lib/aikido/zen/config.rb +312 -0
- data/lib/aikido/zen/context/rack_request.rb +27 -0
- data/lib/aikido/zen/context/rails_request.rb +47 -0
- data/lib/aikido/zen/context.rb +145 -0
- data/lib/aikido/zen/detached_agent/agent.rb +79 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +41 -0
- data/lib/aikido/zen/detached_agent/server.rb +78 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +107 -0
- data/lib/aikido/zen/event.rb +116 -0
- data/lib/aikido/zen/helpers.rb +24 -0
- data/lib/aikido/zen/internals.rb +123 -0
- data/lib/aikido/zen/libzen-v0.1.48-aarch64-linux.so +0 -0
- data/lib/aikido/zen/middleware/allowed_address_checker.rb +26 -0
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
- data/lib/aikido/zen/middleware/context_setter.rb +26 -0
- data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +50 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +197 -0
- data/lib/aikido/zen/outbound_connection.rb +62 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +50 -0
- data/lib/aikido/zen/rails_engine.rb +53 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +50 -0
- data/lib/aikido/zen/request/heuristic_router.rb +115 -0
- data/lib/aikido/zen/request/rails_router.rb +92 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
- data/lib/aikido/zen/request/schema/builder.rb +121 -0
- data/lib/aikido/zen/request/schema/definition.rb +107 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +87 -0
- data/lib/aikido/zen/request.rb +88 -0
- data/lib/aikido/zen/route.rb +96 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +78 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +66 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +68 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +65 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +94 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +266 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +55 -0
- data/lib/aikido/zen/scanners.rb +7 -0
- data/lib/aikido/zen/sink.rb +118 -0
- data/lib/aikido/zen/sinks/action_controller.rb +85 -0
- data/lib/aikido/zen/sinks/async_http.rb +80 -0
- data/lib/aikido/zen/sinks/curb.rb +113 -0
- data/lib/aikido/zen/sinks/em_http.rb +83 -0
- data/lib/aikido/zen/sinks/excon.rb +118 -0
- data/lib/aikido/zen/sinks/file.rb +153 -0
- data/lib/aikido/zen/sinks/http.rb +93 -0
- data/lib/aikido/zen/sinks/httpclient.rb +95 -0
- data/lib/aikido/zen/sinks/httpx.rb +78 -0
- data/lib/aikido/zen/sinks/kernel.rb +33 -0
- data/lib/aikido/zen/sinks/mysql2.rb +31 -0
- data/lib/aikido/zen/sinks/net_http.rb +101 -0
- data/lib/aikido/zen/sinks/patron.rb +103 -0
- data/lib/aikido/zen/sinks/pg.rb +72 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +85 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
- data/lib/aikido/zen/sinks/trilogy.rb +31 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +36 -0
- data/lib/aikido/zen/sinks_dsl.rb +250 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +80 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +303 -0
- data/lib/aikido-zen.rb +3 -0
- data/placeholder/.gitignore +4 -0
- data/placeholder/README.md +11 -0
- data/placeholder/Rakefile +75 -0
- data/placeholder/lib/placeholder.rb.template +3 -0
- data/placeholder/placeholder.gemspec.template +20 -0
- data/tasklib/bench.rake +94 -0
- data/tasklib/libzen.rake +133 -0
- data/tasklib/wrk.rb +88 -0
- metadata +214 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen::Scanners::ShellInjection
|
|
4
|
+
module Helpers
|
|
5
|
+
ESCAPE_CHARS = %W[' "]
|
|
6
|
+
DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES = %W[$ ` \\ !]
|
|
7
|
+
DANGEROUS_CHARS = [
|
|
8
|
+
"#", "!", '"', "$", "&", "'", "(", ")", "*", ";", "<", "=", ">", "?",
|
|
9
|
+
"[", "\\", "]", "^", "`", "{", "|", "}", " ", "\n", "\t", "~"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
COMMANDS = %w[sleep shutdown reboot poweroff halt ifconfig chmod chown ping
|
|
13
|
+
ssh scp curl wget telnet kill killall rm mv cp touch echo cat head
|
|
14
|
+
tail grep find awk sed sort uniq wc ls env ps who whoami id w df du
|
|
15
|
+
pwd uname hostname netstat passwd arch printenv logname pstree hostnamectl
|
|
16
|
+
set lsattr killall5 dmesg history free uptime finger top shopt :]
|
|
17
|
+
|
|
18
|
+
PATH_PREFIXES = %w[/bin/ /sbin/ /usr/bin/ /usr/sbin/ /usr/local/bin/ /usr/local/sbin/]
|
|
19
|
+
|
|
20
|
+
SEPARATORS = [" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">"]
|
|
21
|
+
|
|
22
|
+
# @param command [string]
|
|
23
|
+
# @param user_input [string]
|
|
24
|
+
def self.is_safely_encapsulated(command, user_input)
|
|
25
|
+
segments = command.split(user_input)
|
|
26
|
+
|
|
27
|
+
# The next condition is merely here to be compliant with what javascript does when splitting strings:
|
|
28
|
+
# From js doc https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
|
|
29
|
+
# > If separator appears at the beginning (or end) of the string, it still has the effect of splitting,
|
|
30
|
+
# > resulting in an empty (i.e. zero length) string appearing at the first (or last) position of
|
|
31
|
+
# > the returned array.
|
|
32
|
+
# This is necessary because this code is ported form the firewall-node code.
|
|
33
|
+
if user_input.length > 1
|
|
34
|
+
if command.start_with? user_input
|
|
35
|
+
segments.unshift ""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if command.end_with? user_input
|
|
39
|
+
segments << ""
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Call the helper function to get current and next segments
|
|
44
|
+
get_current_and_next_segments(segments).all? do |segments_pair|
|
|
45
|
+
char_before_user_input = segments_pair[:current_segment][-1]
|
|
46
|
+
char_after_user_input = segments_pair[:next_segment][0]
|
|
47
|
+
|
|
48
|
+
# Check if the character before is an escape character
|
|
49
|
+
is_escape_char = ESCAPE_CHARS.include?(char_before_user_input)
|
|
50
|
+
|
|
51
|
+
unless is_escape_char
|
|
52
|
+
next false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# If characters before and after the user input do not match, return false
|
|
56
|
+
next false if char_before_user_input != char_after_user_input
|
|
57
|
+
|
|
58
|
+
# If user input contains the escape character, return false
|
|
59
|
+
next false if user_input.include?(char_before_user_input)
|
|
60
|
+
|
|
61
|
+
# Handle dangerous characters inside double quotes
|
|
62
|
+
if char_before_user_input == '"' && DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES.any? { |char| user_input.include?(char) }
|
|
63
|
+
next false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
next true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.get_current_and_next_segments(segments)
|
|
71
|
+
segments.each_cons(2).map { |current_segment, next_segment| {current_segment: current_segment, next_segment: next_segment} }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Helper function for sorting commands by length (longer commands first)
|
|
75
|
+
def self.by_length(a, b)
|
|
76
|
+
b.length - a.length
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Escape characters with special meaning either inside or outside character sets.
|
|
80
|
+
# Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler
|
|
81
|
+
# form would be disallowed by Unicode patterns’ stricter grammar.
|
|
82
|
+
#
|
|
83
|
+
# Inspired by https://github.com/sindresorhus/escape-string-regexp/
|
|
84
|
+
def self.escape_string_regexp(string)
|
|
85
|
+
string.gsub(/[|\\{}()\[\]^$+*?.]/) { "\\#{$&}" }.gsub("-", '\\x2d')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Construct the regex for commands
|
|
89
|
+
COMMANDS_REGEX = Regexp.new(
|
|
90
|
+
"([/.]*((#{PATH_PREFIXES.map { |p| Helpers.escape_string_regexp(p) }.join("|")})?((#{COMMANDS.sort(&method(:by_length)).join("|")}))))",
|
|
91
|
+
Regexp::IGNORECASE
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def self.contains_shell_syntax(command, user_input)
|
|
95
|
+
# Check if input is only whitespace
|
|
96
|
+
return false if user_input.strip.empty?
|
|
97
|
+
|
|
98
|
+
# Check if the user input contains any dangerous characters
|
|
99
|
+
if DANGEROUS_CHARS.any? { |c| user_input.include?(c) }
|
|
100
|
+
return true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# If the command is exactly the same as the user input, check if it matches the regex
|
|
104
|
+
if command == user_input
|
|
105
|
+
return match_all(command, COMMANDS_REGEX).any? do |match|
|
|
106
|
+
match[:match].length == command.length && match[:match] == command
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check if the command contains a commonly used command
|
|
111
|
+
match_all(command, COMMANDS_REGEX).each do |match|
|
|
112
|
+
# We found a command like `rm` or `/sbin/shutdown` in the command
|
|
113
|
+
# Check if the command is the same as the user input
|
|
114
|
+
# If it's not the same, continue searching
|
|
115
|
+
next if user_input != match[:match]
|
|
116
|
+
|
|
117
|
+
# Otherwise, we'll check if the command is surrounded by separators
|
|
118
|
+
# These separators are used to separate commands and arguments
|
|
119
|
+
# e.g. `rm<space>-rf`
|
|
120
|
+
# e.g. `ls<newline>whoami`
|
|
121
|
+
# e.g. `echo<tab>hello` Check if the command is surrounded by separators
|
|
122
|
+
char_before = if match[:index] - 1 < 0
|
|
123
|
+
nil
|
|
124
|
+
else
|
|
125
|
+
command[match[:index] - 1]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
char_after = if match[:index] + match[:match].length >= command.length
|
|
129
|
+
nil
|
|
130
|
+
else
|
|
131
|
+
command[match[:index] + match[:match].length]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# e.g. `<separator>rm<separator>`
|
|
135
|
+
if SEPARATORS.include?(char_before) && SEPARATORS.include?(char_after)
|
|
136
|
+
return true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# e.g. `<separator>rm`
|
|
140
|
+
if SEPARATORS.include?(char_before) && char_after.nil?
|
|
141
|
+
return true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# e.g. `rm<separator>`
|
|
145
|
+
if char_before.nil? && SEPARATORS.include?(char_after)
|
|
146
|
+
return true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.match_all(string, regex)
|
|
154
|
+
string.enum_for(:scan, regex).map do |match|
|
|
155
|
+
{match: match[0], index: $~.begin(0)}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shell_injection/helpers"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
module Scanners
|
|
7
|
+
class ShellInjectionScanner
|
|
8
|
+
def self.skips_on_nil_context?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param command [String]
|
|
13
|
+
# @param sink [Aikido::Zen::Sink]
|
|
14
|
+
# @param context [Aikido::Zen::Context]
|
|
15
|
+
# @param operation [Symbol, String]
|
|
16
|
+
#
|
|
17
|
+
def self.call(command:, sink:, context:, operation:)
|
|
18
|
+
context.payloads.each do |payload|
|
|
19
|
+
next unless new(command, payload.value.to_s).attack?
|
|
20
|
+
|
|
21
|
+
return Attacks::ShellInjectionAttack.new(
|
|
22
|
+
sink: sink,
|
|
23
|
+
input: payload,
|
|
24
|
+
command: command,
|
|
25
|
+
context: context,
|
|
26
|
+
operation: "#{sink.operation}.#{operation}",
|
|
27
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param command [String]
|
|
35
|
+
# @param input [String]
|
|
36
|
+
def initialize(command, input)
|
|
37
|
+
@command = command
|
|
38
|
+
@input = input
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attack?
|
|
42
|
+
# Block single ~ character. For example `echo ~`
|
|
43
|
+
if @input == "~"
|
|
44
|
+
if @command.size > 1 && @command.include?("~")
|
|
45
|
+
return true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# we ignore single character since they don't pose a big threat.
|
|
50
|
+
# They are only able to crash the shell, not execute arbitraty commands.
|
|
51
|
+
return false if @input.size <= 1
|
|
52
|
+
|
|
53
|
+
# We ignore cases where the user input is longer than the command because
|
|
54
|
+
# the user input can't be part of the command
|
|
55
|
+
return false if @input.size > @command.size
|
|
56
|
+
|
|
57
|
+
return false unless @command.include?(@input)
|
|
58
|
+
|
|
59
|
+
return false if ShellInjection::Helpers.is_safely_encapsulated @command, @input
|
|
60
|
+
|
|
61
|
+
ShellInjection::Helpers.contains_shell_syntax @command, @input
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../attack"
|
|
4
|
+
require_relative "../internals"
|
|
5
|
+
|
|
6
|
+
module Aikido::Zen
|
|
7
|
+
module Scanners
|
|
8
|
+
class SQLInjectionScanner
|
|
9
|
+
def self.skips_on_nil_context?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Checks if the given SQL query may have dangerous user input injected,
|
|
14
|
+
# and returns an Attack if so, based on the current request.
|
|
15
|
+
#
|
|
16
|
+
# @param query [String]
|
|
17
|
+
# @param context [Aikido::Zen::Context]
|
|
18
|
+
# @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
|
|
19
|
+
# @param dialect [Symbol] one of +:mysql+, +:postgesql+, or +:sqlite+.
|
|
20
|
+
# @param operation [Symbol, String] name of the method being scanned.
|
|
21
|
+
# Expects +sink.operation+ being set to get the full module/name combo.
|
|
22
|
+
#
|
|
23
|
+
# @return [Aikido::Zen::Attack, nil] an Attack if any user input is
|
|
24
|
+
# detected to be attempting a SQL injection, or nil if this is safe.
|
|
25
|
+
#
|
|
26
|
+
# @raise [Aikido::Zen::InternalsError] if an error occurs when loading or
|
|
27
|
+
# calling zenlib. See Sink#scan.
|
|
28
|
+
def self.call(query:, dialect:, sink:, context:, operation:)
|
|
29
|
+
dialect = DIALECTS.fetch(dialect) do
|
|
30
|
+
Aikido::Zen.config.logger.warn "Unknown SQL dialect #{dialect.inspect}"
|
|
31
|
+
DIALECTS[:common]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context.payloads.each do |payload|
|
|
35
|
+
next unless new(query, payload.value.to_s, dialect).attack?
|
|
36
|
+
|
|
37
|
+
return Attacks::SQLInjectionAttack.new(
|
|
38
|
+
sink: sink,
|
|
39
|
+
query: query,
|
|
40
|
+
input: payload,
|
|
41
|
+
dialect: dialect,
|
|
42
|
+
context: context,
|
|
43
|
+
operation: "#{sink.operation}.#{operation}",
|
|
44
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(query, input, dialect)
|
|
52
|
+
@query = query.downcase
|
|
53
|
+
@input = input.downcase
|
|
54
|
+
@dialect = dialect
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def attack?
|
|
58
|
+
# Ignore single char inputs since they shouldn't be able to do much harm
|
|
59
|
+
return false if @input.length <= 1
|
|
60
|
+
|
|
61
|
+
# If the input is longer than the query, then it is not part of it
|
|
62
|
+
return false if @input.length > @query.length
|
|
63
|
+
|
|
64
|
+
# If the input is not included in the query at all, then we are safe
|
|
65
|
+
return false unless @query.include?(@input)
|
|
66
|
+
|
|
67
|
+
# If the input is solely alphanumeric, we can ignore it
|
|
68
|
+
return false if /\A[[:alnum:]_]+\z/i.match?(@input)
|
|
69
|
+
|
|
70
|
+
# If the input is a comma-separated list of numbers, ignore it.
|
|
71
|
+
return false if /\A(?:\d+(?:,\s*)?)+\z/i.match?(@input)
|
|
72
|
+
|
|
73
|
+
Internals.detect_sql_injection(@query, @input, @dialect)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @api private
|
|
77
|
+
Dialect = Struct.new(:name, :internals_key, keyword_init: true) do
|
|
78
|
+
alias_method :to_s, :name
|
|
79
|
+
alias_method :to_int, :internals_key
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Maps easy-to-use Symbols to a struct that keeps both the name and the
|
|
83
|
+
# internal identifier used by libzen.
|
|
84
|
+
#
|
|
85
|
+
# @see https://github.com/AikidoSec/zen-internals/blob/main/src/sql_injection/helpers/select_dialect_based_on_enum.rs
|
|
86
|
+
DIALECTS = {
|
|
87
|
+
common: Dialect.new(name: "SQL", internals_key: 0),
|
|
88
|
+
mysql: Dialect.new(name: "MySQL", internals_key: 8),
|
|
89
|
+
postgresql: Dialect.new(name: "PostgreSQL", internals_key: 9),
|
|
90
|
+
sqlite: Dialect.new(name: "SQLite", internals_key: 12)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
module Scanners
|
|
7
|
+
module SSRF
|
|
8
|
+
# Simple per-request cache of all DNS lookups performed for a given host.
|
|
9
|
+
# We can store this in the context after performing a lookup, and have the
|
|
10
|
+
# SSRF scanner make sure the hostname being inspected doesn't actually
|
|
11
|
+
# resolve to an internal/dangerous IP.
|
|
12
|
+
class DNSLookups < SimpleDelegator
|
|
13
|
+
def initialize
|
|
14
|
+
super(Hash.new { |h, k| h[k] = [] })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add(hostname, addresses)
|
|
18
|
+
self[hostname].concat(Array(addresses))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ===(hostname)
|
|
22
|
+
key?(hostname)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "resolv"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
|
|
6
|
+
module Aikido::Zen
|
|
7
|
+
module Scanners
|
|
8
|
+
module SSRF
|
|
9
|
+
# Little helper to check if a given hostname or address is to be
|
|
10
|
+
# considered "dangerous" when used for an outbound HTTP request.
|
|
11
|
+
#
|
|
12
|
+
# When given a hostname:
|
|
13
|
+
#
|
|
14
|
+
# * If any DNS lookups have been performed and stored in the current Zen
|
|
15
|
+
# context (under the "dns.lookups" metadata key), we will map it to the
|
|
16
|
+
# list of IPs that we've resolved it to.
|
|
17
|
+
#
|
|
18
|
+
# * If not, we'll still try to map it to any statically defined address in
|
|
19
|
+
# the system hosts file (e.g. /etc/hosts).
|
|
20
|
+
#
|
|
21
|
+
# Once we mapped the hostname to an IP address (or, if given an IP
|
|
22
|
+
# address), this will check that it's not a loopback address, a private IP
|
|
23
|
+
# address (as defined by RFCs 1918 and 4193), or in one of the
|
|
24
|
+
# "special-use" IP ranges defined in RFC 5735.
|
|
25
|
+
class PrivateIPChecker
|
|
26
|
+
def initialize(resolver = Resolv::Hosts.new)
|
|
27
|
+
@resolver = resolver
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param hostname_or_address [String]
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def private?(hostname_or_address)
|
|
33
|
+
resolve(hostname_or_address).any? do |ip|
|
|
34
|
+
PRIVATE_RANGES.any? { |range| range === ip }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Source: https://github.com/AikidoSec/firewall-node/blob/main/library/vulnerabilities/ssrf/isPrivateIP.ts
|
|
41
|
+
PRIVATE_IPV4_RANGES = [
|
|
42
|
+
IPAddr.new("0.0.0.0/8"), # "This" network (RFC 1122)
|
|
43
|
+
IPAddr.new("10.0.0.0/8"), # Private-Use Networks (RFC 1918)
|
|
44
|
+
IPAddr.new("100.64.0.0/10"), # Shared Address Space (RFC 6598)
|
|
45
|
+
IPAddr.new("127.0.0.0/8"), # Loopback (RFC 1122)
|
|
46
|
+
IPAddr.new("169.254.0.0/16"), # Link Local (RFC 3927)
|
|
47
|
+
IPAddr.new("172.16.0.0/12"), # Private-Use Networks (RFC 1918)
|
|
48
|
+
IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments (RFC 5736)
|
|
49
|
+
IPAddr.new("192.0.2.0/24"), # TEST-NET-1 (RFC 5737)
|
|
50
|
+
IPAddr.new("192.31.196.0/24"), # AS112 Redirection Anycast (RFC 7535)
|
|
51
|
+
IPAddr.new("192.52.193.0/24"), # Automatic Multicast Tunneling (RFC 7450)
|
|
52
|
+
IPAddr.new("192.88.99.0/24"), # 6to4 Relay Anycast (RFC 3068)
|
|
53
|
+
IPAddr.new("192.168.0.0/16"), # Private-Use Networks (RFC 1918)
|
|
54
|
+
IPAddr.new("192.175.48.0/24"), # AS112 Redirection Anycast (RFC 7535)
|
|
55
|
+
IPAddr.new("198.18.0.0/15"), # Network Interconnect Device Benchmark Testing (RFC 2544)
|
|
56
|
+
IPAddr.new("198.51.100.0/24"), # TEST-NET-2 (RFC 5737)
|
|
57
|
+
IPAddr.new("203.0.113.0/24"), # TEST-NET-3 (RFC 5737)
|
|
58
|
+
IPAddr.new("224.0.0.0/4"), # Multicast (RFC 3171)
|
|
59
|
+
IPAddr.new("240.0.0.0/4"), # Reserved for Future Use (RFC 1112)
|
|
60
|
+
IPAddr.new("255.255.255.255/32") # Limited Broadcast (RFC 919)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
PRIVATE_IPV6_RANGES = [
|
|
64
|
+
IPAddr.new("::/128"), # Unspecified address (RFC 4291)
|
|
65
|
+
IPAddr.new("::1/128"), # Loopback address (RFC 4291)
|
|
66
|
+
IPAddr.new("fc00::/7"), # Unique local address (ULA) (RFC 4193
|
|
67
|
+
IPAddr.new("fe80::/10"), # Link-local address (LLA) (RFC 4291)
|
|
68
|
+
IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
|
|
69
|
+
IPAddr.new("2001:db8::/32"), # Documentation prefix (RFC 3849)
|
|
70
|
+
IPAddr.new("3fff::/20") # Documentation prefix (RFC 9637)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
PRIVATE_RANGES = PRIVATE_IPV4_RANGES + PRIVATE_IPV6_RANGES + PRIVATE_IPV4_RANGES.map(&:ipv4_mapped)
|
|
74
|
+
|
|
75
|
+
def resolved_in_current_context
|
|
76
|
+
context = Aikido::Zen.current_context
|
|
77
|
+
context && context["dns.lookups"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def resolve(hostname_or_address)
|
|
81
|
+
return [] if hostname_or_address.nil?
|
|
82
|
+
|
|
83
|
+
case hostname_or_address
|
|
84
|
+
when Resolv::AddressRegex
|
|
85
|
+
[IPAddr.new(hostname_or_address)]
|
|
86
|
+
when resolved_in_current_context
|
|
87
|
+
resolved_in_current_context[hostname_or_address]
|
|
88
|
+
.map { |address| IPAddr.new(address) }
|
|
89
|
+
else
|
|
90
|
+
@resolver.getaddresses(hostname_or_address.to_s)
|
|
91
|
+
.map { |address| IPAddr.new(address) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|