diff options
Diffstat (limited to 'ncat/ncat_ssl.c')
-rw-r--r-- | ncat/ncat_ssl.c | 669 |
1 files changed, 669 insertions, 0 deletions
diff --git a/ncat/ncat_ssl.c b/ncat/ncat_ssl.c new file mode 100644 index 0000000..43069c6 --- /dev/null +++ b/ncat/ncat_ssl.c @@ -0,0 +1,669 @@ +/*************************************************************************** + * ncat_ssl.c -- SSL support functions. * + ***********************IMPORTANT NMAP LICENSE TERMS************************ + * + * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap + * Project"). Nmap is also a registered trademark of the Nmap Project. + * + * This program is distributed under the terms of the Nmap Public Source + * License (NPSL). The exact license text applying to a particular Nmap + * release or source code control revision is contained in the LICENSE + * file distributed with that version of Nmap or source code control + * revision. More Nmap copyright/legal information is available from + * https://nmap.org/book/man-legal.html, and further information on the + * NPSL license itself can be found at https://nmap.org/npsl/ . This + * header summarizes some key points from the Nmap license, but is no + * substitute for the actual license text. + * + * Nmap is generally free for end users to download and use themselves, + * including commercial use. It is available from https://nmap.org. + * + * The Nmap license generally prohibits companies from using and + * redistributing Nmap in commercial products, but we sell a special Nmap + * OEM Edition with a more permissive license and special features for + * this purpose. See https://nmap.org/oem/ + * + * If you have received a written Nmap license agreement or contract + * stating terms other than these (such as an Nmap OEM license), you may + * choose to use and redistribute Nmap under those terms instead. + * + * The official Nmap Windows builds include the Npcap software + * (https://npcap.com) for packet capture and transmission. It is under + * separate license terms which forbid redistribution without special + * permission. So the official Nmap Windows builds may not be redistributed + * without special permission (such as an Nmap OEM license). + * + * Source is provided to this software because we believe users have a + * right to know exactly what a program is going to do before they run it. + * This also allows you to audit the software for security holes. + * + * Source code also allows you to port Nmap to new platforms, fix bugs, and add + * new features. You are highly encouraged to submit your changes as a Github PR + * or by email to the dev@nmap.org mailing list for possible incorporation into + * the main distribution. Unless you specify otherwise, it is understood that + * you are offering us very broad rights to use your submissions as described in + * the Nmap Public Source License Contributor Agreement. This is important + * because we fund the project by selling licenses with various terms, and also + * because the inability to relicense code has caused devastating problems for + * other Free Software projects (such as KDE and NASM). + * + * The free version of Nmap is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, + * indemnification and commercial support are all available through the + * Npcap OEM program--see https://nmap.org/oem/ + * + ***************************************************************************/ + +/* $Id$ */ + +#include "nbase.h" +#include "ncat_config.h" +#ifdef HAVE_OPENSSL +#include "nsock.h" +#include "ncat.h" + +#include <stdio.h> +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/rsa.h> +#include <openssl/rand.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> + +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && !defined LIBRESSL_VERSION_NUMBER +#define HAVE_OPAQUE_STRUCTS 1 +#define FUNC_ASN1_STRING_data ASN1_STRING_get0_data +#else +#define FUNC_ASN1_STRING_data ASN1_STRING_data +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +#include <openssl/provider.h> +/* Deprecated in OpenSSL 3.0 */ +#define SSL_get_peer_certificate SSL_get1_peer_certificate +#else +#include <openssl/bn.h> +#endif + +/* Required for windows compilation to Eliminate APPLINK errors. + See http://www.openssl.org/support/faq.html#PROG2 */ +#ifdef WIN32 +#include <openssl/applink.c> +#endif + +static SSL_CTX *sslctx; + +static int ssl_gen_cert(X509 **cert, EVP_PKEY **key); + +/* Parameters for automatic key and certificate generation. */ +enum { + DEFAULT_KEY_BITS = 2048, + DEFAULT_CERT_DURATION = 60 * 60 * 24 * 365, +}; +#define CERTIFICATE_COMMENT "Automatically generated by Ncat. See https://nmap.org/ncat/." + +SSL_CTX *setup_ssl_listen(const SSL_METHOD *method) +{ + if (sslctx) + goto done; + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined LIBRESSL_VERSION_NUMBER + SSL_library_init(); + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); + SSL_load_error_strings(); +#elif OPENSSL_VERSION_NUMBER >= 0x30000000L + if (NULL == OSSL_PROVIDER_load(NULL, "legacy") && o.debug) + { + loguser("OpenSSL legacy provider failed to load: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + if (NULL == OSSL_PROVIDER_load(NULL, "default")) + { + loguser("OpenSSL default provider failed to load: %s", + ERR_error_string(ERR_get_error(), NULL)); + } +#endif + + /* RAND_status initializes the random number generator through a variety of + platform-dependent methods, then returns 1 if there is enough entropy or + 0 otherwise. This seems to be a good platform-independent way of seeding + the generator, as well as of refusing to continue without enough + entropy. */ + if (!RAND_status()) + bye("Failed to seed OpenSSL PRNG (RAND_status returned false)."); + + if (!method) + bye("Invalid SSL method: %s.", ERR_error_string(ERR_get_error(), NULL)); + if (!(sslctx = SSL_CTX_new(method))) + bye("SSL_CTX_new(): %s.", ERR_error_string(ERR_get_error(), NULL)); + + SSL_CTX_set_options(sslctx, SSL_OP_ALL | SSL_OP_NO_SSLv2); + + /* Secure ciphers list taken from Nsock. */ + if (o.sslciphers == NULL) { + if (!SSL_CTX_set_cipher_list(sslctx, "ALL:!aNULL:!eNULL:!LOW:!EXP:!RC4:!MD5:@STRENGTH")) + bye("Unable to set OpenSSL cipher list: %s", ERR_error_string(ERR_get_error(), NULL)); + } + else { + if (!SSL_CTX_set_cipher_list(sslctx, o.sslciphers)) + bye("Unable to set OpenSSL cipher list: %s", ERR_error_string(ERR_get_error(), NULL)); + } + + if (o.sslcert == NULL && o.sslkey == NULL) { + X509 *cert; + EVP_PKEY *key; + char digest_buf[SHA1_STRING_LENGTH + 1]; + + if (o.verbose) + loguser("Generating a temporary %d-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.\n", DEFAULT_KEY_BITS); + if (ssl_gen_cert(&cert, &key) == 0) + bye("ssl_gen_cert(): %s.", ERR_error_string(ERR_get_error(), NULL)); + if (o.verbose) { + char *fp; + fp = ssl_cert_fp_str_sha1(cert, digest_buf, sizeof(digest_buf)); + ncat_assert(fp == digest_buf); + loguser("SHA-1 fingerprint: %s\n", digest_buf); + } + if (SSL_CTX_use_certificate(sslctx, cert) != 1) + bye("SSL_CTX_use_certificate(): %s.", ERR_error_string(ERR_get_error(), NULL)); + if (SSL_CTX_use_PrivateKey(sslctx, key) != 1) + bye("SSL_CTX_use_PrivateKey(): %s.", ERR_error_string(ERR_get_error(), NULL)); + X509_free(cert); + EVP_PKEY_free(key); + } else { + if (o.sslcert == NULL || o.sslkey == NULL) + bye("The --ssl-key and --ssl-cert options must be used together."); + if (SSL_CTX_use_certificate_chain_file(sslctx, o.sslcert) != 1) + bye("SSL_CTX_use_certificate_chain_file(): %s.", ERR_error_string(ERR_get_error(), NULL)); + if (SSL_CTX_use_PrivateKey_file(sslctx, o.sslkey, SSL_FILETYPE_PEM) != 1) + bye("SSL_CTX_use_Privatekey_file(): %s.", ERR_error_string(ERR_get_error(), NULL)); + } + +done: + return sslctx; +} + +SSL *new_ssl(int fd) +{ + SSL *ssl; + + if (!(ssl = SSL_new(sslctx))) + bye("SSL_new(): %s.", ERR_error_string(ERR_get_error(), NULL)); + if (!SSL_set_fd(ssl, fd)) + bye("SSL_set_fd(): %s.", ERR_error_string(ERR_get_error(), NULL)); + + return ssl; +} + +/* Match a (user-supplied) hostname against a (certificate-supplied) name, which + may be a wildcard pattern. A wildcard pattern may contain only one '*', it + must be the entire leftmost component, and there must be at least two + components following it. len is the length of pattern; pattern may contain + null bytes so that len != strlen(pattern); pattern may also not be null terminated. + hostname *must* be null-terminated. */ +static int wildcard_match(const char *pattern, const char *hostname, int len) +{ + const char *p = pattern; + const char *h = hostname; + int remaining = len; + if (len > 1 && pattern[0] == '*' && pattern[1] == '.') { + /* A wildcard pattern. */ + const char *dot; + + /* Skip the wildcard component. */ + p += 2; + remaining -= 2; + + /* Ensure there are no more wildcard characters. */ + if (memchr(p, '*', remaining) != NULL) + return 0; + + /* Ensure there's at least one more dot, not counting a dot at the + end. */ + dot = (const char *) memchr(p, '.', remaining); + if (dot == NULL /* not found */ + || dot - p == remaining /* dot in last position */ + || *(dot + 1) == '\0') /* dot immediately before null terminator */ + { + if (o.debug > 1) { + logdebug("Wildcard name \"%.*s\" doesn't have at least two" + " components after the wildcard; rejecting.\n", len, pattern); + } + return 0; + } + + /* Skip the leftmost hostname component. */ + h = strchr(hostname, '.'); + if (h == NULL) + return 0; + h++; + + } + /* Compare what remains of the pattern and hostname. */ + /* Normal string comparison. Check the name length because I'm concerned + about someone somehow embedding a '\0' in the subject and matching + against a shorter name. */ + return remaining == strlen(h) && strncmp(p, h, remaining) == 0; +} + +/* Match a hostname against the contents of a dNSName field of the + subjectAltName extension, if present. This is the preferred place for a + certificate to store its domain name, as opposed to in the commonName field. + It has the advantage that multiple names can be stored, so that one + certificate can match both "example.com" and "www.example.com". + + If num_checked is not NULL, the number of dNSName fields that were checked + before returning will be stored in it. This is so you can distinguish between + the check failing because there were names but none matched, or because there + were no names to match. */ +static int cert_match_dnsname(X509 *cert, const char *hostname, + unsigned int *num_checked) +{ + X509_EXTENSION *ext; + STACK_OF(GENERAL_NAME) *gen_names; + const X509V3_EXT_METHOD *method; + unsigned char *data; + int i; + + if (num_checked != NULL) + *num_checked = 0; + + i = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1); + if (i < 0) + return 0; + /* If there's more than one subjectAltName extension, forget it. */ + if (X509_get_ext_by_NID(cert, NID_subject_alt_name, i) >= 0) + return 0; + ext = X509_get_ext(cert, i); + + /* See the function X509V3_EXT_print in the OpenSSL source for this method + of getting a string value from an extension. */ + method = X509V3_EXT_get(ext); + if (method == NULL) + return 0; + + /* We must copy this address into a temporary variable because ASN1_item_d2i + increments it. We don't want it to corrupt ext->value->data. */ + ASN1_OCTET_STRING* asn1_str = X509_EXTENSION_get_data(ext); + data = asn1_str->data; + /* Here we rely on the fact that the internal representation (the "i" in + "i2d") for NID_subject_alt_name is STACK_OF(GENERAL_NAME). Converting it + to a stack of CONF_VALUE with a i2v method is not satisfactory, because a + CONF_VALUE doesn't contain the length of the value so you can't know the + presence of null bytes. */ +#if (OPENSSL_VERSION_NUMBER > 0x00907000L) + if (method->it != NULL) { + ASN1_OCTET_STRING* asn1_str_a = X509_EXTENSION_get_data(ext); + gen_names = (STACK_OF(GENERAL_NAME) *) ASN1_item_d2i(NULL, + (const unsigned char **) &data, + asn1_str_a->length, ASN1_ITEM_ptr(method->it)); + } else { + ASN1_OCTET_STRING* asn1_str_b = X509_EXTENSION_get_data(ext); + gen_names = (STACK_OF(GENERAL_NAME) *) method->d2i(NULL, + (const unsigned char **) &data, + asn1_str_b->length); + } +#else + gen_names = (STACK_OF(GENERAL_NAME) *) method->d2i(NULL, + (const unsigned char **) &data, + ext->value->length); +#endif + if (gen_names == NULL) + return 0; + + /* Look for a dNSName field with a matching hostname. There may be more than + one dNSName field. */ + for (i = 0; i < sk_GENERAL_NAME_num(gen_names); i++) { + GENERAL_NAME *gen_name; + + gen_name = sk_GENERAL_NAME_value(gen_names, i); + if (gen_name->type == GEN_DNS) { + const char *dnsname = (const char *) FUNC_ASN1_STRING_data(gen_name->d.dNSName); + int dnslen = ASN1_STRING_length(gen_name->d.dNSName); + if (o.debug > 1) + logdebug("Checking certificate DNS name \"%.*s\" against \"%s\".\n", dnslen, dnsname, hostname); + if (num_checked != NULL) + (*num_checked)++; + if (wildcard_match(dnsname, hostname, dnslen)) + return 1; + } + } + + return 0; +} + +/* Returns the number of contiguous blocks of bytes in pattern that do not + contain the '.' byte. */ +static unsigned int num_components(const unsigned char *pattern, size_t len) +{ + const unsigned char *p; + unsigned int count; + + count = 0; + p = pattern; + for (;;) { + while (p - pattern < len && *p == '.') + p++; + if (p - pattern >= len) + break; + while (p - pattern < len && *p != '.') + p++; + count++; + } + + return count; +} + +/* Returns true if the a pattern is strictly less specific than the b + pattern. */ +static int less_specific(const unsigned char *a, size_t a_len, + const unsigned char *b, size_t b_len) +{ + /* Wildcard patterns are always less specific than non-wildcard patterns. */ + if (memchr(a, '*', a_len) != NULL && memchr(b, '*', b_len) == NULL) + return 1; + if (memchr(a, '*', a_len) == NULL && memchr(b, '*', b_len) != NULL) + return 0; + + return num_components(a, a_len) < num_components(b, b_len); +} + +static int most_specific_commonname(X509_NAME *subject, const char **result) +{ + ASN1_STRING *best, *cur; + int i; + + i = -1; + best = NULL; + while ((i = X509_NAME_get_index_by_NID(subject, NID_commonName, i)) != -1) { + cur = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject, i)); + /* We use "not less specific" instead of "more specific" to allow later + entries to supersede earlier ones. */ + if (best == NULL + || !less_specific(FUNC_ASN1_STRING_data(cur), ASN1_STRING_length(cur), + FUNC_ASN1_STRING_data(best), ASN1_STRING_length(best))) { + best = cur; + } + } + + if (best == NULL) { + *result = NULL; + return -1; + } else { + *result = (char *) FUNC_ASN1_STRING_data(best); + return ASN1_STRING_length(best); + } +} + +/* Match a hostname against the contents of the "most specific" commonName field + of a certificate. The "most specific" term is used in RFC 2818 but is not + defined anywhere that I (David Fifield) can find. This is what it means in + Ncat: wildcard patterns are always less specific than non-wildcard patterns. + If both patterns are wildcard or both are non-wildcard, the one with more + name components is more specific. If two names have the same number of + components, the one that comes later in the certificate is more specific. */ +static int cert_match_commonname(X509 *cert, const char *hostname) +{ + X509_NAME *subject; + const char *commonname; + int n; + + subject = X509_get_subject_name(cert); + if (subject == NULL) + return 0; + + n = most_specific_commonname(subject, &commonname); + if (n < 0 || commonname == NULL) + /* No commonName found. */ + return 0; + if (wildcard_match(commonname, hostname, n)) + return 1; + + if (o.verbose) + loguser("Certificate verification error: Connected to \"%s\", but certificate is for \"%s\".\n", hostname, commonname); + + return 0; +} + +/* Verify a host's name against the name in its certificate after connection. + If the verify mode is SSL_VERIFY_NONE, always returns true. Returns nonzero + on success. */ +int ssl_post_connect_check(SSL *ssl, const char *hostname) +{ + X509 *cert = NULL; + unsigned int num_checked; + + if (SSL_get_verify_mode(ssl) == SSL_VERIFY_NONE) + return 1; + + if (hostname == NULL) + return 0; + + cert = SSL_get_peer_certificate(ssl); + if (cert == NULL) + return 0; + + /* RFC 2818 (HTTP Over TLS): If a subjectAltName extension of type dNSName + is present, that MUST be used as the identity. Otherwise, the (most + specific) Common Name field in the Subject field of the certificate MUST + be used. Although the use of the Common Name is existing practice, it is + deprecated and Certification Authorities are encouraged to use the + dNSName instead. */ + if (!cert_match_dnsname(cert, hostname, &num_checked)) { + /* If there were dNSNames, we're done. If not, try the commonNames. */ + if (num_checked > 0 || !cert_match_commonname(cert, hostname)) { + X509_free(cert); + return 0; + } + } + + X509_free(cert); + + return SSL_get_verify_result(ssl) == X509_V_OK; +} + +/* Generate a self-signed certificate and matching RSA keypair. References for + this code are the book Network Programming with OpenSSL, chapter 10, section + "Making Certificates"; and apps/req.c in the OpenSSL source. */ +static int ssl_gen_cert(X509 **cert, EVP_PKEY **key) +{ + X509_NAME *subj; + X509_EXTENSION *ext; + X509V3_CTX ctx; + const char *commonName = "localhost"; + char dNSName[128]; + int rc; +#if OPENSSL_VERSION_NUMBER < 0x30000000L + int ret = 0; + RSA *rsa = NULL; + BIGNUM *bne = NULL; + + *cert = NULL; + *key = NULL; + + /* Generate a private key. */ + *key = EVP_PKEY_new(); + if (*key == NULL) + goto err; + do { + /* Generate RSA key. */ + bne = BN_new(); + ret = BN_set_word(bne, RSA_F4); + if (ret != 1) + goto err; + + rsa = RSA_new(); + ret = RSA_generate_key_ex(rsa, DEFAULT_KEY_BITS, bne, NULL); + if (ret != 1) + goto err; + + rc = RSA_check_key(rsa); + } while (rc == 0); + if (rc == -1) + bye("Error generating RSA key: %s", ERR_error_string(ERR_get_error(), NULL)); + if (EVP_PKEY_assign_RSA(*key, rsa) == 0) { + RSA_free(rsa); + goto err; + } +#else + *cert = NULL; + *key = EVP_RSA_gen(DEFAULT_KEY_BITS); + if (*key == NULL) + goto err; +#endif + + /* Generate a certificate. */ + *cert = X509_new(); + if (*cert == NULL) + goto err; + if (X509_set_version(*cert, 2) == 0) /* Version 3. */ + goto err; + ASN1_INTEGER_set(X509_get_serialNumber(*cert), get_random_u32() & 0x7FFFFFFF); + + /* Set the commonName. */ + subj = X509_get_subject_name(*cert); + if (o.target != NULL) + commonName = o.target; + if (X509_NAME_add_entry_by_txt(subj, "commonName", MBSTRING_ASC, + (unsigned char *) commonName, -1, -1, 0) == 0) { + goto err; + } + + /* Set the dNSName. */ + rc = Snprintf(dNSName, sizeof(dNSName), "DNS:%s", commonName); + if (rc < 0 || rc >= sizeof(dNSName)) + goto err; + X509V3_set_ctx(&ctx, *cert, *cert, NULL, NULL, 0); + ext = X509V3_EXT_conf(NULL, &ctx, "subjectAltName", dNSName); + if (ext == NULL) + goto err; + if (X509_add_ext(*cert, ext, -1) == 0) + goto err; + + /* Set a comment. */ + ext = X509V3_EXT_conf(NULL, &ctx, "nsComment", CERTIFICATE_COMMENT); + if (ext == NULL) + goto err; + if (X509_add_ext(*cert, ext, -1) == 0) + goto err; + +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && !defined LIBRESSL_VERSION_NUMBER + { + ASN1_TIME *tb, *ta; + tb = NULL; + ta = NULL; + + if (X509_set_issuer_name(*cert, X509_get_subject_name(*cert)) == 0 + || (tb = ASN1_STRING_dup(X509_get0_notBefore(*cert))) == 0 + || X509_gmtime_adj(tb, 0) == 0 + || X509_set1_notBefore(*cert, tb) == 0 + || (ta = ASN1_STRING_dup(X509_get0_notAfter(*cert))) == 0 + || X509_gmtime_adj(ta, DEFAULT_CERT_DURATION) == 0 + || X509_set1_notAfter(*cert, ta) == 0 + || X509_set_pubkey(*cert, *key) == 0) { + ASN1_STRING_free(tb); + ASN1_STRING_free(ta); + goto err; + } + ASN1_STRING_free(tb); + ASN1_STRING_free(ta); + } +#else + if (X509_set_issuer_name(*cert, X509_get_subject_name(*cert)) == 0 + || X509_gmtime_adj(X509_get_notBefore(*cert), 0) == 0 + || X509_gmtime_adj(X509_get_notAfter(*cert), DEFAULT_CERT_DURATION) == 0 + || X509_set_pubkey(*cert, *key) == 0) { + goto err; + } +#endif + + /* Sign it. */ + if (X509_sign(*cert, *key, EVP_sha1()) == 0) + goto err; + + return 1; + +err: + if (*cert != NULL) + X509_free(*cert); + if (*key != NULL) + EVP_PKEY_free(*key); + + return 0; +} + +/* Calculate a SHA-1 fingerprint of a certificate and format it as a + human-readable string. Returns strbuf or NULL on error. */ +char *ssl_cert_fp_str_sha1(const X509 *cert, char *strbuf, size_t len) +{ + unsigned char binbuf[SHA1_BYTES]; + unsigned int n; + char *p; + unsigned int i; + + if (len < SHA1_STRING_LENGTH + 1) + return NULL; + n = sizeof(binbuf); + if (X509_digest(cert, EVP_sha1(), binbuf, &n) != 1) + return NULL; + + p = strbuf; + for (i = 0; i < n; i++) { + if (i > 0 && i % 2 == 0) + *p++ = ' '; + Snprintf(p, 3, "%02X", binbuf[i]); + p += 2; + } + ncat_assert(p - strbuf <= len); + *p = '\0'; + + return strbuf; +} + +/* Tries to complete an ssl handshake on the socket received by fdinfo struct + if ssl is enabled on that socket. */ + +int ssl_handshake(struct fdinfo *sinfo) +{ + int ret = 0; + int sslerr = 0; + + if (sinfo == NULL) { + if (o.debug) + logdebug("ncat_ssl.c: Invoking ssl_handshake() with a NULL parameter " + "is a serious bug. Please fix it.\n"); + return -1; + } + + if (!o.ssl) + return -1; + + /* Initialize the socket too if it isn't. */ + if (!sinfo->ssl) + sinfo->ssl = new_ssl(sinfo->fd); + + ret = SSL_accept(sinfo->ssl); + + if (ret == 1) + return NCAT_SSL_HANDSHAKE_COMPLETED; + + sslerr = SSL_get_error(sinfo->ssl, ret); + + if (ret == -1) { + if (sslerr == SSL_ERROR_WANT_READ) + return NCAT_SSL_HANDSHAKE_PENDING_READ; + if (sslerr == SSL_ERROR_WANT_WRITE) + return NCAT_SSL_HANDSHAKE_PENDING_WRITE; + } + + if (o.verbose) { + loguser("Failed SSL connection from %s: %s\n", + inet_socktop(&sinfo->remoteaddr), + ERR_error_string(ERR_get_error(), NULL)); + } + return NCAT_SSL_HANDSHAKE_FAILED; +} + +#endif |