313 lines
No EOL
11 KiB
Text
Executable file
313 lines
No EOL
11 KiB
Text
Executable file
Advisory: Python CGIHTTPServer File Disclosure and Potential Code
|
||
Execution
|
||
|
||
The CGIHTTPServer Python module does not properly handle URL-encoded
|
||
path separators in URLs. This may enable attackers to disclose a CGI
|
||
script's source code or execute arbitrary CGI scripts in the server's
|
||
document root.
|
||
|
||
Details
|
||
=======
|
||
|
||
Product: Python CGIHTTPServer
|
||
Affected Versions:
|
||
2.7 - 2.7.7,
|
||
3.2 - 3.2.4,
|
||
3.3 - 3.3.2,
|
||
3.4 - 3.4.1,
|
||
3.5 pre-release
|
||
Fixed Versions:
|
||
2.7 rev b4bab0788768,
|
||
3.2 rev e47422855841,
|
||
3.3 rev 5676797f3a3e,
|
||
3.4 rev 847e288d6e93,
|
||
3.5 rev f8b3bb5eb190
|
||
Vulnerability Type: File Disclosure, Directory Traversal, Code Execution
|
||
Security Risk: high
|
||
Vendor URL: https://docs.python.org/2/library/cgihttpserver.html
|
||
Vendor Status: fixed version released
|
||
Advisory URL: https://www.redteam-pentesting.de/advisories/rt-sa-2014-008
|
||
Advisory Status: published
|
||
CVE: CVE-2014-4650
|
||
CVE URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-4650
|
||
|
||
|
||
Introduction
|
||
============
|
||
|
||
The CGIHTTPServer module defines a request-handler class, interface
|
||
compatible with BaseHTTPServer. BaseHTTPRequestHandler and inherits
|
||
behavior from SimpleHTTPServer. SimpleHTTPRequestHandler but can also
|
||
run CGI scripts.
|
||
|
||
(from the Python documentation)
|
||
|
||
|
||
More Details
|
||
============
|
||
|
||
The CGIHTTPServer module can be used to set up a simple HTTP server with
|
||
CGI scripts. A sample server script in Python may look like the
|
||
following:
|
||
|
||
------------------------------------------------------------------------
|
||
#!/usr/bin/env python2
|
||
|
||
import CGIHTTPServer
|
||
import BaseHTTPServer
|
||
|
||
if __name__ == "__main__":
|
||
server = BaseHTTPServer.HTTPServer
|
||
handler = CGIHTTPServer.CGIHTTPRequestHandler
|
||
server_address = ("", 8000)
|
||
# Note that only /cgi-bin will work:
|
||
handler.cgi_directories = ["/cgi-bin", "/cgi-bin/subdir"]
|
||
httpd = server(server_address, handler)
|
||
httpd.serve_forever()
|
||
------------------------------------------------------------------------
|
||
|
||
This server should execute any scripts located in the subdirectory
|
||
"cgi-bin". A sample CGI script can be placed in that directory, for
|
||
example a script like the following:
|
||
|
||
------------------------------------------------------------------------
|
||
#!/usr/bin/env python2
|
||
import json
|
||
import sys
|
||
|
||
db_credentials = "SECRET"
|
||
sys.stdout.write("Content-type: text/json\r\n\r\n")
|
||
sys.stdout.write(json.dumps({"text": "This is a Test"}))
|
||
------------------------------------------------------------------------
|
||
|
||
The Python library CGIHTTPServer.py implements the CGIHTTPRequestHandler
|
||
class which inherits from SimpleHTTPServer.SimpleHTTPRequestHandler:
|
||
|
||
class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||
[...]
|
||
def do_GET(self):
|
||
"""Serve a GET request."""
|
||
f = self.send_head()
|
||
if f:
|
||
try:
|
||
self.copyfile(f, self.wfile)
|
||
finally:
|
||
f.close()
|
||
|
||
def do_HEAD(self):
|
||
"""Serve a HEAD request."""
|
||
f = self.send_head()
|
||
if f:
|
||
f.close()
|
||
|
||
def translate_path(self, path):
|
||
[...]
|
||
path = posixpath.normpath(urllib.unquote(path))
|
||
words = path.split('/')
|
||
words = filter(None, words)
|
||
path = os.getcwd()
|
||
[...]
|
||
|
||
The CGIHTTPRequestHandler class inherits, among others, the methods
|
||
do_GET() and do_HEAD() for handling HTTP GET and HTTP HEAD requests. The
|
||
class overrides send_head() and implements several new methods, such as
|
||
do_POST(), is_cgi() and run_cgi():
|
||
|
||
class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||
[...]
|
||
def do_POST(self):
|
||
[...]
|
||
if self.is_cgi():
|
||
self.run_cgi()
|
||
else:
|
||
self.send_error(501, "Can only POST to CGI scripts")
|
||
|
||
def send_head(self):
|
||
"""Version of send_head that support CGI scripts"""
|
||
if self.is_cgi():
|
||
return self.run_cgi()
|
||
else:
|
||
return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
|
||
|
||
def is_cgi(self):
|
||
[...]
|
||
collapsed_path = _url_collapse_path(self.path)
|
||
dir_sep = collapsed_path.find('/', 1)
|
||
head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
|
||
if head in self.cgi_directories:
|
||
self.cgi_info = head, tail
|
||
return True
|
||
return False
|
||
[...]
|
||
def run_cgi(self):
|
||
"""Execute a CGI script."""
|
||
dir, rest = self.cgi_info
|
||
|
||
[...]
|
||
|
||
# dissect the part after the directory name into a script name &
|
||
# a possible additional path, to be stored in PATH_INFO.
|
||
i = rest.find('/')
|
||
if i >= 0:
|
||
script, rest = rest[:i], rest[i:]
|
||
else:
|
||
script, rest = rest, ''
|
||
|
||
scriptname = dir + '/' + script
|
||
scriptfile = self.translate_path(scriptname)
|
||
if not os.path.exists(scriptfile):
|
||
self.send_error(404, "No such CGI script (%r)" % scriptname)
|
||
return
|
||
if not os.path.isfile(scriptfile):
|
||
self.send_error(403, "CGI script is not a plain file (%r)" %
|
||
scriptname)
|
||
return
|
||
[...]
|
||
[...]
|
||
|
||
For HTTP GET requests, do_GET() first invokes send_head(). That method
|
||
calls is_cgi() to determine whether the requested path is to be executed
|
||
as a CGI script. The is_cgi() method uses _url_collapse_path() to
|
||
normalize the path, i.e. remove extraneous slashes (/),current directory
|
||
(.), or parent directory (..) elements, taking care not to permit
|
||
directory traversal below the document root. The is_cgi() function
|
||
returns True when the first path element is contained in the
|
||
cgi_directories list. As _url_collaps_path() and is_cgi() never URL
|
||
decode the path, replacing the forward slash after the CGI directory in
|
||
the URL to a CGI script with the URL encoded variant %2f leads to
|
||
is_cgi() returning False. This will make CGIHTTPRequestHandler's
|
||
send_head() then invoke its parent's send_head() method which translates
|
||
the URL path to a file system path using the translate_path() method and
|
||
then outputs the file's contents raw. As translate_path() URL decodes
|
||
the path, this then succeeds and discloses the CGI script's file
|
||
contents:
|
||
|
||
$ curl http://localhost:8000/cgi-bin%2ftest.py
|
||
#!/usr/bin/env python2
|
||
import json
|
||
import sys
|
||
|
||
db_credentials = "SECRET"
|
||
sys.stdout.write("Content-type: text/json\r\n\r\n")
|
||
sys.stdout.write(json.dumps({"text": "This is a Test"}))
|
||
|
||
Similarly, the CGIHTTPRequestHandler can be tricked into executing CGI
|
||
scripts that would normally not be executable. The class normally only
|
||
allows executing CGI scripts that are direct children of one of the
|
||
directories listed in cgi_directories. Furthermore, only direct
|
||
subdirectories of the document root (the current working directory) can
|
||
be valid CGI directories.
|
||
|
||
This can be seen in the following example. Even though the sample server
|
||
shown above includes "/cgi-bin/subdir" as part of the request handler's
|
||
cgi_directories, a CGI script named test.py in that directory is not
|
||
executed:
|
||
|
||
$ curl http://localhost:8000/cgi-bin/subdir/test.py
|
||
[...]
|
||
<p>Error code 403.
|
||
<p>Message: CGI script is not a plain file ('/cgi-bin/subdir').
|
||
[...]
|
||
|
||
Here, is_cgi() set self.cgi_info to ('/cgi-bin', 'subdir/test.py') and
|
||
returned True. Next, run_cgi() further dissected these paths to perform
|
||
some sanity checks, thereby mistakenly assuming subdir to be the
|
||
executable script's filename and test.py to be path info. As subdir is
|
||
not an executable file, run_cgi() returns an error message. However, if
|
||
the forward slash between subdir and test.py is replaced with %2f,
|
||
invoking the script succeeds:
|
||
|
||
$ curl http://localhost:8000/cgi-bin/subdir%2ftest.py
|
||
{"text": "This is a Test"}
|
||
|
||
This is because neither is_cgi() nor run_cgi() URL decode the path
|
||
during processing until run_cgi() tries to determine whether the target
|
||
script is an executable file. More specifically, as subdir%2ftest.py
|
||
does not contain a forward slash, it is not split into the script name
|
||
subdir and path info test.py, as in the previous example.
|
||
|
||
Similarly, using URL encoded forward slashes, executables outside of a
|
||
CGI directory can be executed:
|
||
|
||
$ curl http://localhost:8000/cgi-bin/..%2ftraversed.py
|
||
{"text": "This is a Test"}
|
||
|
||
|
||
Workaround
|
||
==========
|
||
|
||
Subclass CGIHTTPRequestHandler and override the is_cgi() method with a
|
||
variant that first URL decodes the supplied path, for example:
|
||
|
||
class FixedCGIHTTPRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||
def is_cgi(self):
|
||
self.path = urllib.unquote(self.path)
|
||
return CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self)
|
||
|
||
|
||
Fix
|
||
===
|
||
|
||
Update to the latest Python version from the Mercurial repository at
|
||
http://hg.python.org/cpython/
|
||
|
||
|
||
Security Risk
|
||
=============
|
||
|
||
The vulnerability can be used to gain access to the contents of CGI
|
||
binaries or the source code of CGI scripts. This may reveal sensitve
|
||
information, for example access credentials. This can greatly help
|
||
attackers in mounting further attacks and is therefore considered to
|
||
pose a high risk. Furthermore attackers may be able to execute code that
|
||
was not intended to be executed. However, this is limited to files
|
||
stored in the server's working directory or in its subdirectories.
|
||
|
||
The CGIHTTPServer code does contain this warning:
|
||
"SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL"
|
||
Even when used on a local computer this may allow other local users to
|
||
execute code in the context of another user.
|
||
|
||
|
||
Timeline
|
||
========
|
||
|
||
2014-04-07 Vulnerability identified
|
||
2014-06-11 Customer approved disclosure to vendor
|
||
2014-06-11 Vendor notified
|
||
2014-06-15 Vendor disclosed vulnerability in their public bug tracker
|
||
and addressed it in public source code repository
|
||
2014-06-23 CVE number requested
|
||
2014-06-25 CVE number assigned
|
||
2014-06-26 Advisory released
|
||
|
||
|
||
References
|
||
==========
|
||
|
||
http://bugs.python.org/issue21766
|
||
|
||
|
||
RedTeam Pentesting GmbH
|
||
=======================
|
||
|
||
RedTeam Pentesting offers individual penetration tests, short pentests,
|
||
performed by a team of specialised IT-security experts. Hereby, security
|
||
weaknesses in company networks or products are uncovered and can be
|
||
fixed immediately.
|
||
|
||
As there are only few experts in this field, RedTeam Pentesting wants to
|
||
share its knowledge and enhance the public knowledge with research in
|
||
security related areas. The results are made available as public
|
||
security advisories.
|
||
|
||
More information about RedTeam Pentesting can be found at
|
||
https://www.redteam-pentesting.de.
|
||
|
||
|
||
--
|
||
RedTeam Pentesting GmbH Tel.: +49 241 510081-0
|
||
Dennewartstr. 25-27 Fax : +49 241 510081-99
|
||
52068 Aachen https://www.redteam-pentesting.de
|
||
Germany Registergericht: Aachen HRB 14004
|
||
Gesch<EFBFBD>ftsf<EFBFBD>hrer: Patrick Hof, Jens Liebchen |