ronin-vulns 0.1.0.beta1
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/.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
|