Grudge match - Apache's mod_negotiation vs mod_speling!Submitted by mtesauro on Wed, 06/03/2009 - 09:17 |
So there was an interesting post by Andres (w3af project lead) yesterday on the w3af-users list noting an unusual behavior with Apache. Here's a very quick overview
-
Andres posts about how Apache will provide a list of files who match a request for only a file name (and no extension). The example he used was a request for “backup” where Apache produced a page with links to “backup.zip” and “backup.tgz”
-
I replied that he should look at mod_speling. It was a bit early in the day for me and, regrettably, I didn't notice the very subtle clue Andres left by putting “mod_negotiation” in the subject of his email. (Note to self: Drink more coffee)
-
Andres pointed out that mod_speling wasn't on and that it was in fact mod_negotiation that was causing the behavior which was his nice way of telling me to “Wake up!”.
-
We had a bit of back and forth about the issue
In my last email I mentioned:
It would be interesting to turn on mod_speling and off mod_negotiation on that host and try the same request and see what happens. That's an edge case I've not seen/played with.
Since nobody bit on my suggestion, I took a bit of time last night to play around with Apache plus mod_speling and mod_negotiation.
First some quick details. The HTTP request/reply that Andres posted looked like the one below. Note this was actually done on my server but I replicated exactly the request that Andres did.
GET /backup HTTP/1.0
Accept: foobar/xyz
User-Agent: w3af
Host: 198.214.110.128
Connection: Close
HTTP/1.1 406 Not Acceptable
Date: Tue, 02 Jun 2009 15:45:55 GMT
Server: Apache/2.2.8 (Ubuntu)
Alternates: {"backup.tgz" 1 {type application/x-gzip} {length 7022}}, {"backup.zip" 1 {type application/zip} {length 7074}}
Vary: negotiate,accept
TCN: list
Content-Length: 507
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>406 Not Acceptable</title>
</head><body>
<h1>Not Acceptable</h1>
<p>An appropriate representation of the requested resource /backup could not be found on this server.</p>
Available variants:
<ul>
<li><a href="backup.tgz">backup.tgz</a> , type application/x-gzip</li>
<li><a href="backup.zip">backup.zip</a> , type application/zip</li>
</ul>
<hr>
<address>Apache/2.2.8 (Ubuntu) Server at 198.214.110.128 Port 80</address>
</body></html>
Connection closed by foreign host.
He also provided a listing of his web root. Note this is also from my server:
root@mtesauro:/var/www# ls backup.tgz backup.zip index.html
Summary Details for the Impatient
First I setup a backup.zip and backup.tgz in the web root (as shown above) and tried variations on
mod_negotiation and mod_speling. Then I tried various request headers to see if that influenced the behavior. There were some interesting outcomes:
-
mod_speling doesn't behave as I expected. For variations on the request, I kept getting 404s. For example the following all produced 404s: /backup /backup.zzz /crackup.zip
-
mod_negotiation seems to rely upon the Accept request header to provide the listing of options. At first this seemed odd to me but I checked the RFCs and
"...the response SHOULD include an entity containing a list of available entity characteristics and location(s) from which the user or user agent can choose the one most appropriate."
-
The crucial bit seems to be the Accept header on the request which has some interesting behavior
-
If there is a MIME type match, the file matching that MIME type is served. So if you have “Accept: application/x-gzip”, then you get the .tgz file and no listing is created.
-
If the MIME type doesn't match (or is unknown to Apache) then a HTTP Status of 406 is returned with a listing of matches. This is the behavior that Andres mentioned in his post. BTW, you can get a list of MIME types Apache knows about by grep'ing for “AddType” and “TypesConfig” in your Apache config directory. TypesConfig should give a path to a MIME types file and AddType supplements/over-rides that list.
-
The 'super glob' MIME type of “*/*” qualifies as a match. The file served in this case, will be the first file in the HTTP 406 list. Basically, its the first file that would be found if you did a ls -1 [requested file] like “ls -1 backup*”. Since “t” in backup.tgz is listed before the 'z' in backup.zip, it is the default file served.
-
-
Interestingly, Apache will reply with HTTP 1.1 when it does a 406 Status reply even if the originating request is HTTP 1.0 which seems sensible as 406 isn't in the HTTP 1.0 RFC and HTTP 1.0 clients wouldn't know about a 406.
UPDATE: Please see my comment below for another (much earlier) blog post on this.
Lingering questions:
-
Since HTTP 1.0 doesn't even include Status 406, what variations in behavior exist between HTTP 1.0 and 1.1 requests (if any)?
-
Since mod_speling didn't work as expected, look further into the how that does matching. Is path at all important in the matching? Does the fact that there are two close matches make mod_speling generate a 404?
-
Does the order of Accept headers have an effect on what file is downloaded if you do something like “Accept: application/zip,application/x-gzip”? Or maybe adding */* either at the front or rear of the list? I suspect the first match would be served but haven't tried that.
-
What does the Apache source say about all this? We do have access to the source afterall.
The Details for those that 'Just Gotta Know'
Setup: Ubuntu Hardy-Heron 8-04 with the default Apache .deb installed (2.2.8-1ubuntu0.5)
# cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=8.04 DISTRIB_CODENAME=hardy DISTRIB_DESCRIPTION="Ubuntu 8.04.2"
First, I created the necessary files:
root@mtesauro:~# tar -czvf /var/www/backup.tgz /etc/services root@mtesauro:~# file /var/www/backup.tgz /var/www/backup.tgz: gzip compressed data, from Unix, last modified: Tue Jun 2 10:26:24 2009 root@mtesauro:~# cd /var/www/ root@mtesauro:/var/www# zip backup /etc/services adding: etc/services (deflated 62%) root@mtesauro:/var/www# file backup.zip backup.zip: Zip archive data, at least v2.0 to extract root@mtesauro:/var/www# ls backup.tgz backup.zip index.html
Then I made sure that mod_speling and mod_negotiation were not enabled:
root@mtesauro:/var/www# a2dismod speling Module speling already disabled root@mtesauro:/var/www# a2dismod negotiation Module negotiation already disabled
Next, I tried the same request Andres did but with neither module enabled. Consider this a test case of the baseline behavior of Apache.
GET /backup HTTP/1.0 Accept: foobar/xyz User-Agent: w3af Host: 192.168.1.128 Connection: Close HTTP/1.1 404 Not Found Date: Tue, 02 Jun 2009 15:51:42 GMT Server: Apache/2.2.8 (Ubuntu) Content-Length: 284 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> [snip]
Which 404's as expected. So lets turn on mod_negotiation and confirm Andres' post:
root@mtesauro:/var/www# a2enmod negotiation Module negotiation installed; run /etc/init.d/apache2 force-reload to enable. root@mtesauro:/var/www# /etc/init.d/apache2 force-reload * Reloading web server config apache2 [ OK ]
Then as a joe user:
GET /backup HTTP/1.0
Accept: foobar/xyz
User-Agent: w3af
Host: 192.168.1.128
Connection: Close
HTTP/1.1 406 Not Acceptable
Date: Tue, 02 Jun 2009 15:56:20 GMT
Server: Apache/2.2.8 (Ubuntu)
Alternates: {"backup.tgz" 1 {type application/x-gzip} {length 7022}}, {"backup.zip" 1 {type application/zip} {length 7074}}
Vary: negotiate,accept
TCN: list
Content-Length: 507
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>406 Not Acceptable</title>
[snip]So it does work as expected and reported by Andres. Lets see what happens when we try the same with mod_negotiation off plus mod_speling on:
root@mtesauro:/var/www# a2dismod negotiation Module negotiation disabled; run /etc/init.d/apache2 force-reload to fully disable. root@mtesauro:/var/www# a2enmod speling Module speling installed; run /etc/init.d/apache2 force-reload to enable. root@mtesauro:/var/www# /etc/init.d/apache2 force-reload * Reloading web server config apache2 [ OK ]
And the request:
GET /backup HTTP/1.0 Accept: foobar/xyz User-Agent: w3af Host: 192.168.1.128 Connection: Close HTTP/1.1 404 Not Found Date: Tue, 02 Jun 2009 15:59:58 GMT Server: Apache/2.2.8 (Ubuntu) Content-Length: 284 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> [snip]
Interesting. Mod_speling gives us a 404. Let try various purposeful mis-spellings of the file names and see what happens:
GET /backup.zzz HTTP/1.0 Accept: foobar/xyz User-Agent: w3af Host: 192.168.1.128 Connection: Close HTTP/1.1 404 Not Found Date: Tue, 02 Jun 2009 16:02:06 GMT Server: Apache/2.2.8 (Ubuntu) Content-Length: 288 Connection: close Content-Type: text/html; charset=iso-8859-1 [snip] GET /backup.zit HTTP/1.0 Accept: foobar/xyz User-Agent: w3af Host: 192.168.1.128 Connection: Close HTTP/1.1 404 Not Found Date: Tue, 02 Jun 2009 16:03:16 GMT Server: Apache/2.2.8 (Ubuntu) Content-Length: 288 Connection: close Content-Type: text/html; charset=iso-8859-1 [snip] GET /crackup.zip HTTP/1.0 Accept: foobar/xyz User-Agent: w3af Host: 192.168.1.128 Connection: Close HTTP/1.1 404 Not Found Date: Tue, 02 Jun 2009 16:06:22 GMT Server: Apache/2.2.8 (Ubuntu) Content-Length: 289 Connection: close
So mod_speling doesn't seem very helpful (especially for attackers). Since we appear to have narrowed this behavior to just mod_negotiation, lets turn off mod_speling, turn on mod_negotiation, and see if the HTTP request headers have anything to do with the behavior:
root@mtesauro:/var/www# a2dismod speling Module speling disabled; run /etc/init.d/apache2 force-reload to fully disable. root@mtesauro:/var/www# a2enmod negotiation Module negotiation installed; run /etc/init.d/apache2 force-reload to enable. root@mtesauro:/var/www# /etc/init.d/apache2 force-reload * Reloading web server config apache2 [ OK ]
And lets reduce the request to the bare minimum required by HTTP 1.0 – which is only the first line of our original request:
GET /backup HTTP/1.0 [snip bunch of gibberish which was backup.tgz]
And the minimal HTTP 1.1 request which is just the above + the host header:
GET /backup HTTP/1.0 Host: 192.168.1.128 [snip bunch of gibberish which was backup.tgz]
Lets iterate through other headers and see what happens:
GET /backup HTTP/1.0 User-Agent: w3af [snip bunch of gibberish which was backup.tgz] GET /backup HTTP/1.0 Connection: Close [snip bunch of gibberish which was backup.tgz] GET /backup HTTP/1.0 Accept: foobar/xyz [snip of a HTTP/1.1 406 Not Acceptable reply with the file list]
So the Accept header seems to provoke the HTTP 406 response. Next, lets vary the content of that header and look a bit more deeply into this behavior:
GET /backup HTTP/1.0 Accept: */* [snip bunch of gibberish which was backup.tgz] GET /backup HTTP/1.0 Accept: application/zip [snip bunch of gibberish which was backup.zip] GET /backup HTTP/1.0 Accept: application/tgz [snip of a HTTP/1.1 406 Not Acceptable reply with the file list] [Note: the application/tgz was not a known MIME type] GET /backup HTTP/1.0 Accept: application/x-gzip [snip bunch of gibberish which was backup.tgz]
So it would appear that Accept has great control over the behavior and if you want to get a list of matches send a MIME type that is bogus. I've already detailed other behavior specifics above so I won't cover that ground again. However, if you want to know what MIME types your Apache instance is aware of, you need to look at the configuration options for both AddType and TypesConfig which control how the mod_mime works. For example, on my setup:
# grep -R AddType /etc/apache2/ /etc/apache2/mods-available/mime.conf:# AddType allows you to add to or override the MIME configuration /etc/apache2/mods-available/mime.conf:#AddType application/x-gzip .tgz /etc/apache2/mods-available/mime.conf:AddType application/x-compress .Z /etc/apache2/mods-available/mime.conf:AddType application/x-gzip .gz .tgz /etc/apache2/mods-available/mime.conf:AddType application/x-bzip2 .bz2 /etc/apache2/mods-available/mime.conf:AddType text/html .shtml /etc/apache2/mods-available/ssl.conf:AddType application/x-x509-ca-cert .crt /etc/apache2/mods-available/ssl.conf:AddType application/x-pkcs7-crl .crl /etc/apache2/mods-enabled/mime.conf:# AddType allows you to add to or override the MIME configuration /etc/apache2/mods-enabled/mime.conf:#AddType application/x-gzip .tgz /etc/apache2/mods-enabled/mime.conf:AddType application/x-compress .Z /etc/apache2/mods-enabled/mime.conf:AddType application/x-gzip .gz .tgz /etc/apache2/mods-enabled/mime.conf:AddType application/x-bzip2 .bz2 /etc/apache2/mods-enabled/mime.conf:AddType text/html .shtml root@mtesauro:/var/www# grep -R TypesConfig /etc/apache2/ /etc/apache2/mods-available/mime.conf:# TypesConfig points to the file containing the list of mappings from /etc/apache2/mods-available/mime.conf:TypesConfig /etc/mime.types /etc/apache2/mods-enabled/mime.conf:# TypesConfig points to the file containing the list of mappings from /etc/apache2/mods-enabled/mime.conf:TypesConfig /etc/mime.types root@mtesauro:/var/www# head /etc/mime.types ############################################################################### # # MIME-TYPES and the extensions that represent them # # This file is part of the "mime-support" package. Please send email (not a # bug report) to mime-support@packages.debian.org if you would like new types # and/or extensions to be added. # # The reason that all types are managed by the mime-support package instead # allowing individual packages to install types in much the same way as they root@mtesauro:/var/www# tail /etc/mime.types video/x-ms-wmv wmv video/x-ms-wmx wmx video/x-ms-wvx wvx video/x-msvideo avi video/x-sgi-movie movie x-conference/x-cooltalk ice x-epoc/x-sisx-app sisx x-world/x-vrml vrm vrml wrl
Hope that helps explain my conclusions.
- mtesauro's blog
- Login or register to post comments

UPDATE to this post
I'm catching up on my email inbox and just realized that someone else has blogged about this. From their post:
So this has already been looked at by Stefano to the point of finding (and reporting) vulnerabilities in Apache. Still it was fun to play with.
BTW, I met Stefano in Poland at AppSec EU (and Andres face to face as well). I blogged about the presentation he and Luca Carettoni gave on HTTP Parameter Pollution.