Skip to content

Missing error check and insufficient random bytes in HTTP Digest authentication for SOAP

Low
bukka published GHSA-76gg-c692-v2mw Jun 12, 2023

Package

No package listed

Affected versions

< 8.0.29
< 8.1.20
< 8.2.7

Patched versions

8.0.29
8.1.20
8.2.7

Description

This report was co-authored by @TimWolla

Summary

The random byte generation function used in the SOAP HTTP Digest authentication code is not checked for failure. This can result in a stack information leak. Furthermore, there's an insufficient number of random bytes used.

Details

Context:

php-src/ext/soap/php_http.c

Lines 664 to 671 in af809ef

php_random_bytes_throw(&nonce, sizeof(nonce));
nonce &= 0x7fffffff;
PHP_MD5Init(&md5ctx);
snprintf(cnonce, sizeof(cnonce), ZEND_LONG_FMT, nonce);
PHP_MD5Update(&md5ctx, (unsigned char*)cnonce, strlen(cnonce));
PHP_MD5Final(hash, &md5ctx);
make_digest(cnonce, hash);

If php_random_bytes_throw fails, the nonce will be uninitialized, but
still sent to the server. The client nonce is intended to protect
against a malicious server. See section 5.10 and 5.12 of RFC 7616,
and bullet point 2 below.

Tim pointed out that even though it's the MD5 of the nonce that gets sent,
enumerating 31 bits is trivial. So we have still a stack information leak
of 31 bits.

Furthermore, Tim found the following issues:

  • The small size of cnonce might cause the server to erroneously reject
    a request due to a repeated (cnonce, nc) pair. As per the birthday
    problem 31 bits of randomness will return a duplication with 50%
    chance after less than 55000 requests and nc always starts counting at 1.

  • The cnonce is intended to protect the client and password against a
    malicious server that returns a constant server nonce where the server
    precomputed a rainbow table between passwords and correct client response.
    As storage is fairly cheap, a server could precompute the client responses
    for (a subset of) client nonces and still have a chance of reversing the
    client response with the same probability as the cnonce duplication.

    Precomputing the rainbow table for all 2^31 cnonces increases the rainbow
    table size by factor 2 billion, which is infeasible. But precomputing it
    for 2^14 cnonces only increases the table size by factor 16k and the server
    would still have a 10% chance of successfully reversing a password with a
    single client request.

PoC

We do not have a proof-of-concept.

Impact

The weak randomness affects applications that use SOAP with HTTP Digest authentication against a possibly malicious server over HTTP. The stack information leak applies to both HTTP and HTTPS, but is only a few bytes.

Proposed patch

This patch fixes the issues by increasing the nonce size, and checking
the return value of php_random_bytes_throw(). In the process we also get
rid of the MD5 hashing of the nonce.

From f26fcd3a152a5c6b2d140cb35a5040671696f6e1 Mon Sep 17 00:00:00 2001
From: Niels Dossche <[email protected]>
Date: Sun, 16 Apr 2023 15:05:03 +0200
Subject: [PATCH] Fix missing randomness check and insufficient random bytes
 for SOAP HTTP Digest
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

If php_random_bytes_throw fails, the nonce will be uninitialized, but
still sent to the server. The client nonce is intended to protect
against a malicious server. See section 5.10 and 5.12 of RFC 7616 [1],
and bullet point 2 below.

Tim pointed out that even though it's the MD5 of the nonce that gets sent,
enumerating 31 bits is trivial. So we have still a stack information leak
of 31 bits.

Furthermore, Tim found the following issues:
* The small size of cnonce might cause the server to erroneously reject
  a request due to a repeated (cnonce, nc) pair. As per the birthday
  problem 31 bits of randomness will return a duplication with 50%
  chance after less than 55000 requests and nc always starts counting at 1.
* The cnonce is intended to protect the client and password against a
  malicious server that returns a constant server nonce where the server
  precomputed a rainbow table between passwords and correct client response.
  As storage is fairly cheap, a server could precompute the client responses
  for (a subset of) client nonces and still have a chance of reversing the
  client response with the same probability as the cnonce duplication.

  Precomputing the rainbow table for all 2^31 cnonces increases the rainbow
  table size by factor 2 billion, which is infeasible. But precomputing it
  for 2^14 cnonces only increases the table size by factor 16k and the server
  would still have a 10% chance of successfully reversing a password with a
  single client request.

This patch fixes the issues by increasing the nonce size, and checking
the return value of php_random_bytes_throw(). In the process we also get
rid of the MD5 hashing of the nonce.

[1] RFC 7616: https://www.rfc-editor.org/rfc/rfc7616

Co-authored-by: Tim Düsterhus <[email protected]>
---
 ext/soap/php_http.c | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c
index db3c97b647..a619949c69 100644
--- a/ext/soap/php_http.c
+++ b/ext/soap/php_http.c
@@ -657,18 +657,23 @@ try_again:
 			has_authorization = 1;
 			if (Z_TYPE_P(digest) == IS_ARRAY) {
 				char          HA1[33], HA2[33], response[33], cnonce[33], nc[9];
-				zend_long     nonce;
+				unsigned char nonce[16];
 				PHP_MD5_CTX   md5ctx;
 				unsigned char hash[16];
 
-				php_random_bytes_throw(&nonce, sizeof(nonce));
-				nonce &= 0x7fffffff;
+				if (UNEXPECTED(php_random_bytes_throw(&nonce, sizeof(nonce)) != SUCCESS)) {
+					ZEND_ASSERT(EG(exception));
+					php_stream_close(stream);
+					convert_to_null(Z_CLIENT_HTTPURL_P(this_ptr));
+					convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr));
+					convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr));
+					smart_str_free(&soap_headers_z);
+					smart_str_free(&soap_headers);
+					return FALSE;
+				}
 
-				PHP_MD5Init(&md5ctx);
-				snprintf(cnonce, sizeof(cnonce), ZEND_LONG_FMT, nonce);
-				PHP_MD5Update(&md5ctx, (unsigned char*)cnonce, strlen(cnonce));
-				PHP_MD5Final(hash, &md5ctx);
-				make_digest(cnonce, hash);
+				php_hash_bin2hex(cnonce, nonce, sizeof(nonce));
+				cnonce[32] = 0;
 
 				if ((tmp = zend_hash_str_find(Z_ARRVAL_P(digest), "nc", sizeof("nc")-1)) != NULL &&
 					Z_TYPE_P(tmp) == IS_LONG) {
-- 
2.40.0

Severity

Low

CVE ID

CVE-2023-3247

Weaknesses

Unchecked Return Value

The product does not check the return value from a method or function, which can prevent it from detecting unexpected states and conditions. Learn more on MITRE.

Small Space of Random Values

The number of possible random values is smaller than needed by the product, making it more susceptible to brute force attacks. Learn more on MITRE.

Credits