📜 ⬆️ ⬇️

About one little-known vulnerability in web sites

The first rule of security in developing Web applications is: -
Do not trust the data coming from the client.
Almost all of this rule is well known and respected. We pass form validators through cookies, even URIs.
But recently I was surprised to find that there is one variable coming from a client that almost no one filters.
It will be a question of the compromise of the web application through the substitution of the value HTTP_HOST and SERVER_NAME .


If you perform a search on GitHub by the keyword "HTTP_HOST", then you can find about 43 pages of repositories that use $_SERVER['HTTP_HOST'] . At a quick scan, I found quite a few cases where the data that came in this variable were left without filtering. Most often, only the existence of $_SERVER['HTTP_HOST'] , but they are not validated.

I will describe the situation for the Nginx + php bundle (php-fpm or fcgi-spawn), for other web servers or another programming language the situation will be different in details, but the general principles are preserved.
Apache behavior is described here: shiflett.org/blog/2006/mar/server-name-versus-http-host
')

Ways to compromise


For illustration, we will use telnet
The headers that the browser sends to the server look like this:
 GET / HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:site.dev Referer:http://site.dev/index.htm User-Agent:TelnetTest 

If you remove a line from them (sending a request without HTTP_HOST )
 Host:site.dev, 
then the server will return
 400 Bad Request 

Another way to send headers:
 GET http://site.dev/ HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:site.dev Referer:http://site.dev/index.htm User-Agent:TelnetTest 

The result will be exactly the same as when sending the first heading.
 GET / HTTP/1.1 

But here, if the first title is not passed
 GET http://site.dev/ HTTP/1.1 
but
 GET http://site.dev/ 
Then all subsequent headers will be discarded, Nginx will work out the server section defined for
  server_name site.dev; 
But HTTP_HOST and SERVER_NAME will not be determined.
HTTP_HOST an empty HTTP_HOST fails:
 Host: 
But it turns out to pass
 Host:_ 
or
 Host:"" 

Now the fun part.
Connect to telnet
 $ telnet site.dev 80 Trying 127.0.0.1... Connected to site.dev. Escape character is '^]'. 

Send off
 GET http://site.dev/phpinfo.php HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:~%#$^&*()<>?@\!."'{}[]=+| Referer:http://site.dev/index.htm User-Agent:TelnetTest 

And look:
 _SERVER["SERVER_NAME"]: ~%#$^&*()<>?@\!."'{}[]=+| _SERVER["HTTP_HOST"]: ~%#$^&*()<>?@\!."'{}[]=+| 

Server response:
 HTTP/1.1 200 OK Server: nginx/1.0.10 Date: Wed, 23 Jan 2013 10:31:14 GMT Content-Type: text/html Transfer-Encoding: chunked Connection: keep-alive 

If there is a '/' in the Host: header, the server will return 400 Bad Request .
Those. such Host:../../ header will not work, such Host:http://evil.site too

Vulnerabilities


Access to private data
SQL injections

Ways to protect


The easiest and most affordable way to protect (found here: stackoverflow.com/questions/1459739/php-serverhttp-host-vs-serverserver-name-am-i-understanding-the-ma ).
 $allowed_hosts = array('foo.example.com', 'bar.example.com'); if (!isset($_SERVER['HTTP_HOST']) || !in_array($_SERVER['HTTP_HOST'], $allowed_hosts)) { header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request'); exit; } 

The most effective way to protect is to explicitly determine HTTP_HOST on the web server side.
To understand how to redefine HTTP_HOST add the following lines to Nginx's config:
 fastcgi_param HTTP_HOST1 $http_host; fastcgi_param HTTP_HOST2 $host; fastcgi_param HTTP_HOST3 $server_name; 

Suppose we have two server sections defined:
 server { listen 80; server_name site1.dev; ... } server { listen 80; server_name site2.dev site3.dev; ... } 

We make such a request

 $ telnet site1.dev 80 Trying 127.0.0.1... Connected to site.dev. Escape character is '^]'. 


 GET http://site3.dev/phpinfo.php HTTP/1.1 Host:~%#$^&*()<>?@\!."'{}[]=+| User-Agent:TelnetTest 

At the output we get
 _SERVER["HTTP_HOST1"]: ~%#$^&*()<>?@\!."'{}[]=+| _SERVER["HTTP_HOST2"]: site3.dev _SERVER["HTTP_HOST3"]: site2.dev 

Everything is logical. And the most correct entry would be:
 fastcgi_param HTTP_HOST $host; 

If you make a request for
 $ telnet site3.dev 80 

 GET /phpinfo.php HTTP/1.1 Host:~%#$^&*()<>?@\!."'{}[]=+| User-Agent:TelnetTest 

then the section will work
 server { listen 80 default_server; server_name ""; return 444; } 

which will easily eliminate such a request.

Source: https://habr.com/ru/post/166855/


All Articles