CVE-2017-7272 (published 2017-03-27T17:59Z)

PHP through 7.1.11 enables potential SSRF in applications that accept an fsockopen or pfsockopen hostname argument with an expectation that the port number is constrained. Because a :port syntax is recognized, fsockopen will use the port number that is specified in the hostname argument, instead of the port number in the second argument of the function.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 7.4, HIGH (CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/php/php-src
commit_details: repo_url: https://github.com/php/php-src
author: Sara Golemon
author_date: 2017-03-07 11:27:46-08:00
msg: Detect invalid port in xp_socket parse ip address

For historical reasons, fsockopen() accepts the port and hostname
separately: fsockopen('127.0.0.1', 80)

However, with the introdcution of stream transports in PHP 4.3,
it became possible to include the port in the hostname specifier:

fsockopen('127.0.0.1:80')
Or more formally: fsockopen('tcp://127.0.0.1:80')

Confusing results when these two forms are combined, however.
fsockopen('127.0.0.1:80', 443) results in fsockopen() attempting
to connect to '127.0.0.1:80:443' which any reasonable stack would
consider invalid.

Unfortunately, PHP parses the address looking for the first colon
(with special handling for IPv6, don't worry) and calls atoi()
from there.  atoi() in turn, simply stops parsing at the first
non-numeric character and returns the value so far.

The end result is that the explicitly supplied port is treated
as ignored garbage, rather than producing an error.

This diff replaces atoi() with strtol() and inspects the
stop character.  If additional "garbage" of any kind is found,
it fails and returns an error.
repository: repo_url: https://github.com/php/php-src
repo_name: php/php-src
description: The PHP Interpreter
repo_language: C
owner: php
file_changes: filename: parseip-001.phpt
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+--TEST--
+Use of double-port in fsockopen()
+--FILE--
+<?php
+
+$try = [
+  '127.0.0.1:80',
+  'tcp://127.0.0.1:80',
+  '[::1]:80',
+  'tcp://[::1]:80',
+  'localhost:80',
+  'tcp://localhost:80',
+];
+
+foreach ($try as $addr) {
+  echo "== $addr ==\n";
+  var_dump(@fsockopen($addr, 81, $errno, $errstr), $errstr);
+}
+--EXPECTF--
+== 127.0.0.1:80 ==
+bool(false)
+string(41) "Failed to parse address "127.0.0.1:80:81""
+== tcp://127.0.0.1:80 ==
+bool(false)
+string(41) "Failed to parse address "127.0.0.1:80:81""
+== [::1]:80 ==
+bool(false)
+string(37) "Failed to parse address "[::1]:80:81""
+== tcp://[::1]:80 ==
+bool(false)
+string(37) "Failed to parse address "[::1]:80:81""
+== localhost:80 ==
+bool(false)
+string(41) "Failed to parse address "localhost:80:81""
+== tcp://localhost:80 ==
+bool(false)
+string(41) "Failed to parse address "localhost:80:81""
\ No newline at end of file

programming_language: Lua
filename: xp_socket.c
change_type: ModificationType.MODIFY
diff: @@ -571,37 +571,44 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po
 	char *host = NULL;
 
 #ifdef HAVE_IPV6
-	char *p;
-
 	if (*(str) == '[' && str_len > 1) {
 		/* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */
-		p = memchr(str + 1, ']', str_len - 2);
+		char *p = memchr(str + 1, ']', str_len - 2), *e = NULL;
 		if (!p || *(p + 1) != ':') {
 			if (get_err) {
 				*err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str);
 			}
 			return NULL;
 		}
-		*portno = atoi(p + 2);
+		*portno = strtol(p + 2, &e, 10);
+		if (e && *e) {
+			if (get_err) {
+				*err = strpprintf(0, "Failed to parse address \"%s\"", str);
+			}
+			return NULL;
+		}
 		return estrndup(str + 1, p - str - 1);
 	}
 #endif
+
 	if (str_len) {
 		colon = memchr(str, ':', str_len - 1);
 	} else {
 		colon = NULL;
 	}
+
 	if (colon) {
-		*portno = atoi(colon + 1);
-		host = estrndup(str, colon - str);
-	} else {
-		if (get_err) {
-			*err = strpprintf(0, "Failed to parse address \"%s\"", str);
+		char *e = NULL;
+		*portno = strtol(colon + 1, &e, 10);
+		if (!e || !*e) {
+			return estrndup(str, colon - str);
 		}
-		return NULL;
 	}
 
-	return host;
+	if (get_err) {
+		*err = strpprintf(0, "Failed to parse address \"%s\"", str);
+	}
+	return NULL;
 }
 
 static inline char *parse_ip_address(php_stream_xport_param *xparam, int *portno)

programming_language: C++