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,266 @@
|
|
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/lfi/test_file'
|
23
|
+
|
24
|
+
require 'ronin/support/text/patterns'
|
25
|
+
require 'ronin/support/crypto'
|
26
|
+
require 'ronin/support/compression'
|
27
|
+
require 'uri/query_params'
|
28
|
+
require 'base64'
|
29
|
+
|
30
|
+
module Ronin
|
31
|
+
module Vulns
|
32
|
+
#
|
33
|
+
# Represents a Local File Inclusion (LFI) vulnerability.
|
34
|
+
#
|
35
|
+
# ## Features
|
36
|
+
#
|
37
|
+
# * Supports UNIX and Windows paths.
|
38
|
+
# * Supports `%00` null terminator trick (fixed in PHP 5.3).
|
39
|
+
# * Supports Base64, ROT13, and Zlib `php://filter/`s.
|
40
|
+
#
|
41
|
+
class LFI < WebVuln
|
42
|
+
|
43
|
+
include Ronin::Support
|
44
|
+
|
45
|
+
# The test file for UNIX systems.
|
46
|
+
UNIX_TEST_FILE = TestFile.new('/etc/passwd', %r{(?:[a-z][a-z0-9_-]*:x:\d+:\d+:[^:]*:(?:/[A-Za-z0-9_-]*)+:(?:/[A-Za-z0-9_-]*)+\n)+})
|
47
|
+
|
48
|
+
# The test file for Windows systems.
|
49
|
+
WINDOWS_TEST_FILE = TestFile.new('\\boot.ini', /\[boot loader\](?:\r?\n(?:[^\[\r\n].*)?)*\r?\n(?:\[operating system\](?:\r?\n(?:[^\[\r\n].*)?)*\r?\n)?/m)
|
50
|
+
|
51
|
+
# The default directory traversal depth.
|
52
|
+
DEFAULT_DEPTH = 6
|
53
|
+
|
54
|
+
# Targeted Operating System (OS)
|
55
|
+
#
|
56
|
+
# @return [:unix, :windows, nil]
|
57
|
+
attr_reader :os
|
58
|
+
|
59
|
+
# Optional filter bypass technique to use.
|
60
|
+
#
|
61
|
+
# @return [:null_byte, :base64, :rot13, :zlib, nil]
|
62
|
+
attr_reader :filter_bypass
|
63
|
+
|
64
|
+
# The number of directories to traverse up
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
attr_reader :depth
|
68
|
+
|
69
|
+
# The directory separator character.
|
70
|
+
#
|
71
|
+
# @return [String]
|
72
|
+
attr_reader :separator
|
73
|
+
|
74
|
+
# The escape path to add to every LFI path
|
75
|
+
#
|
76
|
+
# @return [String]
|
77
|
+
attr_reader :escape_path
|
78
|
+
|
79
|
+
# The common file to test with.
|
80
|
+
#
|
81
|
+
# @return [TestFile]
|
82
|
+
attr_reader :test_file
|
83
|
+
|
84
|
+
#
|
85
|
+
# Creates a new LFI object.
|
86
|
+
#
|
87
|
+
# @param [String, URI::HTTP] url
|
88
|
+
# The URL to exploit.
|
89
|
+
#
|
90
|
+
# @param [:unix, :windows, nil] os
|
91
|
+
# Operating System to specifically target.
|
92
|
+
#
|
93
|
+
# @param [Integer] depth
|
94
|
+
# Number of directories to escape up.
|
95
|
+
#
|
96
|
+
# @param [:null_byte, :double_escape, :base64, :rot13, :zlib, nil] filter_bypass
|
97
|
+
# Specifies which filter bypass technique to use.
|
98
|
+
#
|
99
|
+
# * `:null_byte - appends a `%00` null byte to the escaped path.
|
100
|
+
# **Note:* this technique only works on PHP < 5.3.
|
101
|
+
# * `:double_escape` - Double escapes the {#escape_path}
|
102
|
+
# (ex: `....//....//`).
|
103
|
+
# * `:base64` - Base64 encodes the included local file.
|
104
|
+
# * `:rot13` - ROT13 encodes the included local file.
|
105
|
+
# * `:zlib` - Zlib compresses and Base64 encodes the included local
|
106
|
+
# file.
|
107
|
+
#
|
108
|
+
# @param [Hash{Symbol => Object}] kwargs
|
109
|
+
# Additional keyword arguments for {WebVuln#initialize}.
|
110
|
+
#
|
111
|
+
def initialize(url, os: :unix,
|
112
|
+
depth: DEFAULT_DEPTH,
|
113
|
+
filter_bypass: nil,
|
114
|
+
**kwargs)
|
115
|
+
super(url,**kwargs)
|
116
|
+
|
117
|
+
@os = os
|
118
|
+
|
119
|
+
case @os
|
120
|
+
when :unix
|
121
|
+
@separator = '/'
|
122
|
+
@test_file = UNIX_TEST_FILE
|
123
|
+
when :windows
|
124
|
+
@separator = '\\'
|
125
|
+
@test_file = WINDOWS_TEST_FILE
|
126
|
+
else
|
127
|
+
raise(ArgumentError,"unknown os keyword value (#{@os.inspect}) must be either :unix or :windows")
|
128
|
+
end
|
129
|
+
|
130
|
+
case filter_bypass
|
131
|
+
when :null_byte, :double_escape, :base64, :rot13, :zlib, nil
|
132
|
+
@filter_bypass = filter_bypass
|
133
|
+
else
|
134
|
+
raise(ArgumentError,"unknown filter_bypass keyword value (#{filter_bypass.inspect}) must be :null_byte, :double_escape, :base64, :rot13, :zlib, or nil")
|
135
|
+
end
|
136
|
+
|
137
|
+
@depth = depth
|
138
|
+
@escape_path = ("..#{@separator}" * @depth)
|
139
|
+
|
140
|
+
apply_filter_bypasses
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
#
|
146
|
+
# Pre-applies additional filter-bypass rules to {#escape_path}.
|
147
|
+
#
|
148
|
+
def apply_filter_bypasses
|
149
|
+
if @filter_bypass == :double_escape
|
150
|
+
# HACK: String#gsub interpretes "\\" as a special character in the
|
151
|
+
# replace string, so we must use String#gsub with a block.
|
152
|
+
@escape_path.gsub!("..#{@separator}") do
|
153
|
+
"....#{@separator}#{@separator}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
public
|
159
|
+
|
160
|
+
#
|
161
|
+
# Escapes the given path.
|
162
|
+
#
|
163
|
+
# @param [String] path
|
164
|
+
# The given path to escape.
|
165
|
+
#
|
166
|
+
# @return [String]
|
167
|
+
# The escaped path.
|
168
|
+
#
|
169
|
+
# @note
|
170
|
+
# Relative paths and absolute Windows paths to other drives will not
|
171
|
+
# be escaped.
|
172
|
+
#
|
173
|
+
def escape(path)
|
174
|
+
if @os == :windows && path.start_with?('C:\\')
|
175
|
+
# escape absolute Windows paths to the C: drive
|
176
|
+
"#{@escape_path}#{path[3..]}"
|
177
|
+
elsif @os == :windows && path =~ /\A[A-Z]:/
|
178
|
+
# pass through absolute Windows paths to other drives
|
179
|
+
path
|
180
|
+
elsif path.start_with?(@separator)
|
181
|
+
# escape absolute paths
|
182
|
+
"#{@escape_path}#{path[1..]}"
|
183
|
+
else
|
184
|
+
# pass through relative paths
|
185
|
+
path
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
#
|
190
|
+
# Builds a `../../..` escaped path for the given file path.
|
191
|
+
#
|
192
|
+
# @param [String] path
|
193
|
+
# The path to escape.
|
194
|
+
#
|
195
|
+
# @return [String]
|
196
|
+
# The `../../../` escaped path.
|
197
|
+
#
|
198
|
+
# @note
|
199
|
+
# * If the given path begins with `php:`, then no `../../../` prefix
|
200
|
+
# will be added.
|
201
|
+
# * If initialized with `filter_bypass: :null_byte`, then a `\0`
|
202
|
+
# character will be appended to the path.
|
203
|
+
#
|
204
|
+
def encode_payload(path)
|
205
|
+
case @filter_bypass
|
206
|
+
when :base64
|
207
|
+
"php://filter/convert.base64-encode/resource=#{path}"
|
208
|
+
when :rot13
|
209
|
+
"php://filter/read=string.rot13/resource=#{path}"
|
210
|
+
when :zlib
|
211
|
+
"php://filter/zlib.deflate/convert.base64-encode/resource=#{path}"
|
212
|
+
when :null_byte
|
213
|
+
"#{escape(path)}\0"
|
214
|
+
else
|
215
|
+
escape(path)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
#
|
220
|
+
# Determines whether the URL is vulnerable to Local File Inclusion (LFI).
|
221
|
+
#
|
222
|
+
# @return [Boolean]
|
223
|
+
#
|
224
|
+
def vulnerable?
|
225
|
+
response = exploit(@test_file.path)
|
226
|
+
body = response.body
|
227
|
+
|
228
|
+
case @filter_bypass
|
229
|
+
when :base64
|
230
|
+
body.scan(Text::Patterns::BASE64).any? do |string|
|
231
|
+
Base64.decode64(string) =~ @test_file
|
232
|
+
end
|
233
|
+
when :rot13
|
234
|
+
Crypto.rot(body,-13) =~ @test_file
|
235
|
+
when :zlib
|
236
|
+
body.scan(Text::Patterns::BASE64).any? do |string|
|
237
|
+
begin
|
238
|
+
Compression.zlib_inflate(Base64.decode64(string)) =~ @test_file
|
239
|
+
rescue Zlib::DataError
|
240
|
+
end
|
241
|
+
end
|
242
|
+
else
|
243
|
+
body =~ @test_file
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Returns the type or kind of vulnerability.
|
249
|
+
#
|
250
|
+
# @return [Symbol]
|
251
|
+
#
|
252
|
+
# @note
|
253
|
+
# This is used internally to map an vulnerability class to a printable
|
254
|
+
# type.
|
255
|
+
#
|
256
|
+
# @api private
|
257
|
+
#
|
258
|
+
# @abstract
|
259
|
+
#
|
260
|
+
def self.vuln_type
|
261
|
+
:lfi
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,118 @@
|
|
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
|
+
require 'chars'
|
24
|
+
require 'cgi'
|
25
|
+
|
26
|
+
module Ronin
|
27
|
+
module Vulns
|
28
|
+
#
|
29
|
+
# Represents an Open Redirect vulnerability.
|
30
|
+
#
|
31
|
+
# ## Features
|
32
|
+
#
|
33
|
+
# * Checks 301, 302, 303, 307, and 308 HTTP redirects.
|
34
|
+
# * Checks `meta` refresh redirects.
|
35
|
+
# * Includes random alpha-numeric data in the test values.
|
36
|
+
#
|
37
|
+
class OpenRedirect < WebVuln
|
38
|
+
|
39
|
+
# The desired redirect URL to use in the test.
|
40
|
+
#
|
41
|
+
# @return [String]
|
42
|
+
attr_reader :test_url
|
43
|
+
|
44
|
+
#
|
45
|
+
# Initializes the Open Redirect vulnerability.
|
46
|
+
#
|
47
|
+
# @param [String, URI::HTTP] url
|
48
|
+
# The URL to exploit.
|
49
|
+
#
|
50
|
+
# @param [String] test_url
|
51
|
+
# The desired redirect URL to test the URL with.
|
52
|
+
#
|
53
|
+
def initialize(url, test_url: self.class.random_test_url, **kwargs)
|
54
|
+
super(url,**kwargs)
|
55
|
+
|
56
|
+
@test_url = test_url
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Generates a random redirect URL to use in tests.
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
# A random URL to https://ronin-rb.dev/vulns/open_redirect.html.
|
64
|
+
#
|
65
|
+
# @api private
|
66
|
+
#
|
67
|
+
def self.random_test_url
|
68
|
+
"https://ronin-rb.dev/vulns/open_redirect.html?id=#{Chars::ALPHA_NUMERIC.random_string(5)}"
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Tests whther the URL has a vulnerable Open Redirect.
|
73
|
+
#
|
74
|
+
# @return [Boolean]
|
75
|
+
#
|
76
|
+
def vulnerable?
|
77
|
+
response = exploit(@test_url)
|
78
|
+
|
79
|
+
case response.code
|
80
|
+
when '301', '302', '303', '307', '308'
|
81
|
+
if (locations = response.get_fields('Location'))
|
82
|
+
escaped_test_url = Regexp.escape(@test_url)
|
83
|
+
regexp = %r{\A#{escaped_test_url}(?:[\?&].+)?\z}
|
84
|
+
|
85
|
+
locations.last =~ regexp
|
86
|
+
end
|
87
|
+
else
|
88
|
+
content_type = response.content_type
|
89
|
+
|
90
|
+
if content_type && content_type.include?('text/html')
|
91
|
+
escaped_test_url = Regexp.escape(CGI.escapeHTML(@test_url))
|
92
|
+
regexp = %r{<meta\s+http-equiv\s*=\s*(?:"refresh"|'refresh'|refresh)\s+content\s*=\s*(?:"\s*\d+\s*;\s*url\s*=\s*'\s*#{escaped_test_url}\s*'\s*"|'\s*\d+\s*;\s*url\s*=\s*"\s*#{escaped_test_url}\s*"\s*'|\s*\d+;url=(?:"#{escaped_test_url}"|'#{escaped_test_url}'))\s*(?:/\s*)?>}i
|
93
|
+
|
94
|
+
response.body =~ regexp
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Returns the type or kind of vulnerability.
|
101
|
+
#
|
102
|
+
# @return [Symbol]
|
103
|
+
#
|
104
|
+
# @note
|
105
|
+
# This is used internally to map an vulnerability class to a printable
|
106
|
+
# type.
|
107
|
+
#
|
108
|
+
# @api private
|
109
|
+
#
|
110
|
+
# @abstract
|
111
|
+
#
|
112
|
+
def self.vuln_type
|
113
|
+
:open_redirect
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,224 @@
|
|
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
|
+
|
24
|
+
module Ronin
|
25
|
+
module Vulns
|
26
|
+
class ReflectedXSS < WebVuln
|
27
|
+
#
|
28
|
+
# Represents information about the context which the XSS occurs within.
|
29
|
+
#
|
30
|
+
class Context
|
31
|
+
|
32
|
+
# Where in the HTML the XSS occurs.
|
33
|
+
#
|
34
|
+
# @return [:double_quoted_attr_value, :single_quoted_attr_value, :unquoted_attr_value, :attr_name, :attr_list, :tag_name, :tag_body]
|
35
|
+
# The context which the XSS occurs in.
|
36
|
+
# * `:tag_body` occurred within a tag's body (ex: `<tag>XSS...</tag>`)
|
37
|
+
# * `:double_quoted_attr_value` - occurred in a double quoted
|
38
|
+
# attribute value (ex: `<tag name="XSS">...</tag>`)
|
39
|
+
# * `:single_quoted_attr_value` - occurred in a single quoted
|
40
|
+
# attribute value (ex: `<tag name='XSS'>...</tag>`)
|
41
|
+
# * `:unquoted_attr_value` - occurred in an unquoted attribute value
|
42
|
+
# (ex: `<tag name=XSS>...</tag>`)
|
43
|
+
# * `:attr_name` - occurred in an attribute name
|
44
|
+
# (ex: `<tag nameXSS ...>`)
|
45
|
+
# * `:attr_list` - occurred in the attribute list
|
46
|
+
# (ex: `<tag XSS>...</tag>`)
|
47
|
+
# * `:tag_name` - occurred in the tag name (ex: `<tagXSS>...</tag>`)
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
attr_reader :location
|
51
|
+
|
52
|
+
# The name of the parent tag which the XSS occurs in.
|
53
|
+
#
|
54
|
+
# @return [String]
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
attr_reader :tag
|
58
|
+
|
59
|
+
# The attribute name that the XSS occurs in.
|
60
|
+
#
|
61
|
+
# @return [String, nil]
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
attr_reader :attr
|
65
|
+
|
66
|
+
#
|
67
|
+
# Initializes the context.
|
68
|
+
#
|
69
|
+
# @param [:double_quoted_attr_value, :single_quoted_attr_value, :unquoted_attr_value, :attr_name, :attr_list, :tag_name, :tag_body] location
|
70
|
+
#
|
71
|
+
# @param [String] tag
|
72
|
+
#
|
73
|
+
# @param [String, nil] attr
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
#
|
77
|
+
def initialize(location, tag: nil, attr: nil)
|
78
|
+
@location = location
|
79
|
+
|
80
|
+
@tag = tag
|
81
|
+
@attr = attr
|
82
|
+
end
|
83
|
+
|
84
|
+
# HTML identifier regexp
|
85
|
+
#
|
86
|
+
# @api private
|
87
|
+
IDENTIFIER = /[A-Za-z0-9_-]+/
|
88
|
+
|
89
|
+
# HTML attribute name regexp.
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
ATTR_NAME = IDENTIFIER
|
93
|
+
|
94
|
+
# HTML attribute regexp.
|
95
|
+
#
|
96
|
+
# @api private
|
97
|
+
ATTR = /#{ATTR_NAME}(?:\s*=\s*"[^"]+"|\s*=\s*'[^']+'|=[^"'\s]+)?/
|
98
|
+
|
99
|
+
# HTML attribute list regexp.
|
100
|
+
#
|
101
|
+
# @api private
|
102
|
+
ATTR_LIST = /(?:\s+#{ATTR})*/
|
103
|
+
|
104
|
+
# HTML tag name regexp.
|
105
|
+
#
|
106
|
+
# @api private
|
107
|
+
TAG_NAME = IDENTIFIER
|
108
|
+
|
109
|
+
# Regexp matching when an XSS occurs within a tag's inner HTML.
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
IN_TAG_BODY = %r{<(#{TAG_NAME})#{ATTR_LIST}\s*(?:>|/>)[^<>]*\z}
|
113
|
+
|
114
|
+
# Regexp matching when an XSS occurs within a double-quoted attribute
|
115
|
+
# value.
|
116
|
+
#
|
117
|
+
# @api private
|
118
|
+
IN_DOUBLE_QUOTED_ATTR_VALUE = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\s*=\s*"[^"]+\z}
|
119
|
+
|
120
|
+
# Regexp matching when an XSS occurs within a single-quoted attribute
|
121
|
+
# value.
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
IN_SINGLE_QUOTED_ATTR_VALUE = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\s*=\s*'[^']+\z}
|
125
|
+
|
126
|
+
# Regexp matching when an XSS occurs within an unquoted attribute value.
|
127
|
+
#
|
128
|
+
# @api private
|
129
|
+
IN_UNQUOTED_ATTR_VALUE = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})=[^"'\s]+\z}
|
130
|
+
|
131
|
+
# Regexp matching when an XSS occurs within an attribute's name.
|
132
|
+
#
|
133
|
+
# @api private
|
134
|
+
IN_ATTR_NAME = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\z}
|
135
|
+
|
136
|
+
# Regexp matching when an XSS occurs within a tag's attribute list.
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
IN_ATTR_LIST = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+\z}
|
140
|
+
|
141
|
+
# Regexp matching when an XSS occurs within a tag's name.
|
142
|
+
#
|
143
|
+
# @api private
|
144
|
+
IN_TAG_NAME = %r{<(#{TAG_NAME})\z}
|
145
|
+
|
146
|
+
#
|
147
|
+
# Determine the context of the XSS by checking the characters that come
|
148
|
+
# before the given index.
|
149
|
+
#
|
150
|
+
# @param [String] body
|
151
|
+
# The HTML response body to inspect.
|
152
|
+
#
|
153
|
+
# @param [Integer] index
|
154
|
+
# The index which the XSS occurs at.
|
155
|
+
#
|
156
|
+
# @return [Context]
|
157
|
+
# The context which the XSS occurs in.
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
#
|
161
|
+
def self.identify(body,index)
|
162
|
+
prefix = body[0,index]
|
163
|
+
|
164
|
+
if (match = prefix.match(IN_TAG_BODY))
|
165
|
+
new(:tag_body, tag: match[1])
|
166
|
+
elsif (match = prefix.match(IN_DOUBLE_QUOTED_ATTR_VALUE))
|
167
|
+
new(:double_quoted_attr_value, tag: match[1], attr: match[2])
|
168
|
+
elsif (match = prefix.match(IN_SINGLE_QUOTED_ATTR_VALUE))
|
169
|
+
new(:single_quoted_attr_value, tag: match[1], attr: match[2])
|
170
|
+
elsif (match = prefix.match(IN_UNQUOTED_ATTR_VALUE))
|
171
|
+
new(:unquoted_attr_value, tag: match[1], attr: match[2])
|
172
|
+
elsif (match = prefix.match(IN_ATTR_NAME))
|
173
|
+
new(:attr_name, tag: match[1], attr: match[2])
|
174
|
+
elsif (match = prefix.match(IN_ATTR_LIST))
|
175
|
+
new(:attr_list, tag: match[1])
|
176
|
+
elsif (match = prefix.match(IN_TAG_NAME))
|
177
|
+
new(:tag_name, tag: match[1])
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# The minimum set of required characters needed for an XSS.
|
182
|
+
#
|
183
|
+
# @api private
|
184
|
+
MINIMAL_REQUIRED_CHARS = Set['>', ' ', '/', '<']
|
185
|
+
|
186
|
+
# The mapping of contexts and their required characters.
|
187
|
+
#
|
188
|
+
# @api private
|
189
|
+
REQUIRED_CHARS = {
|
190
|
+
double_quoted_attr_value: MINIMAL_REQUIRED_CHARS + ['"'],
|
191
|
+
single_quoted_attr_value: MINIMAL_REQUIRED_CHARS + ["'"],
|
192
|
+
unquoted_attr_value: MINIMAL_REQUIRED_CHARS,
|
193
|
+
|
194
|
+
attr_name: MINIMAL_REQUIRED_CHARS,
|
195
|
+
attr_list: MINIMAL_REQUIRED_CHARS,
|
196
|
+
tag_name: MINIMAL_REQUIRED_CHARS,
|
197
|
+
tag_body: MINIMAL_REQUIRED_CHARS
|
198
|
+
}
|
199
|
+
|
200
|
+
#
|
201
|
+
# Determines if the XSS is viable, given the context and the allowed
|
202
|
+
# characters.
|
203
|
+
#
|
204
|
+
# @param [Set<String>] allowed_chars
|
205
|
+
# The allowed characters.
|
206
|
+
#
|
207
|
+
# @return [Boolean]
|
208
|
+
# Specifies whether enough characters are allowed to perform an XSS in
|
209
|
+
# the given context.
|
210
|
+
#
|
211
|
+
# @api private
|
212
|
+
#
|
213
|
+
def viable?(allowed_chars)
|
214
|
+
required_chars = REQUIRED_CHARS.fetch(@location) do
|
215
|
+
raise(NotImplementedError,"cannot determine viability for unknown XSS location type: #{@location.inspect}")
|
216
|
+
end
|
217
|
+
|
218
|
+
allowed_chars.superset?(required_chars)
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|