# frozen_string_literal: true
#
# ronin-vulns - A Ruby library for blind vulnerability testing.
#
# Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-vulns is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-vulns is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-vulns. If not, see .
#
require 'ronin/vulns/web_vuln'
module Ronin
module Vulns
class ReflectedXSS < WebVuln
#
# Represents information about the context which the XSS occurs within.
#
class Context
# Where in the HTML the XSS occurs.
#
# @return [:double_quoted_attr_value, :single_quoted_attr_value, :unquoted_attr_value, :attr_name, :attr_list, :tag_name, :tag_body, :comment]
# The context which the XSS occurs in.
# * `:tag_body` occurred within a tag's body (ex: `XSS...`)
# * `:double_quoted_attr_value` - occurred in a double quoted
# attribute value (ex: `...`)
# * `:single_quoted_attr_value` - occurred in a single quoted
# attribute value (ex: `...`)
# * `:unquoted_attr_value` - occurred in an unquoted attribute value
# (ex: `...`)
# * `:attr_name` - occurred in an attribute name
# (ex: ``)
# * `:attr_list` - occurred in the attribute list
# (ex: `...`)
# * `:tag_name` - occurred in the tag name (ex: `...`)
# * `:comment` - occurred in a comment (ex: ``)
#
# @api public
attr_reader :location
# The name of the parent tag which the XSS occurs in.
#
# @return [String, nil]
#
# @api public
attr_reader :tag
# The attribute name that the XSS occurs in.
#
# @return [String, nil]
#
# @api public
attr_reader :attr
#
# Initializes the context.
#
# @param [:double_quoted_attr_value, :single_quoted_attr_value, :unquoted_attr_value, :attr_name, :attr_list, :tag_name, :tag_body, :comment] location
#
# @param [String, nil] tag
#
# @param [String, nil] attr
#
# @api private
#
def initialize(location, tag: nil, attr: nil)
@location = location
@tag = tag
@attr = attr
end
# HTML identifier regexp
#
# @api private
IDENTIFIER = /[A-Za-z0-9_-]+/
# HTML attribute name regexp.
#
# @api private
ATTR_NAME = IDENTIFIER
# HTML attribute regexp.
#
# @api private
ATTR = /#{ATTR_NAME}(?:\s*=\s*"[^"]+"|\s*=\s*'[^']+'|=[^"'\s]+)?/
# HTML attribute list regexp.
#
# @api private
ATTR_LIST = /(?:\s+#{ATTR})*/
# HTML comment regexp.
#
# @api private
COMMENT = /]*>/
# HTML tag name regexp.
#
# @api private
TAG_NAME = IDENTIFIER
# Regexp matching when an XSS occurs within a tag's inner HTML.
#
# @api private
IN_TAG_BODY = %r{<(#{TAG_NAME})#{ATTR_LIST}\s*(?:>|/>)([^<>]|#{COMMENT})*\z}
# Regexp matching when an XSS occurs within a double-quoted attribute
# value.
#
# @api private
IN_DOUBLE_QUOTED_ATTR_VALUE = /<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\s*=\s*"[^"]+\z/
# Regexp matching when an XSS occurs within a single-quoted attribute
# value.
#
# @api private
IN_SINGLE_QUOTED_ATTR_VALUE = /<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\s*=\s*'[^']+\z/
# Regexp matching when an XSS occurs within an unquoted attribute value.
#
# @api private
IN_UNQUOTED_ATTR_VALUE = /<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})=[^"'\s]+\z/
# Regexp matching when an XSS occurs within an attribute's name.
#
# @api private
IN_ATTR_NAME = /<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\z/
# Regexp matching when an XSS occurs within a tag's attribute list.
#
# @api private
IN_ATTR_LIST = /<(#{TAG_NAME})#{ATTR_LIST}\s+\z/
# Regexp matching when an XSS occurs within a tag's name.
#
# @api private
IN_TAG_NAME = /<(#{TAG_NAME})\z/
# Regexp matching when an XSS occurs within a comment.
#
# @api private
IN_COMMENT = /]*\z/
#
# Determine the context of the XSS by checking the characters that come
# before the given index.
#
# @param [String] body
# The HTML response body to inspect.
#
# @param [Integer] index
# The index which the XSS occurs at.
#
# @return [Context]
# The context which the XSS occurs in.
#
# @api private
#
def self.identify(body,index)
prefix = body[0,index]
if (match = prefix.match(IN_TAG_BODY))
new(:tag_body, tag: match[1])
elsif (match = prefix.match(IN_DOUBLE_QUOTED_ATTR_VALUE))
new(:double_quoted_attr_value, tag: match[1], attr: match[2])
elsif (match = prefix.match(IN_SINGLE_QUOTED_ATTR_VALUE))
new(:single_quoted_attr_value, tag: match[1], attr: match[2])
elsif (match = prefix.match(IN_UNQUOTED_ATTR_VALUE))
new(:unquoted_attr_value, tag: match[1], attr: match[2])
elsif (match = prefix.match(IN_ATTR_NAME))
new(:attr_name, tag: match[1], attr: match[2])
elsif (match = prefix.match(IN_ATTR_LIST))
new(:attr_list, tag: match[1])
elsif (match = prefix.match(IN_TAG_NAME))
new(:tag_name, tag: match[1])
elsif prefix.match?(IN_COMMENT)
new(:comment)
end
end
# The minimum set of required characters needed for an XSS.
#
# @api private
MINIMAL_REQUIRED_CHARS = Set['>', ' ', '/', '<']
# The mapping of contexts and their required characters.
#
# @api private
REQUIRED_CHARS = {
double_quoted_attr_value: MINIMAL_REQUIRED_CHARS + ['"'],
single_quoted_attr_value: MINIMAL_REQUIRED_CHARS + ["'"],
unquoted_attr_value: MINIMAL_REQUIRED_CHARS,
attr_name: MINIMAL_REQUIRED_CHARS,
attr_list: MINIMAL_REQUIRED_CHARS,
tag_name: MINIMAL_REQUIRED_CHARS,
tag_body: MINIMAL_REQUIRED_CHARS,
comment: MINIMAL_REQUIRED_CHARS
}
#
# Determines if the XSS is viable, given the context and the allowed
# characters.
#
# @param [Set] allowed_chars
# The allowed characters.
#
# @return [Boolean]
# Specifies whether enough characters are allowed to perform an XSS in
# the given context.
#
# @api private
#
def viable?(allowed_chars)
required_chars = REQUIRED_CHARS.fetch(@location) do
raise(NotImplementedError,"cannot determine viability for unknown XSS location type: #{@location.inspect}")
end
allowed_chars.superset?(required_chars)
end
end
end
end
end