Module: Aikido::Zen::Scanners::ShellInjection::Helpers

Defined in:
lib/aikido/zen/scanners/shell_injection/helpers.rb

Constant Summary collapse

ESCAPE_CHARS =
%W[' "]
DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES =
%W[$ ` \\ !]
DANGEROUS_CHARS =
[
  "#", "!", '"', "$", "&", "'", "(", ")", "*", ";", "<", "=", ">", "?",
  "[", "\\", "]", "^", "`", "{", "|", "}", " ", "\n", "\t", "~"
]
COMMANDS =
%w[sleep shutdown reboot poweroff halt ifconfig chmod chown ping
ssh scp curl wget telnet kill killall rm mv cp touch echo cat head
tail grep find awk sed sort uniq wc ls env ps who whoami id w df du
pwd uname hostname netstat passwd arch printenv logname pstree hostnamectl
set lsattr killall5 dmesg history free uptime finger top shopt :]
PATH_PREFIXES =
%w[/bin/ /sbin/ /usr/bin/ /usr/sbin/ /usr/local/bin/ /usr/local/sbin/]
SEPARATORS =
[" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">"]
COMMANDS_REGEX =

Construct the regex for commands

Regexp.new(
  "([/.]*((#{PATH_PREFIXES.map { |p| Helpers.escape_string_regexp(p) }.join("|")})?((#{COMMANDS.sort(&method(:by_length)).join("|")}))))",
  Regexp::IGNORECASE
)

Class Method Summary collapse

Class Method Details

.by_length(a, b) ⇒ Object

Helper function for sorting commands by length (longer commands first)



75
76
77
# File 'lib/aikido/zen/scanners/shell_injection/helpers.rb', line 75

def self.by_length(a, b)
  b.length - a.length
end

.contains_shell_syntax(command, user_input) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/aikido/zen/scanners/shell_injection/helpers.rb', line 94

def self.contains_shell_syntax(command, user_input)
  # Check if input is only whitespace
  return false if user_input.strip.empty?

  # Check if the user input contains any dangerous characters
  if DANGEROUS_CHARS.any? { |c| user_input.include?(c) }
    return true
  end

  # If the command is exactly the same as the user input, check if it matches the regex
  if command == user_input
    return match_all(command, COMMANDS_REGEX).any? do |match|
      match[:match].length == command.length && match[:match] == command
    end
  end

  # Check if the command contains a commonly used command
  match_all(command, COMMANDS_REGEX).each do |match|
    # We found a command like `rm` or `/sbin/shutdown` in the command
    # Check if the command is the same as the user input
    # If it's not the same, continue searching
    next if user_input != match[:match]

    # Otherwise, we'll check if the command is surrounded by separators
    # These separators are used to separate commands and arguments
    # e.g. `rm<space>-rf`
    # e.g. `ls<newline>whoami`
    # e.g. `echo<tab>hello` Check if the command is surrounded by separators
    char_before = if match[:index] - 1 < 0
      nil
    else
      command[match[:index] - 1]
    end

    char_after = if match[:index] + match[:match].length >= command.length
      nil
    else
      command[match[:index] + match[:match].length]
    end

    # e.g. `<separator>rm<separator>`
    if SEPARATORS.include?(char_before) && SEPARATORS.include?(char_after)
      return true
    end

    # e.g. `<separator>rm`
    if SEPARATORS.include?(char_before) && char_after.nil?
      return true
    end

    # e.g. `rm<separator>`
    if char_before.nil? && SEPARATORS.include?(char_after)
      return true
    end
  end

  false
end

.escape_string_regexp(string) ⇒ Object

Escape characters with special meaning either inside or outside character sets. Use a simple backslash escape when it’s always valid, and a ‘xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.

Inspired by github.com/sindresorhus/escape-string-regexp/



84
85
86
# File 'lib/aikido/zen/scanners/shell_injection/helpers.rb', line 84

def self.escape_string_regexp(string)
  string.gsub(/[|\\{}()\[\]^$+*?.]/) { "\\#{$&}" }.gsub("-", '\\x2d')
end

.get_current_and_next_segments(segments) ⇒ Object



70
71
72
# File 'lib/aikido/zen/scanners/shell_injection/helpers.rb', line 70

def self.get_current_and_next_segments(segments)
  segments.each_cons(2).map { |current_segment, next_segment| {current_segment: current_segment, next_segment: next_segment} }
end

.is_safely_encapsulated(command, user_input) ⇒ Object

Parameters:

  • command (string)
  • user_input (string)


24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/aikido/zen/scanners/shell_injection/helpers.rb', line 24

def self.is_safely_encapsulated(command, user_input)
  segments = command.split(user_input)

  # The next condition is merely here to be compliant with what javascript does when splitting strings:
  # From js doc https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
  #   > If separator appears at the beginning (or end) of the string, it still has the effect of splitting,
  #   > resulting in an empty (i.e. zero length) string appearing at the first (or last) position of
  #   > the returned array.
  # This is necessary because this code is ported form the firewall-node code.
  if user_input.length > 1
    if command.start_with? user_input
      segments.unshift ""
    end

    if command.end_with? user_input
      segments << ""
    end
  end

  # Call the helper function to get current and next segments
  get_current_and_next_segments(segments).all? do |segments_pair|
    char_before_user_input = segments_pair[:current_segment][-1]
    char_after_user_input = segments_pair[:next_segment][0]

    # Check if the character before is an escape character
    is_escape_char = ESCAPE_CHARS.include?(char_before_user_input)

    unless is_escape_char
      next false
    end

    # If characters before and after the user input do not match, return false
    next false if char_before_user_input != char_after_user_input

    # If user input contains the escape character, return false
    next false if user_input.include?(char_before_user_input)

    # Handle dangerous characters inside double quotes
    if char_before_user_input == '"' && DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES.any? { |char| user_input.include?(char) }
      next false
    end

    next true
  end
end

.match_all(string, regex) ⇒ Object



153
154
155
156
157
# File 'lib/aikido/zen/scanners/shell_injection/helpers.rb', line 153

def self.match_all(string, regex)
  string.enum_for(:scan, regex).map do |match|
    {match: match[0], index: $~.begin(0)}
  end
end