sqreen 0.8.11465220943 → 1.0.0.pre1480953244

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,266 +0,0 @@
1
- # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
- # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
-
4
- # Inspired by
5
- # https://practicingruby.com/articles/parsing-json-the-hard-way
6
-
7
- require 'strscan'
8
- require 'sqreen/exception'
9
-
10
- module Sqreen
11
- module Parsers
12
- class ParsingException < ::Sqreen::Exception
13
- end
14
-
15
- # Forward declaration
16
- class SQLTokenizer; end
17
-
18
- # https://www.sqlite.org/lang_select.html
19
- # https://www.sqlite.org/lang_expr.html
20
- class SQLiteTokenizer < SQLTokenizer
21
- def initialize(_db_infos,
22
- _backslash_escape = false,
23
- _has_hex = false)
24
- @name = 'SQLite'
25
- end
26
- end
27
-
28
- # How string litterals work in MySQL
29
- # https://dev.mysql.com/doc/refman/5.0/en/string-literals.html
30
- # https://dev.mysql.com/doc/refman/5.0/en/number-literals.html
31
- #
32
- # Configuration modifiers:
33
- # sqlmode: The backslash charcter does not perform escapes
34
- # https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ansi_quotes
35
- # sqlmode: No backslash escapes
36
- # https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_backslash_escapes
37
-
38
- # The double quote cannot be a string delimiter
39
- #
40
- # Double quote strangeness:
41
- #
42
- # mysql> SELECT 'hello', '"hello"', '""hello""', 'hel''lo', '\'hello';
43
- # +-------+---------+-----------+--------+--------+
44
- # | hello | "hello" | ""hello"" | hel'lo | 'hello |
45
- # +-------+---------+-----------+--------+--------+
46
- #
47
- # mysql> SELECT "hello", "'hello'", "''hello''", "hel""lo", "\"hello";
48
- # +-------+---------+-----------+--------+--------+
49
- # | hello | 'hello' | ''hello'' | hel"lo | "hello |
50
- # +-------+---------+-----------+--------+--------+
51
- class MySQLTokenizer < SQLTokenizer
52
- def initialize(db_infos,
53
- backslash_escape = true,
54
- has_hex = true)
55
- super
56
- @name = 'MySQL'
57
- end
58
- end
59
-
60
- class SQLTokenizer
61
- NUMBER = /[-+]*(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/
62
- COMMAND = /[a-zA-Z_][a-zA-Z_0-9]*/
63
- HEX = /0x[0-9a-fA-F]+/
64
-
65
- binary_operators_list = %w(\+ - ~ \! <=> <> != <= = >= < > := % NOT OR \|\| AND IS IN BETWEEN LIKE REGEXP)
66
- binary_operators_list << 'SOUNDS LIKE' << 'IS NULL' << 'IS NOT NULL'
67
- BINARY_OPERATORS_LIST = binary_operators_list.freeze
68
-
69
- BITS_OPERATORS_LIST = %w(\| & << >> \+ - \* / DIV MOD % ^).freeze
70
-
71
- BINARY_OPERATORS = Regexp.new(BINARY_OPERATORS_LIST.join('|'))
72
- BITS_OPERATORS = Regexp.new(BITS_OPERATORS_LIST.join('|'))
73
-
74
- SQ = "'".freeze
75
- DQ = '"'.freeze
76
- BQ = '`'.freeze
77
-
78
- attr_reader :ss
79
-
80
- def initialize(db_infos,
81
- backslash_escape = true,
82
- has_hex = true)
83
-
84
- @db_infos = db_infos
85
- @ss = nil
86
- @backslash_escape = backslash_escape
87
- @has_hex = has_hex
88
- end
89
-
90
- def tokenize(str)
91
- @ss = StringScanner.new(str)
92
- end
93
-
94
- def each_token
95
- while token = next_token
96
- type = get_type(token[0])
97
- puts token.inspect if $DEBUG
98
- puts 'After: ' + ss.rest if $DEBUG
99
- yield(token, type)
100
- end
101
- end
102
-
103
- # Can raise a ParsingException.
104
- def next_token
105
- @ss.skip(/^[ \t\n]+/)
106
-
107
- return if @ss.eos?
108
-
109
- # FIXME: multiple commands (; separated)
110
- # FIXME endline comments (-- )
111
-
112
- res = if text = @ss.scan(COMMAND)
113
- [:COMMAND, text]
114
- elsif @has_hex && text = @ss.scan(HEX)
115
- [:HEX_STRING, text]
116
- elsif text = @ss.scan(NUMBER)
117
- [:NUMBER, text]
118
- elsif text = extract_string(SQ)
119
- [:SINGLE_QUOTED_STRING, SQ + text + SQ]
120
- elsif text = extract_string(DQ)
121
- [:DOUBLE_QUOTED_STRING, DQ + text + DQ]
122
- elsif text = extract_string(BQ)
123
- [:BACK_QUOTED_STRING, BQ + text + BQ]
124
- elsif @ss.peek(1) == '*' && @ss.pos += 1
125
- [:ASTERISK, '*']
126
- elsif @ss.peek(3) == '/*!' && @ss.pos += 3 and text = @ss.search_full(/\*\//, true, true)
127
- [:INLINE_COMMENT, '/*!' + text]
128
- elsif @ss.peek(2) == '/*' && @ss.pos += 2 and text = @ss.search_full(/\*\//, true, true)
129
- [:INLINE_COMMENT, '/*' + text]
130
- elsif text = @ss.scan(/\)/)
131
- [:PAR_CLOSE, text]
132
- elsif text = @ss.scan(/\(/)
133
- [:PAR_OPEN, text]
134
- elsif text = @ss.scan(/,/)
135
- [:COMA, text]
136
- elsif @ss.peek(1) == '?' && @ss.pos += 1
137
- [:QUESTION_MARK, '?']
138
- elsif text = @ss.scan(/;/)
139
- [:QUERY_END, text]
140
- elsif @ss.peek(1) == ':' && @ss.pos += 1
141
- [:LABEL_MARK, ':']
142
- elsif @ss.peek(1) == '.' && @ss.pos += 1
143
- [:DOT, '.']
144
- elsif text = @ss.scan(BINARY_OPERATORS)
145
- [:BINARY_OPERATOR, text]
146
- # elsif text = @ss.scan(BITS_OPERATORS) then
147
- # [:BITS_OPERATOR, text]
148
- else
149
- pos = @ss.pos
150
- @ss.reset
151
- request = @ss.rest
152
- @ss.pos = pos
153
- raise ParsingException, format('could not find anything %s', request.inspect)
154
- end
155
-
156
- res
157
- end
158
-
159
- TYPES_MAPPING = {
160
- :literal_string => [
161
- :SINGLE_QUOTED_STRING,
162
- :DOUBLE_QUOTED_STRING,
163
- :HEX_STRING,
164
- ],
165
- :literal_number => [
166
- :NUMBER,
167
- ],
168
- :object => [
169
- :BACK_QUOTED_STRING,
170
- ],
171
- :command => [
172
- :COMMAND,
173
- ],
174
- }.freeze
175
-
176
- INVERT_TYPE_MAPPING = TYPES_MAPPING.inject({}) do |new, types_h|
177
- meta_type, types = types_h
178
- for type in types
179
- new[type] = meta_type
180
- end
181
- new
182
- end
183
-
184
- def get_type(token_type)
185
- INVERT_TYPE_MAPPING[token_type]
186
- end
187
-
188
- def extract_string(delim)
189
- return unless @ss.peek(1) == delim
190
- @ss.pos += 1
191
- find_string(delim)
192
- end
193
-
194
- def find_dq_str
195
- find_string('"')
196
- end
197
-
198
- def find_sq_str(_string_scanner)
199
- find_string("'")
200
- end
201
-
202
- def find_string(delim)
203
- remain = @ss.rest
204
- end_idx = find_impair_delim_pos(remain, delim)
205
- raise ParsingException, format('no end for string %s', remain.inspect) if end_idx.nil?
206
-
207
- # So we point on the last internal char of string
208
- res = remain[0...end_idx]
209
-
210
- # Ruby 1.9.3: String.bytes = Enumerator has no size method
211
- bytes_size = 0
212
- res.bytes.each { bytes_size += 1 }
213
-
214
- # Remain is a unicode string. @ss the StringScanner works with
215
- # bytes, so we need to move it forward by numbre of bytes in remain.
216
- @ss.pos += bytes_size + 1
217
- res
218
- end
219
-
220
- # MySQL string exctraction is a pain since:
221
- # - a string may hold antislashes, which escapes the next char
222
- # - a string may hold their delimiter (' or ") twice: '' or "", which
223
- # translates \' or \"
224
- # Hence we browse the string. We skip \*, and if we find delimiters, we
225
- # count how many there are.
226
- # - If its an odd number, (1, 3...) this is the end of the string.
227
- # - Else, they have all been escaped, we can ignore them.
228
- def find_impair_delim_pos(str, delim)
229
- skip = false
230
- first = nil
231
- str.chars.each_with_index do |char, i|
232
- if skip
233
- skip = false
234
- next
235
- end
236
- if @backslash_escape && char == '\\'
237
- # if we have an escape, let's skip it
238
- skip = true
239
- next
240
- end
241
- if char == delim
242
- first = i unless first
243
- else
244
- if first
245
- # first time we reach the end of a `delim` sequence
246
- nb = i - first
247
- if nb.odd?
248
- # we had an odd number of delim
249
- return i - 1
250
- else
251
- # we had an even number of delim, keep searching
252
- first = nil
253
- end
254
- end
255
- end
256
- end
257
- i = str.size - 1
258
- if first
259
- nb = i - first + 1
260
- return i if nb.odd?
261
- end
262
- nil
263
- end
264
- end
265
- end
266
- end
@@ -1,110 +0,0 @@
1
- # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
- # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
-
4
- require 'strscan'
5
- require 'sqreen/exception'
6
-
7
- module Sqreen
8
- module Parsers
9
- # A command fragment
10
- class UnixAtom
11
- attr_accessor :kind, :val, :start, :end
12
-
13
- def initialize(kind, value, start)
14
- self.kind = kind
15
- self.val = value
16
- self.start = start
17
- self.end = start + value.size
18
- end
19
-
20
- def executable?
21
- kind == :exec
22
- end
23
- end
24
-
25
- # A basic Shell command parser
26
- class Unix
27
- attr_reader :atoms
28
-
29
- def initialize
30
- @ifs = ENV.fetch('IFS', " \t\n")
31
- @white = Regexp.new("[#{@ifs}]+")
32
- @nonwhite = Regexp.new("[^#{@ifs}]+")
33
- @control = ';&|'
34
- @nonwhite_and_control = Regexp.new("[^#{@ifs}#{@control}]+")
35
- end
36
-
37
- # Parse a shell command.
38
- #
39
- # fills atoms with parsing results
40
- # @param cmd [String] Command to be parsed
41
- # @return [Boolean] true if successful
42
- def parse(cmd)
43
- @atoms = []
44
- each_token(cmd) do |atom|
45
- @atoms << atom
46
- end
47
-
48
- true
49
- end
50
-
51
- protected
52
-
53
- def each_token(cmd)
54
- scan = StringScanner.new(cmd)
55
-
56
- @first = true
57
- loop do
58
- token = next_token(scan)
59
- break if token.nil?
60
- yield token
61
- end
62
- end
63
-
64
- def next_token(scan)
65
- scan.skip(@white)
66
-
67
- return nil if scan.eos?
68
-
69
- pos = scan.pos
70
- text = scan.scan(Regexp.new("#{@nonwhite}="))
71
- return UnixAtom.new(:param, text + find_word(scan), pos) if text
72
- next_ch = scan.peek(1)
73
- if @control.include?(next_ch)
74
- @first = true
75
- return UnixAtom.new(:control, scan.scan(Regexp.new("[#{@control}]+")), pos)
76
- end
77
- word = find_word(scan)
78
- return nil unless word
79
- if @first
80
- @first = false
81
- return UnixAtom.new(:exec, word, pos)
82
- else
83
- return UnixAtom.new(:field, word, pos)
84
- end
85
- end
86
-
87
- def find_word(scan)
88
- first = scan.peek(1)
89
- return '' if first.match(@white)
90
-
91
- if first == "'"
92
- scan.getch
93
- "'" + scan.scan_until(/'|\z/)
94
- elsif first == '"'
95
- txt = ['"']
96
- scan.getch
97
- until scan.eos?
98
- txt << scan.scan_until(/"|\z/)
99
- escaped = txt[-1].match(/\\+"\z/)
100
- break unless escaped
101
- break if escaped.size.even?
102
- end
103
- txt.join
104
- else
105
- scan.scan(@nonwhite_and_control)
106
- end
107
- end
108
- end
109
- end
110
- end
@@ -1,33 +0,0 @@
1
- # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
- # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
-
4
- require 'sqreen/rule_callback'
5
- require 'sqreen/detect'
6
-
7
- module Sqreen
8
- module Rules
9
- # Look for Shell injections
10
- class ShellCB < RuleCB
11
- def pre(_inst, *args, &_block)
12
- Sqreen.log.debug { "<< #{@klass} #{@method} #{Thread.current}" }
13
- Sqreen.log.debug { args.inspect }
14
-
15
- cmd = args[0]
16
- params = framework.request_params
17
- return if params.nil? || params == {}
18
- Sqreen.log.debug { 'Searching injection in:' }
19
- Sqreen.log.debug { 'command: ' + cmd }
20
- Sqreen.log.debug { 'params: ' + params.inspect }
21
-
22
- # FIXME: Handle IFS coming from spawn/exec/system ENV argument
23
- inj = Sqreen::Detect::ShellInjection.new
24
- shi = inj.user_escape?(cmd, params)
25
- Sqreen.log.warn { "presence of a shell injection: #{shi}" }
26
- return unless shi
27
- infos = { :sh_cmd => cmd }
28
- record_event(infos)
29
- { :status => :raise }
30
- end
31
- end
32
- end
33
- end
@@ -1,41 +0,0 @@
1
- # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
- # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
-
4
- require 'sqreen/rule_callback'
5
- require 'sqreen/detect'
6
-
7
- module Sqreen
8
- module Rules
9
- # Look for SQL injections
10
- class SQLCB < RuleCB
11
- def pre(inst, *args, &_block)
12
- Sqreen.log.debug { "<< #{@klass} #{@method} #{Thread.current}" }
13
- Sqreen.log.debug { args.inspect }
14
-
15
- request = args[0]
16
- params = framework.request_params
17
- return if params.nil? || params == {}
18
- Sqreen.log.debug { 'Searching injection in:' }
19
- Sqreen.log.debug { 'request: ' + request }
20
- Sqreen.log.debug { 'params: ' + params.inspect }
21
-
22
- db_type, db_infos = framework.db_settings(:connection_adapter => inst)
23
- if db_type.nil?
24
- Sqreen.log.debug { "Database '#{db_infos[:name]}' not supported yet" }
25
- return
26
- end
27
- inj = Sqreen::Detect::SQLInjection.new(db_type, db_infos)
28
- sqli = inj.user_escape?(request, params)
29
- Sqreen.log.info { "presence of an SQLi: #{sqli}" }
30
- return unless sqli
31
- infos = {
32
- :db_request => request,
33
- :db_type => db_type,
34
- :db_infos => db_infos,
35
- }
36
- record_event(infos)
37
- { :status => :raise }
38
- end
39
- end
40
- end
41
- end