sqreen 0.8.11465220943 → 1.0.0.pre1480953244
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 +4 -4
- data/lib/sqreen.rb +0 -1
- data/lib/sqreen/binding_accessor.rb +61 -49
- data/lib/sqreen/condition_evaluator.rb +17 -12
- data/lib/sqreen/conditionable.rb +2 -1
- data/lib/sqreen/configuration.rb +2 -0
- data/lib/sqreen/deliveries/batch.rb +2 -2
- data/lib/sqreen/events/attack.rb +1 -1
- data/lib/sqreen/frameworks/generic.rb +54 -15
- data/lib/sqreen/frameworks/rails.rb +1 -21
- data/lib/sqreen/frameworks/sinatra.rb +5 -0
- data/lib/sqreen/instrumentation.rb +7 -10
- data/lib/sqreen/log.rb +1 -0
- data/lib/sqreen/metrics/base.rb +4 -0
- data/lib/sqreen/metrics_store.rb +4 -4
- data/lib/sqreen/remote_command.rb +5 -4
- data/lib/sqreen/rules_callbacks.rb +0 -4
- data/lib/sqreen/rules_callbacks/matcher_rule.rb +8 -6
- data/lib/sqreen/rules_callbacks/reflected_xss.rb +55 -11
- data/lib/sqreen/runner.rb +76 -73
- data/lib/sqreen/runtime_infos.rb +3 -2
- data/lib/sqreen/serializer.rb +46 -0
- data/lib/sqreen/session.rb +22 -12
- data/lib/sqreen/version.rb +1 -1
- metadata +6 -14
- data/lib/sqreen/detect.rb +0 -14
- data/lib/sqreen/detect/shell_injection.rb +0 -61
- data/lib/sqreen/detect/sql_injection.rb +0 -115
- data/lib/sqreen/parsers/sql.rb +0 -98
- data/lib/sqreen/parsers/sql_tokenizer.rb +0 -266
- data/lib/sqreen/parsers/unix.rb +0 -110
- data/lib/sqreen/rules_callbacks/shell.rb +0 -33
- data/lib/sqreen/rules_callbacks/sql.rb +0 -41
- data/lib/sqreen/rules_callbacks/system_shell.rb +0 -25
@@ -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
|
data/lib/sqreen/parsers/unix.rb
DELETED
@@ -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
|