ronin-vulns 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.github/workflows/ruby.yml +31 -0
- data/.gitignore +13 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/COPYING.txt +165 -0
- data/ChangeLog.md +22 -0
- data/Gemfile +34 -0
- data/README.md +328 -0
- data/Rakefile +34 -0
- data/bin/ronin-vulns +19 -0
- data/data/rfi_test.asp +21 -0
- data/data/rfi_test.aspx +25 -0
- data/data/rfi_test.cfm +27 -0
- data/data/rfi_test.jsp +19 -0
- data/data/rfi_test.php +24 -0
- data/data/rfi_test.pl +25 -0
- data/gemspec.yml +41 -0
- data/lib/ronin/vulns/cli/command.rb +39 -0
- data/lib/ronin/vulns/cli/commands/lfi.rb +145 -0
- data/lib/ronin/vulns/cli/commands/open_redirect.rb +119 -0
- data/lib/ronin/vulns/cli/commands/reflected_xss.rb +99 -0
- data/lib/ronin/vulns/cli/commands/rfi.rb +156 -0
- data/lib/ronin/vulns/cli/commands/scan.rb +316 -0
- data/lib/ronin/vulns/cli/commands/sqli.rb +133 -0
- data/lib/ronin/vulns/cli/commands/ssti.rb +126 -0
- data/lib/ronin/vulns/cli/logging.rb +78 -0
- data/lib/ronin/vulns/cli/web_vuln_command.rb +347 -0
- data/lib/ronin/vulns/cli.rb +45 -0
- data/lib/ronin/vulns/lfi/test_file.rb +91 -0
- data/lib/ronin/vulns/lfi.rb +266 -0
- data/lib/ronin/vulns/open_redirect.rb +118 -0
- data/lib/ronin/vulns/reflected_xss/context.rb +224 -0
- data/lib/ronin/vulns/reflected_xss/test_string.rb +149 -0
- data/lib/ronin/vulns/reflected_xss.rb +184 -0
- data/lib/ronin/vulns/rfi.rb +224 -0
- data/lib/ronin/vulns/root.rb +28 -0
- data/lib/ronin/vulns/sqli/error_pattern.rb +89 -0
- data/lib/ronin/vulns/sqli.rb +397 -0
- data/lib/ronin/vulns/ssti/test_expression.rb +104 -0
- data/lib/ronin/vulns/ssti.rb +203 -0
- data/lib/ronin/vulns/url_scanner.rb +218 -0
- data/lib/ronin/vulns/version.rb +26 -0
- data/lib/ronin/vulns/vuln.rb +49 -0
- data/lib/ronin/vulns/web_vuln/http_request.rb +223 -0
- data/lib/ronin/vulns/web_vuln.rb +774 -0
- data/man/ronin-vulns-lfi.1 +107 -0
- data/man/ronin-vulns-lfi.1.md +80 -0
- data/man/ronin-vulns-open-redirect.1 +98 -0
- data/man/ronin-vulns-open-redirect.1.md +73 -0
- data/man/ronin-vulns-reflected-xss.1 +95 -0
- data/man/ronin-vulns-reflected-xss.1.md +71 -0
- data/man/ronin-vulns-rfi.1 +107 -0
- data/man/ronin-vulns-rfi.1.md +80 -0
- data/man/ronin-vulns-scan.1 +138 -0
- data/man/ronin-vulns-scan.1.md +103 -0
- data/man/ronin-vulns-sqli.1 +107 -0
- data/man/ronin-vulns-sqli.1.md +80 -0
- data/man/ronin-vulns-ssti.1 +99 -0
- data/man/ronin-vulns-ssti.1.md +74 -0
- data/ronin-vulns.gemspec +60 -0
- metadata +161 -0
@@ -0,0 +1,397 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# ronin-vulns - A Ruby library for blind vulnerability testing.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
|
7
|
+
#
|
8
|
+
# ronin-vulns is free software: you can redistribute it and/or modify
|
9
|
+
# it under the terms of the GNU Lesser General Public License as published
|
10
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
11
|
+
# (at your option) any later version.
|
12
|
+
#
|
13
|
+
# ronin-vulns is distributed in the hope that it will be useful,
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
# GNU Lesser General Public License for more details.
|
17
|
+
#
|
18
|
+
# You should have received a copy of the GNU Lesser General Public License
|
19
|
+
# along with ronin-vulns. If not, see <https://www.gnu.org/licenses/>.
|
20
|
+
#
|
21
|
+
|
22
|
+
require 'ronin/vulns/web_vuln'
|
23
|
+
require 'ronin/vulns/sqli/error_pattern'
|
24
|
+
|
25
|
+
require 'time'
|
26
|
+
|
27
|
+
module Ronin
|
28
|
+
module Vulns
|
29
|
+
#
|
30
|
+
# Represents a SQL injection vulnerability.
|
31
|
+
#
|
32
|
+
# ## Features
|
33
|
+
#
|
34
|
+
# * Supports testing ` OR 1=1` and ` AND 1=0`.
|
35
|
+
# * Supports testing SQL sleep functions.
|
36
|
+
#
|
37
|
+
class SQLI < WebVuln
|
38
|
+
|
39
|
+
# Specifies whether to escape a quoted string value.
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
attr_reader :escape_quote
|
43
|
+
|
44
|
+
# Specifies whether to escape parenthesis.
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
attr_reader :escape_parens
|
48
|
+
|
49
|
+
# Specifies whether to terminate the SQL statement with `--`.
|
50
|
+
#
|
51
|
+
# @return [Boolean]
|
52
|
+
attr_reader :terminate
|
53
|
+
|
54
|
+
#
|
55
|
+
# Initializes the SQL injection vulnerability.
|
56
|
+
#
|
57
|
+
# @param [URI::HTTP, String] url
|
58
|
+
# The URL to test or exploit.
|
59
|
+
#
|
60
|
+
# @param [Boolean] escape_quote
|
61
|
+
# Specifies whether to escape a quoted string value.
|
62
|
+
#
|
63
|
+
# @param [Boolean] escape_parens
|
64
|
+
# Specifies whether to escape parenthesis.
|
65
|
+
#
|
66
|
+
# @param [Boolean] terminate
|
67
|
+
# Specifies whether to terminate the SQL statement with `--`.
|
68
|
+
#
|
69
|
+
def initialize(url, escape_quote: false,
|
70
|
+
escape_parens: false,
|
71
|
+
terminate: false,
|
72
|
+
**kwargs)
|
73
|
+
super(url,**kwargs)
|
74
|
+
|
75
|
+
@escape_quote = escape_quote
|
76
|
+
@escape_parens = escape_parens
|
77
|
+
@terminate = terminate
|
78
|
+
|
79
|
+
@escape_string = build_escape_string
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
#
|
85
|
+
# Builds the SQL escape String.
|
86
|
+
#
|
87
|
+
# @return [String]
|
88
|
+
#
|
89
|
+
def build_escape_string
|
90
|
+
if @escape_quote && @escape_parens
|
91
|
+
"#{original_value}')"
|
92
|
+
elsif @escape_quote
|
93
|
+
"#{original_value}'"
|
94
|
+
elsif @escape_parens
|
95
|
+
"#{original_value})"
|
96
|
+
else
|
97
|
+
original_value
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
public
|
102
|
+
|
103
|
+
#
|
104
|
+
# Scans the URL for SQL injections.
|
105
|
+
#
|
106
|
+
# @param [URI::HTTP, String] url
|
107
|
+
# The URL to test or exploit.
|
108
|
+
#
|
109
|
+
# @param [Ronin::Support::Network::HTTP, nil] http
|
110
|
+
# An HTTP session to use for testing the URL.
|
111
|
+
#
|
112
|
+
# @param [Hash{Symbol => Object}] kwargs
|
113
|
+
# Additional keyword arguments for {WebVuln.scan}.
|
114
|
+
#
|
115
|
+
# @yield [sqli]
|
116
|
+
# If a block is given it will be yielded each discovered SQL injection
|
117
|
+
# vulnerability.
|
118
|
+
#
|
119
|
+
# @yieldparam [SQLi] sqli
|
120
|
+
# A discovered SQL injection vulnerability in the URL.
|
121
|
+
#
|
122
|
+
# @return [Array<SQLi>]
|
123
|
+
# All discovered SQL injection vulnerabilities.
|
124
|
+
#
|
125
|
+
def self.scan(url, http: nil, **kwargs, &block)
|
126
|
+
url = URI(url)
|
127
|
+
http ||= Support::Network::HTTP.connect_uri(url)
|
128
|
+
|
129
|
+
escape_quotes = [false, true]
|
130
|
+
escape_parens = [false, true]
|
131
|
+
terminations = [false, true]
|
132
|
+
|
133
|
+
vulns = []
|
134
|
+
|
135
|
+
escape_quotes.each do |escape_quote|
|
136
|
+
escape_parens.each do |escape_paren|
|
137
|
+
terminations.each do |terminate|
|
138
|
+
vulns.concat(super(url, escape_quote: escape_quote,
|
139
|
+
escape_parens: escape_paren,
|
140
|
+
terminate: terminate,
|
141
|
+
http: http,
|
142
|
+
**kwargs,
|
143
|
+
&block))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
return vulns
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Escapes the given SQL and turns it into a SQL injection.
|
153
|
+
#
|
154
|
+
# @param [#to_s] sql
|
155
|
+
# The SQL expression to escape.
|
156
|
+
#
|
157
|
+
# @return [String]
|
158
|
+
# The escaped SQL expression.
|
159
|
+
#
|
160
|
+
def escape(sql)
|
161
|
+
sqli = if sql.start_with?(';')
|
162
|
+
"#{@escape_string}#{sql}"
|
163
|
+
else
|
164
|
+
"#{@escape_string} #{sql}"
|
165
|
+
end
|
166
|
+
|
167
|
+
if @terminate
|
168
|
+
sqli << '--'
|
169
|
+
else
|
170
|
+
sqli.chop! if (@escape_parens && sqli.end_with?(')'))
|
171
|
+
sqli.chop! if (@escape_quote && sqli.end_with?("'"))
|
172
|
+
end
|
173
|
+
|
174
|
+
return sqli
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Encodes the SQL payload.
|
179
|
+
#
|
180
|
+
# @see #escape
|
181
|
+
#
|
182
|
+
def encode_payload(sql)
|
183
|
+
escape(sql)
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# Tests whether the URL is vulnerable to SQL injection.
|
188
|
+
#
|
189
|
+
# @return [Boolean]
|
190
|
+
#
|
191
|
+
def vulnerable?
|
192
|
+
test_or_true_and_false || test_sleep
|
193
|
+
end
|
194
|
+
|
195
|
+
# SQL error message patterns for various databases.
|
196
|
+
ERROR_PATTERNS = {
|
197
|
+
postgresql: ErrorPattern[
|
198
|
+
/PostgreSQL.*ERROR/,
|
199
|
+
/Warning.*\Wpg_/,
|
200
|
+
/valid PostgreSQL result/,
|
201
|
+
/Npgsql\./,
|
202
|
+
/PG::SyntaxError:/,
|
203
|
+
/org\.postgresql\.util\.PSQLException/,
|
204
|
+
/ERROR:\s\ssyntax error at or near/,
|
205
|
+
/ERROR: parser: parse error at or near/,
|
206
|
+
/PostgreSQL query failed/,
|
207
|
+
/org\.postgresql\.jdbc/,
|
208
|
+
/Pdo[.\/_\\]Pgsql/,
|
209
|
+
/PSQLException/
|
210
|
+
],
|
211
|
+
|
212
|
+
mysql: ErrorPattern[
|
213
|
+
/SQL syntax.*MySQL/,
|
214
|
+
/Warning.*\Wmysqli?_/,
|
215
|
+
/MySQLSyntaxErrorException/,
|
216
|
+
/valid MySQL result/,
|
217
|
+
/check the manual that corresponds to your (MySQL|MariaDB) server version/,
|
218
|
+
/Unknown column '[^ ]+' in 'field list'/,
|
219
|
+
/MySqlClient\./,
|
220
|
+
/com\.mysql\.jdbc/,
|
221
|
+
/Zend_Db_(?:Adapter|Statement)_Mysqli_Exception/,
|
222
|
+
/Pdo[.\/_\\]Mysql/,
|
223
|
+
/MySqlException/
|
224
|
+
],
|
225
|
+
|
226
|
+
sqlite: ErrorPattern[
|
227
|
+
/SQLite\/JDBCDriver/,
|
228
|
+
/SQLite\.Exception/,
|
229
|
+
/(Microsoft|System)\.Data\.SQLite\.SQLiteException/,
|
230
|
+
/Warning.*\W(?:sqlite_|SQLite3::)/,
|
231
|
+
/\[SQLITE_ERROR\]/,
|
232
|
+
/SQLite error \d+:/,
|
233
|
+
/sqlite3\.OperationalError:/,
|
234
|
+
/SQLite3::SQLException/,
|
235
|
+
/org\.sqlite\.JDBC/,
|
236
|
+
/Pdo[.\/_\\]Sqlite/,
|
237
|
+
/SQLiteException/
|
238
|
+
],
|
239
|
+
|
240
|
+
mssql: ErrorPattern[
|
241
|
+
/Driver.* SQL[\-\_\ ]*Server/,
|
242
|
+
/OLE DB.* SQL Server/,
|
243
|
+
/\bSQL Server[^<"]+Driver/,
|
244
|
+
/Warning.*\W(?:mssql|sqlsrv)_/,
|
245
|
+
/\bSQL Server[^<"]+[0-9a-fA-F]{8}/,
|
246
|
+
/System\.Data\.SqlClient\.SqlException/,
|
247
|
+
/Exception.*\bRoadhouse\.Cms\./m,
|
248
|
+
/Microsoft SQL Native Client error '[0-9a-fA-F]{8}/,
|
249
|
+
/\[SQL Server\]/,
|
250
|
+
/ODBC SQL Server Driver/,
|
251
|
+
/ODBC Driver \d+ for SQL Server/,
|
252
|
+
/SQLServer JDBC Driver/,
|
253
|
+
/com\.jnetdirect\.jsql/,
|
254
|
+
/macromedia\.jdbc\.sqlserver/,
|
255
|
+
/Zend_Db_(?:Adapter|Statement)_Sqlsrv_Exception/,
|
256
|
+
/com\.microsoft\.sqlserver\.jdbc/,
|
257
|
+
/Pdo[.\/_\\](?:Mssql|SqlSrv)/,
|
258
|
+
/SQL(?:Srv|Server)Exception/
|
259
|
+
],
|
260
|
+
|
261
|
+
oracle: ErrorPattern[
|
262
|
+
/\bORA-\d{5}/,
|
263
|
+
/Oracle error/,
|
264
|
+
/Oracle.*Driver/,
|
265
|
+
/Warning.*\W(?:oci|ora)_/,
|
266
|
+
/quoted string not properly terminated/,
|
267
|
+
/SQL command not properly ended/,
|
268
|
+
/macromedia\.jdbc\.oracle/,
|
269
|
+
/oracle\.jdbc/,
|
270
|
+
/Zend_Db_(?:Adapter|Statement)_Oracle_Exception/,
|
271
|
+
/Pdo[.\/_\\](?:Oracle|OCI)/,
|
272
|
+
/OracleException/
|
273
|
+
]
|
274
|
+
}
|
275
|
+
|
276
|
+
#
|
277
|
+
# Checks if the response contains a SQL error message.
|
278
|
+
#
|
279
|
+
# @param [Net::HTTPResponse] response
|
280
|
+
# The HTTP response object to check.
|
281
|
+
#
|
282
|
+
# @return [Boolean]
|
283
|
+
# Indicates whether the response was a `500` and if the respones body
|
284
|
+
# contained a SQL error message.
|
285
|
+
#
|
286
|
+
def check_for_sql_errors(response)
|
287
|
+
if response.code == '500'
|
288
|
+
ERROR_PATTERNS.each do |database,error_pattern|
|
289
|
+
if error_pattern =~ response.body
|
290
|
+
return true
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
return false
|
296
|
+
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# Returns a random ID.
|
300
|
+
#
|
301
|
+
# @return [Integer]
|
302
|
+
# A four digit ID.
|
303
|
+
#
|
304
|
+
# @api private
|
305
|
+
#
|
306
|
+
def random_id
|
307
|
+
rand(8_999) + 1_000
|
308
|
+
end
|
309
|
+
|
310
|
+
#
|
311
|
+
# Tests whether the URL is vulnerable to SQL injection, using the
|
312
|
+
# ` OR 1=1` vs. ` AND 1=0` technique.
|
313
|
+
#
|
314
|
+
# @return [Boolean]
|
315
|
+
#
|
316
|
+
# @api private
|
317
|
+
#
|
318
|
+
def test_or_true_and_false
|
319
|
+
id = random_id
|
320
|
+
response1 = exploit("OR #{id}=#{id}")
|
321
|
+
response2 = exploit("AND #{random_id}=#{random_id}")
|
322
|
+
|
323
|
+
# check for SQL errors in both responses
|
324
|
+
if check_for_sql_errors(response1) || check_for_sql_errors(response2)
|
325
|
+
return true
|
326
|
+
end
|
327
|
+
|
328
|
+
if response1.code =~ /^20[0-6]$/ && response2.code =~ /^20[0-6]$/
|
329
|
+
# the first response contained more results than the second response
|
330
|
+
return response1.body.length > response2.body.length
|
331
|
+
elsif response1.code =~ /^20[0-6]$/ && response2.code =~ /^(?:404|500)$/
|
332
|
+
# if the second response return an error, that indicates the
|
333
|
+
# SQL expression evaluated to false and returned no results.
|
334
|
+
return true
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# Various SQL sleep functions or statements.
|
339
|
+
#
|
340
|
+
# @api private
|
341
|
+
SLEEP_TESTS = [
|
342
|
+
'SLEEP(5)',
|
343
|
+
"PG_SLEEP(5)",
|
344
|
+
"WAITFOR DELAY '0:0:5'"
|
345
|
+
]
|
346
|
+
|
347
|
+
#
|
348
|
+
# Tests whether the URL is vulnerable to SQL injection, by calling SQL
|
349
|
+
# sleep functions to see if it takes longer for the response to be
|
350
|
+
# returned.
|
351
|
+
#
|
352
|
+
# @return [Boolean]
|
353
|
+
#
|
354
|
+
# @api private
|
355
|
+
#
|
356
|
+
def test_sleep
|
357
|
+
SLEEP_TESTS.each do |sql|
|
358
|
+
[sql, ";SELECT #{sql}"].each do |sqli|
|
359
|
+
start_time = Time.now
|
360
|
+
response = exploit(sqli)
|
361
|
+
stop_time = Time.now
|
362
|
+
delta = (stop_time - start_time)
|
363
|
+
|
364
|
+
# check for SQL errors first
|
365
|
+
if check_for_sql_errors(response)
|
366
|
+
return true
|
367
|
+
end
|
368
|
+
|
369
|
+
# if the response took more than 5 seconds, our SQL sleep function
|
370
|
+
# probably worked.
|
371
|
+
return true if delta > 5.0
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
return false
|
376
|
+
end
|
377
|
+
|
378
|
+
#
|
379
|
+
# Returns the type or kind of vulnerability.
|
380
|
+
#
|
381
|
+
# @return [Symbol]
|
382
|
+
#
|
383
|
+
# @note
|
384
|
+
# This is used internally to map an vulnerability class to a printable
|
385
|
+
# type.
|
386
|
+
#
|
387
|
+
# @api private
|
388
|
+
#
|
389
|
+
# @abstract
|
390
|
+
#
|
391
|
+
def self.vuln_type
|
392
|
+
:sqli
|
393
|
+
end
|
394
|
+
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# ronin-vulns - A Ruby library for blind vulnerability testing.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
|
6
|
+
#
|
7
|
+
# ronin-vulns is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published
|
9
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# ronin-vulns is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU Lesser General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with ronin-vulns. If not, see <https://www.gnu.org/licenses/>.
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'ronin/vulns/web_vuln'
|
22
|
+
|
23
|
+
module Ronin
|
24
|
+
module Vulns
|
25
|
+
#
|
26
|
+
# Represents a Server Side Template Injection (SSTI) vulnerability.
|
27
|
+
#
|
28
|
+
class SSTI < WebVuln
|
29
|
+
#
|
30
|
+
# Represents a expression to test SSTI with (ex: `7*7`).
|
31
|
+
#
|
32
|
+
class TestExpression
|
33
|
+
|
34
|
+
# The expression string.
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
attr_reader :string
|
38
|
+
|
39
|
+
# The expected result of the string.
|
40
|
+
#
|
41
|
+
# @return [String]
|
42
|
+
attr_reader :result
|
43
|
+
|
44
|
+
#
|
45
|
+
# Initializes the test expression.
|
46
|
+
#
|
47
|
+
# @param [String] string
|
48
|
+
# The expression string.
|
49
|
+
#
|
50
|
+
# @param [String] result
|
51
|
+
# The expected result of the expression.
|
52
|
+
#
|
53
|
+
def initialize(string,result)
|
54
|
+
@string = string
|
55
|
+
@result = result
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Parses an expression string and calculates the result.
|
60
|
+
#
|
61
|
+
# @param [String] string
|
62
|
+
# The expression string to parse.
|
63
|
+
#
|
64
|
+
# @return [TestExpression]
|
65
|
+
# The parsed test expression.
|
66
|
+
#
|
67
|
+
# @raise [ArgumentError]
|
68
|
+
# Could not parse the test expression.
|
69
|
+
#
|
70
|
+
def self.parse(string)
|
71
|
+
unless (match = string.match(/\A(\d+)\s*([\*\/\+\-])\s*(\d+)\z/))
|
72
|
+
raise(ArgumentError,"could not parse the expression: #{string.inspect}")
|
73
|
+
end
|
74
|
+
|
75
|
+
lvalue = match[1].to_i
|
76
|
+
op = match[2]
|
77
|
+
rvalue = match[3].to_i
|
78
|
+
|
79
|
+
result = case op
|
80
|
+
when '*' then lvalue * rvalue
|
81
|
+
when '/' then lvalue / rvalue
|
82
|
+
when '+' then lvalue + rvalue
|
83
|
+
when '-' then lvalue - rvalue
|
84
|
+
else
|
85
|
+
raise(NotImplementedError,"unsupported expression operator: #{op.inspect}")
|
86
|
+
end
|
87
|
+
|
88
|
+
return new(string,result.to_s)
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# The test expression as a String.
|
93
|
+
#
|
94
|
+
# @return [String]
|
95
|
+
# The {#string} value.
|
96
|
+
#
|
97
|
+
def to_s
|
98
|
+
@string
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# ronin-vulns - A Ruby library for blind vulnerability testing.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
|
6
|
+
#
|
7
|
+
# ronin-vulns is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published
|
9
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# ronin-vulns is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU Lesser General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with ronin-vulns. If not, see <https://www.gnu.org/licenses/>.
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'ronin/vulns/web_vuln'
|
22
|
+
require 'ronin/vulns/ssti/test_expression'
|
23
|
+
|
24
|
+
module Ronin
|
25
|
+
module Vulns
|
26
|
+
#
|
27
|
+
# Represents a Server Side Template Injection (SSTI) vulnerability.
|
28
|
+
#
|
29
|
+
class SSTI < WebVuln
|
30
|
+
|
31
|
+
# List of common Server Side Template Injection (SSTI) escapes.
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
ESCAPES = [
|
35
|
+
nil, # does not escape the expression
|
36
|
+
->(expression) { "{{#{expression}}}" },
|
37
|
+
->(expression) { "${#{expression}}" },
|
38
|
+
->(expression) { "${{#{expression}}}" },
|
39
|
+
->(expression) { "\#{#{expression}}" },
|
40
|
+
->(expression) { "<%= #{expression} %>" }
|
41
|
+
]
|
42
|
+
|
43
|
+
# How to escape the payload so that it's executed.
|
44
|
+
#
|
45
|
+
# @return [Proc, nil]
|
46
|
+
# The proc that will accept a String and return a String, or `nil` to
|
47
|
+
# indicate that the payload will not be escaped.
|
48
|
+
attr_reader :escape
|
49
|
+
|
50
|
+
# The test expression to use when testing the URL for SSTI.
|
51
|
+
#
|
52
|
+
# @return [TestExpression]
|
53
|
+
attr_reader :test_expr
|
54
|
+
|
55
|
+
#
|
56
|
+
# Initializes the Server Side Template Injection (SSTI) vulnerability.
|
57
|
+
#
|
58
|
+
# @param [String, URI::HTTP] url
|
59
|
+
# The URL to exploit.
|
60
|
+
#
|
61
|
+
# @param [Proc, nil] escape
|
62
|
+
# How to escape a given payload. Either a proc that will accept a String
|
63
|
+
# and return a String, or `nil` to indicate that the payload will not
|
64
|
+
# be escaped.
|
65
|
+
#
|
66
|
+
# @param [TestExpression] test_expr
|
67
|
+
# The test payload and expected result to check for when testing the URL
|
68
|
+
# for SSTI.
|
69
|
+
#
|
70
|
+
def initialize(url, escape: nil,
|
71
|
+
test_expr: self.class.random_test,
|
72
|
+
**kwargs)
|
73
|
+
super(url,**kwargs)
|
74
|
+
|
75
|
+
@escape = escape
|
76
|
+
@test_expr = test_expr
|
77
|
+
|
78
|
+
unless @test_expr
|
79
|
+
raise(ArgumentError,"must specify both a test expression")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Generates a random `N*M` SSTI test.
|
85
|
+
#
|
86
|
+
# @return [TestExpression]
|
87
|
+
# A random test expression.
|
88
|
+
#
|
89
|
+
def self.random_test
|
90
|
+
int1 = rand(999) + 1_000
|
91
|
+
int2 = rand(999) + 1_000
|
92
|
+
|
93
|
+
string = "#{int1}*#{int2}"
|
94
|
+
result = (int1 * int2).to_s
|
95
|
+
|
96
|
+
return TestExpression.new(string,result)
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Scans the URL for Server Side Template Injection (SSTI) vulnerabilities.
|
101
|
+
#
|
102
|
+
# @param [URI::HTTP, String] url
|
103
|
+
# The URL to scan.
|
104
|
+
#
|
105
|
+
# @param [Hash{Symbol => Object}] kwargs
|
106
|
+
# Additional keyword arguments for {#initialize}.
|
107
|
+
#
|
108
|
+
# @option kwargs [Proc, nil] :escape
|
109
|
+
# The escape method to use. If `escape:` is not given, then all escapes
|
110
|
+
# in {ESCAPES} will be tested..
|
111
|
+
#
|
112
|
+
# @option kwargs [Array<Symbol, String>, Symbol, String, true, nil] :query_params
|
113
|
+
# The query param name(s) to test.
|
114
|
+
#
|
115
|
+
# @option kwargs [Array<Symbol, String>, Symbol, String, nil] :header_names
|
116
|
+
# The header name(s) to test.
|
117
|
+
#
|
118
|
+
# @option kwargs [Array<Symbol, String>, Symbol, String, true, nil] :cookie_params
|
119
|
+
# The cookie param name(s) to test.
|
120
|
+
#
|
121
|
+
# @option kwargs [Array<Symbol, String>, Symbol, String, nil] :form_params
|
122
|
+
# The form param name(s) to test.
|
123
|
+
#
|
124
|
+
# @option kwargs [Ronin::Support::Network::HTTP, nil] :http
|
125
|
+
# An HTTP session to use for testing the LFI.
|
126
|
+
#
|
127
|
+
# @option kwargs [Hash{String => String}, nil] :headers
|
128
|
+
# Additional headers to send with requests.
|
129
|
+
#
|
130
|
+
# @option kwargs [String, Ronin::Support::Network::HTTP::Cookie, nil] :cookie
|
131
|
+
# Additional cookie params to send with requests.
|
132
|
+
#
|
133
|
+
# @option kwargs [String, nil] :referer
|
134
|
+
# Optional `Referer` header to send with requests.
|
135
|
+
#
|
136
|
+
# @option kwargs [Hash{String => String}, nil] :form_data
|
137
|
+
# Additional form data to send with requests.
|
138
|
+
#
|
139
|
+
# @yield [vuln]
|
140
|
+
# If a block is given it will be yielded each discovered vulnerability.
|
141
|
+
#
|
142
|
+
# @yieldparam [SSTI] vuln
|
143
|
+
# A discovered SSTI vulnerability in the URL.
|
144
|
+
#
|
145
|
+
# @return [Array<SSTI>]
|
146
|
+
# All discovered SSTI vulnerabilities.
|
147
|
+
#
|
148
|
+
def self.scan(url, **kwargs,&block)
|
149
|
+
if kwargs.has_key?(:escape)
|
150
|
+
super(url, **kwargs, &block)
|
151
|
+
else
|
152
|
+
ESCAPES.each do |escape|
|
153
|
+
super(url, escape: escape, **kwargs, &block)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Escapes the payload using {#escape}.
|
160
|
+
#
|
161
|
+
# @param [String] payload
|
162
|
+
#
|
163
|
+
# @return [String]
|
164
|
+
#
|
165
|
+
def encode_payload(payload)
|
166
|
+
if @escape then @escape.call(payload)
|
167
|
+
else payload
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# Determine whether the URL is vulnerable to Server Side Template
|
173
|
+
# Injection (SSTI).
|
174
|
+
#
|
175
|
+
# @return [Boolean]
|
176
|
+
#
|
177
|
+
def vulnerable?
|
178
|
+
response = exploit(@test_expr.string)
|
179
|
+
body = response.body
|
180
|
+
|
181
|
+
return body.include?(@test_expr.result)
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Returns the type or kind of vulnerability.
|
186
|
+
#
|
187
|
+
# @return [Symbol]
|
188
|
+
#
|
189
|
+
# @note
|
190
|
+
# This is used internally to map an vulnerability class to a printable
|
191
|
+
# type.
|
192
|
+
#
|
193
|
+
# @api private
|
194
|
+
#
|
195
|
+
# @abstract
|
196
|
+
#
|
197
|
+
def self.vuln_type
|
198
|
+
:ssti
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|