Hi team,
Here is the sample vulnerable code
class TesttestController < ApplicationController
def index
redirect_to "/demo/?your_reset_token=your_reset_token"
end
end
The _compute_redirect_to_location will take the input**"/demo/?your_reset_token=your_reset_token"asoptions** variables.
File Fileaction_controller\metal\redirecting.rb**** line 63
def redirect_to(options = {}, response_options = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_options)
self.location = _compute_redirect_to_location(request, options)
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end
Then it will check if the options, because the input isString, so it will be the concatenate ofrequest.protocol + request.host_with_port + optionsFileaction_controller\metal\redirecting.rb line 96
def _compute_redirect_to_location(request, options) #:nodoc:
case options
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
# characters; and is terminated by a colon (":").
# See https://tools.ietf.org/html/rfc3986#section-3.1
# The protocol relative scheme starts with a double slash "//".
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
options
when String
request.protocol + request.host_with_port + options
when Proc
_compute_redirect_to_location request, instance_eval(&options)
else
url_for(options)
end.delete("\0\r\n")
end
module_function :_compute_redirect_to_location
public :_compute_redirect_to_location
The request.protocol will behttp://orhttps://Therequest.host_with_port will callraw_host_with_portto check if there is theX_FORWARD_FORelse, it will take the input fromHTTP_HOSTthen continue the**_compute_redirect_to_location** process.
file action_dispatch\http\url.rb line 220
def raw_host_with_port
if forwarded = x_forwarded_host.presence
forwarded.split(/,\s?/).last
else
get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
end
end
So at the end, our input will be request.protocol + request.host_with_port + options and being set asHeader[“location”] value.
We cant change the HTTP_HOST to something else because the HTTP_HOST must be declare in the the config.host and raise 403 Status ( see theimg4.JPG )
But if we use the IP, we can bypass this and here is the result, which 149.28.128.52 is the attacker IP, and there is no check for IP address but only the hostname.
The img2.JPG is the PoC request when use HTTP_HOST.
The img3.JPG is the PoC request when use HTTP_X_FORWARD_HOST.
Fix:
There should be a check for IP ADDRESS as well as the HOSTNAME in the enviroment config if user dont specific the host as parameter for redirect_to function.
Password Reset Poisoning - which user use the HTTP_HOST as the input for the reset token.
Web-cache - which user use the HTTP_HOST as the input