CVE-2017-9118
Patch Available.
This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.
Overview
PHP is a general-purpose scripting language used mostly for web development. It runs behind web servers through server APIs such as Apache mod_php and PHP-FPM, and powers content management systems and frameworks such as WordPress, Drupal, Moodle and Laravel. Its PCRE extension provides regular expression functions such as preg_replace() and preg_replace_callback(), and is built into every PHP installation.
A Buffer Overflow vulnerability (CVE-2017-9118) has been identified in php_pcre_replace_impl(), the code behind PHP's preg_replace() and preg_replace_callback() functions, which allows an attacker who can make an application build a replacement result of extreme size to cause a heap out-of-bounds write, crashing the PHP process and possibly corrupting memory. PHP tracks this issue as bug #74604.
Per MITRE CWE-787: The product writes data past the end, or before the beginning, of the intended buffer.
This issue affects all versions of PHP before 7.4.27, PHP 8.0 before 8.0.14 and PHP 8.1 before 8.1.1, which includes the End-of-Life release lines PHP 5.6, 7.0, 7.1, 7.2 and 7.3 through their final releases. From PHP 7.0 on, only 32-bit builds can reach the flaw.
Details
Module Info
- Product: PHP
- Affected packages:
php-src(ext/pcre) - Affected versions: <7.4.27, >=8.0.0 <8.0.14, >=8.1.0 <8.1.1
- GitHub repository: https://github.com/php/php-src
- Published packages: https://www.php.net/downloads
- Package manager: N/A
- Fixed in:
- OSS PHP 7.4.27, 8.0.14, 8.1.1
- NES for PHP 5.6.1 (based on PHP 5.6.40)
- NES for PHP 7.2.1 (based on PHP 7.2.34)
Vulnerability Info
This High-severity vulnerability is found in the pcre extension. The CVE record, assigned by MITRE, gives no CVSS score (NVD scores it 7.5 under CVSS 3.1, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) and no affected version range beyond the version the reporter tested (PHP 7.1.5), and the PHP project reclassified the report from a security issue to an ordinary bug. It was fixed upstream in PHP 7.4.27, 8.0.14 and 8.1.1. The End-of-Life PHP 5.6, 7.0, 7.1, 7.2 and 7.3 release lines never received that fix.
preg_replace() builds its result piece by piece: the text before each match, then the replacement, and finally the rest of the subject. It grows the result buffer as it goes and tracks how much it has written. In PHP 5.6 that count is an int, passed in and updated through the result_len pointer, while the buffer size is a size_t. In PHP 5.6.40 the copy step reads:
if (new_len + 1 > alloc_len) {
new_buf = safe_emalloc(2, new_len + 1, alloc_len);
alloc_len = 1 + alloc_len + 2 * (size_t)new_len;
memcpy(new_buf, result, *result_len);
efree(result);
result = new_buf;
}
/* copy the part of the string before the match */
memcpy(&result[*result_len], piece, match-piece);
*result_len += match-piece;Once the result passes 2 GiB, the int count wraps negative, and the next piece is written at that negative offset from the buffer, outside the allocation. PHP 7 tracks these lengths as size_t, but its overflow-checked size computation did not include the header that zend_string_alloc() adds to every string. A result length just under SIZE_MAX can therefore wrap when the header is added, so a buffer far smaller than requested is allocated and the copy that follows writes past it. Such lengths are only reachable on 32-bit builds.
Exploitation needs an application that runs preg_replace() or preg_replace_callback() with attacker-controlled input large enough to produce a result of that size. HeroDevs' analysis puts the PHP 5.6 threshold at a result over 2 GiB, which also needs memory_limit to be disabled or set above that size. On PHP 7.2 the flaw can only be reached on 32-bit builds.
The upstream fix accounts for the maximum zend_string header overhead when it computes the new buffer size, so the size can no longer wrap:
if (new_len >= alloc_len) {
alloc_len = zend_safe_address_guarded(2, new_len, ZSTR_MAX_OVERHEAD) - ZSTR_MAX_OVERHEAD;
if (result == NULL) {
result = zend_string_alloc(alloc_len, 0);
} else {
result = zend_string_extend(result, alloc_len, 0);
}
}NES for PHP carries this fix to PHP 7.2. PHP 5.6 has its own form of the bug, so NES for PHP 5 computes each length as a size_t and refuses a result that would exceed 2 GiB with a "Result is too big" warning and a NULL result, before anything is copied.
Mitigation
The PHP project publishes security fixes only for its supported release lines. PHP 5.6, 7.0, 7.1, 7.2 and 7.3 are End-of-Life and will not receive any public updates to address this issue.
Until an upgrade is possible, keep memory_limit enabled and well below the sizes needed to reach the overflow, and limit the length of attacker-supplied input passed to preg_replace() and preg_replace_callback().
Users of the affected components should apply one of the following mitigations:
- Upgrade to PHP 7.4.27, 8.0.14, 8.1.1, or later.
- Leverage a commercial support partner like HeroDevs (NES for PHP) for post-EOL security support.
Credits
- whitehat002 (finder)
- Christoph M. Becker (remediation)