diff --git a/ext/openssl/tests/close_in_early_data_cb_UAF.phpt b/ext/openssl/tests/close_in_early_data_cb_UAF.phpt new file mode 100644 index 000000000000..9f47600d2841 --- /dev/null +++ b/ext/openssl/tests/close_in_early_data_cb_UAF.phpt @@ -0,0 +1,93 @@ +--TEST-- +Closing the stream in the `early_data_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--CLEAN-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_id_context' => 'audit-early-data', + 'max_early_data' => 16384, + 'early_data_cb' => function ($stream, string $data): void { + fclose($stream); + }, +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); + +for ($connection = 0; $connection < 2; $connection++) { + $client = @stream_socket_accept($server, 10); + if ($client) { + fwrite($client, "response\n"); + fclose($client); + } +} +PHP; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'PHP' +$session = null; +$context = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function ($stream, $newSession) use (&$session): void { + $session = $newSession; + }, +]]); + +$first = stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +fgets($first); +fclose($first); + +$earlyDataContext = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $session, + 'early_data' => 'attacker-controlled-early-data', +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $earlyDataContext, +); +PHP; + +require 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('audit_early_data', $certFile); + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/tests/close_in_psk_client_cb_UAF.phpt b/ext/openssl/tests/close_in_psk_client_cb_UAF.phpt new file mode 100644 index 000000000000..d5d96c9b0b90 --- /dev/null +++ b/ext/openssl/tests/close_in_psk_client_cb_UAF.phpt @@ -0,0 +1,61 @@ +--TEST-- +Closing the stream in the `psk_client_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--FILE-- + [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER, + 'ciphers' => 'PSK', + 'psk_server_cb' => static function ($stream, string $identity): ?Openssl\Psk { + if ($identity !== 'audit-client') { + return null; + } + + return new Openssl\Psk(str_repeat("\x41", 32)); + }, +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); +@stream_socket_accept($server, 10); +PHP; + +$clientCode = <<<'PHP' +$context = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'ciphers' => 'PSK', + 'verify_peer' => false, + 'verify_peer_name' => false, + 'psk_client_cb' => static function ($stream): ?Openssl\Psk { + fclose($stream); + + return new Openssl\Psk(str_repeat("\x41", 32), 'audit-client'); + }, +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +PHP; + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/tests/close_in_psk_server_cb_UAF.phpt b/ext/openssl/tests/close_in_psk_server_cb_UAF.phpt new file mode 100644 index 000000000000..3a561ef877e8 --- /dev/null +++ b/ext/openssl/tests/close_in_psk_server_cb_UAF.phpt @@ -0,0 +1,55 @@ +--TEST-- +Closing the stream in the `psk_server_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--FILE-- + [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_SERVER, + 'psk_server_cb' => function ($stream, string $identity): ?Openssl\Psk { + fclose($stream); + return null; + }, +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); +@stream_socket_accept($server, 10); +PHP; + +$clientCode = <<<'PHP' +$context = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'psk_client_cb' => fn ($stream) => new Openssl\Psk( + str_repeat("\x42", 32), + 'remote-identity', + ), +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +PHP; + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/tests/close_in_session_get_cb_UAF.phpt b/ext/openssl/tests/close_in_session_get_cb_UAF.phpt new file mode 100644 index 000000000000..edf926c19c19 --- /dev/null +++ b/ext/openssl/tests/close_in_session_get_cb_UAF.phpt @@ -0,0 +1,103 @@ +--TEST-- +Closing the stream in the `session_get_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--CLEAN-- + +--FILE-- + [ + 'local_cert' => '%s', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER, + 'session_id_context' => 'audit-session-get', + 'session_new_cb' => static function ($stream, $session) use (&$sessions): void { + $sessions[bin2hex($session->id)] = $session; + }, + 'session_get_cb' => static function ($stream, string $sessionId) use (&$sessions) { + $session = $sessions[bin2hex($sessionId)] ?? null; + fclose($stream); + + return $session; + }, +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); + +for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 10); + if ($client) { + fwrite($client, "response\n"); + fclose($client); + } +} +PHP; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'PHP' +$session = null; +$context = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => static function ($stream, $newSession) use (&$session): void { + $session = $newSession; + }, +]]); + +$first = stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +if ($first) { + @fgets($first); + fclose($first); +} + +$resumeContext = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $session, +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $resumeContext, +); +PHP; + +require 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('audit_session_get', $certFile); + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/tests/close_in_session_new_cb_UAF.phpt b/ext/openssl/tests/close_in_session_new_cb_UAF.phpt new file mode 100644 index 000000000000..1b088f6711f6 --- /dev/null +++ b/ext/openssl/tests/close_in_session_new_cb_UAF.phpt @@ -0,0 +1,71 @@ +--TEST-- +Closing the stream in the `session_new_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--CLEAN-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_cache' => true, + 'session_id_context' => 'audit-session', +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); +$client = @stream_socket_accept($server, 10); +if ($client) { + fwrite($client, "ticket\n"); + fclose($client); +} +PHP; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'PHP' +$context = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function ($stream, $session): void { + fclose($stream); + }, +]]); + +$client = @stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +if ($client) { + @fgets($client); +} +PHP; + +require 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('audit_session', $certFile); + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 706f4ec5c80f..57f6f2c763c9 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1788,11 +1788,19 @@ static unsigned int php_openssl_psk_client_cb(SSL *ssl, const char *hint, return 0; } + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zval result; if (php_openssl_call_psk_cb(stream, &sslsock->psk_callbacks->client_cb, NULL, 0, &result) != SUCCESS) { + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } return 0; } + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } if (Z_TYPE(result) == IS_NULL) { return 0; @@ -1962,11 +1970,19 @@ static int php_openssl_psk_find_session_cb(SSL *ssl, const unsigned char *identi return 1; } + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zval result; if (php_openssl_call_psk_cb(stream, &sslsock->psk_callbacks->server_cb, identity, identity_len, &result) != SUCCESS) { + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } return 0; } + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } if (Z_TYPE(result) == IS_NULL) { return 1; /* identity unknown - let handshake fall through */ @@ -2200,7 +2216,12 @@ static int php_openssl_session_new_cb(SSL *ssl, SSL_SESSION *session) ZVAL_RES(&args[0], stream->res); php_openssl_session_object_init(&args[1], session); + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zend_call_known_fcc(&sslsock->session_callbacks->new_cb, NULL, 2, args, NULL); + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } zval_ptr_dtor(&args[1]); @@ -2233,7 +2254,12 @@ static SSL_SESSION *php_openssl_session_get_cb(SSL *ssl, const unsigned char *se SSL_SESSION *session = NULL; + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zend_call_known_fcc(&sslsock->session_callbacks->get_cb, &retval, 2, args, NULL); + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } zval_ptr_dtor(&args[1]); if (php_openssl_is_session_ce(&retval)) { @@ -2952,8 +2978,13 @@ static int php_openssl_handshake_server_early_data(php_stream *stream, zval args[2]; ZVAL_RES(&args[0], stream->res); ZVAL_STRINGL(&args[1], (char *) buf, readbytes); + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zend_call_known_fcc(&sslsock->early_data_callbacks->read_cb, NULL, 2, args, NULL); + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } zval_ptr_dtor(&args[1]); } continue;