From ec4ceb2c20598a98b5616e2b5ad4552a6064b037 Mon Sep 17 00:00:00 2001 From: Maurizio S <46047144+akabe1@users.noreply.github.com> Date: Sat, 20 Jan 2024 11:49:05 +0100 Subject: [PATCH 1/5] Add mTLS feature Added new feature to support mutual TLS via client certificate and private key, when a remote server requires client authentication. --- testssl.sh | 81 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/testssl.sh b/testssl.sh index fb689df..4c50f24 100755 --- a/testssl.sh +++ b/testssl.sh @@ -389,6 +389,7 @@ XMPP_HOST="" PROXYIP="" # $PROXYIP:$PROXPORT is your proxy if --proxy is defined ... PROXYPORT="" # ... and openssl has proxy support PROXY="" # Once check_proxy() executed it contains $PROXYIP:$PROXPORT +MTLS="" # mTLS authentication with client certificate and private key VULN_COUNT=0 SERVICE="" # Is the server running an HTTP server, SMTP, POP or IMAP? URI="" @@ -2316,6 +2317,12 @@ s_client_options() { fi # $keyopts may be set as an environment variable to enable client authentication (see PR #1383) tm_out "$options $keyopts" + + # In case of mutual TLS authentication is required by the server + # Note: the PEM certificate file must contain: client certificate and certificate key (not encrypted) + if [[ -n "$MTLS" ]]; then + options+=" -cert $MTLS" + fi } ###### check code starts here ###### @@ -2375,10 +2382,14 @@ service_detection() { out " $SERVICE, thus skipping HTTP specific checks" fileout "${jsonID}" "INFO" "$SERVICE, thus skipping HTTP specific checks" ;; - *) if [[ "$CLIENT_AUTH" == required ]]; then - out " certificate-based authentication => skipping all HTTP checks" - echo "certificate-based authentication => skipping all HTTP checks" >$TMPFILE - fileout "${jsonID}" "INFO" "certificate-based authentication => skipping all HTTP checks" + *) if [[ ! -z $MTLS ]]; then + out " not identified, but mTLS authentication is set ==> trying HTTP checks" + SERVICE=HTTP + fileout "${jsonID}" "DEBUG" "Couldn't determine service -- ASSUME_HTTP set" + elif [[ "$CLIENT_AUTH" == required ]] && [[ -z $MTLS ]]; then + out " certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" + echo "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" >$TMPFILE + fileout "${jsonID}" "INFO" "certificate-based authentication without providing client certificate and private key => skipping all HTTP checks" else out " Couldn't determine what's running on port $PORT" if "$ASSUME_HTTP"; then @@ -2430,6 +2441,7 @@ run_http_header() { local url redirect local jsonID="HTTP_status_code" local spaces=" " + local cert_option="" HEADERFILE=$TEMPDIR/$NODEIP.http_header.txt if [[ $NR_HEADER_FAIL -eq 0 ]]; then @@ -2444,12 +2456,17 @@ run_http_header() { pr_bold " HTTP Status Code " [[ -z "$1" ]] && url="/" || url="$1" - tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE & + + # Set -cert option value if mTLS authentication is selected + if [[ ! -z "$MTLS" ]]; then + cert_option="-cert $MTLS" + fi + tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS $cert_option -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE & wait_kill $! $HEADER_MAXSLEEP if [[ $? -eq 0 ]]; then # Issue HTTP GET again as it properly finished within $HEADER_MAXSLEEP and didn't hang. # Doing it again in the foreground to get an accurate header time - tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE + tm_out "$GET_REQ11" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS $cert_option -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") >$HEADERFILE 2>$ERRFILE NOW_TIME=$(date "+%s") HTTP_TIME=$(awk -F': ' '/^date:/ { print $2 } /^Date:/ { print $2 }' $HEADERFILE) HTTP_AGE=$(awk -F': ' '/^[aA][gG][eE]: / { print $2 }' $HEADERFILE) @@ -2601,7 +2618,7 @@ run_http_date() { local spaces=" " jsonID="HTTP_clock_skew" - if [[ $SERVICE != HTTP ]] || [[ "$CLIENT_AUTH" == required ]]; then + if [[ $SERVICE != HTTP ]] || { [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]]; }; then return 0 fi if [[ ! -s $HEADERFILE ]]; then @@ -6710,6 +6727,12 @@ sub_session_resumption() { local sess_data=$(mktemp $TEMPDIR/sub_session_data_resumption.$NODEIP.XXXXXX) local -a rw_line local protocol="$1" + local cert_option="" + + # Set -cert option value if mTLS authentication is selected + if [[ ! -z "$MTLS" ]]; then + cert_option="-cert $MTLS" + fi if [[ "$2" == ID ]]; then local byID=true @@ -6721,7 +6744,8 @@ sub_session_resumption() { return 1 fi fi - [[ "$CLIENT_AUTH" == required ]] && return 6 + # Return 6 if client authentication is required and none PEM file (containing client certificate+private key) is provided + [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]] && return 6 if ! "$HAS_TLS13" && "$HAS_NO_SSL2"; then addcmd+=" -no_ssl2" else @@ -6738,7 +6762,7 @@ sub_session_resumption() { addcmd+=" $protocol" fi - $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $addcmd -sess_out $sess_data") $tmpfile + $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $cert_option $addcmd -sess_out $sess_data") $tmpfile ret1=$? if [[ $ret1 -ne 0 ]]; then # MacOS and LibreSSL return 1 here, that's why we need to check whether the handshake contains e.g. a certificate @@ -6756,7 +6780,7 @@ sub_session_resumption() { # [[ ! $(<$sess_data) =~ -----.*\ SSL\ SESSION\ PARAMETERS----- ]] ret=2 else - $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $addcmd -sess_in $sess_data") $tmpfile 2>$ERRFILE + $OPENSSL s_client $(s_client_options "$STARTTLS $BUGS -connect $NODEIP:$PORT $PROXY $SNI $cert_option $addcmd -sess_in $sess_data") $tmpfile 2>$ERRFILE ret2=$? if [[ $DEBUG -ge 2 ]]; then echo -n "$ret1, $ret2, " @@ -17037,9 +17061,9 @@ run_renego() { [[ $DEBUG -ge 1 ]] && out ", no renegotiation support in TLS 1.3 only servers" outln fileout "$jsonID" "OK" "not vulnerable, TLS 1.3 only" "$cve" "$cwe" - elif [[ "$CLIENT_AUTH" == required ]]; then - prln_warning "client x509-based authentication prevents this from being tested" - fileout "$jsonID" "WARN" "client x509-based authentication prevents this from being tested" + elif [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]]; then + prln_warning "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested" + fileout "$jsonID" "WARN" "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested" sec_client_renego=1 else # We will need $ERRFILE for mitigation detection @@ -17203,7 +17227,7 @@ run_crime() { fileout "$jsonID" "OK" "not vulnerable" "$cve" "$cwe" fi else - if [[ $SERVICE == HTTP ]] || [[ "$CLIENT_AUTH" == required ]]; then + if [[ $SERVICE == HTTP ]] || [[ "$CLIENT_AUTH" == required ]] || [[ ! -z "$MTLS" ]]; then pr_svrty_high "VULNERABLE (NOT ok)" fileout "$jsonID" "HIGH" "VULNERABLE" "$cve" "$cwe" "$hint" else @@ -17262,8 +17286,13 @@ sub_breach_helper() { local get_command="$1" local detected_compression="" local -i was_killed=0 + local cert_option="" - safe_echo "$get_command" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") 1>$TMPFILE 2>$ERRFILE & + # Set -cert option value if mTLS authentication is selected + if [[ ! -z "$MTLS" ]]; then + cert_option="-cert $MTLS" + fi + safe_echo "$get_command" | $OPENSSL s_client $(s_client_options "$OPTIMAL_PROTO $BUGS $cert_option -quiet -ign_eof -connect $NODEIP:$PORT $PROXY $SNI") 1>$TMPFILE 2>$ERRFILE & wait_kill $! $HEADER_MAXSLEEP was_killed=$? # !=0 when it was killed detected_compression=$(grep -ia ^Content-Encoding: $TMPFILE) @@ -17313,9 +17342,9 @@ run_breach() { [[ $VULN_COUNT -le $VULN_THRESHLD ]] && outln && pr_headlineln " Testing for BREACH (HTTP compression) vulnerability " && outln pr_bold " BREACH"; out " ($cve) " - if [[ "$CLIENT_AUTH" == required ]]; then - prln_warning "client x509-based authentication prevents this from being tested" - fileout "$jsonID" "WARN" "client x509-based authentication prevents this from being tested" "$cve" "$cwe" + if [[ "$CLIENT_AUTH" == required ]] && [[ -z "$MTLS" ]]; then + prln_warning "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested" + fileout "$jsonID" "WARN" "not having provided client certificate and private key file, the client x509-based authentication prevents this from being tested" "$cve" "$cwe" return 7 fi @@ -20500,6 +20529,7 @@ tuning / connect options (most also can be preset via environment variables): --ids-friendly skips a few vulnerability checks which may cause IDSs to block the scanning IP --phone-out allow to contact external servers for CRL download and querying OCSP responder --add-ca path to with *.pem or a comma separated list of CA files to include in trust check + --mtls path to file, it must be in PEM format and contain client certificate with certificate key (not encrypted) --basicauth provide HTTP basic auth information. --reqheader
add custom http request headers @@ -23807,6 +23837,10 @@ parse_cmd_line() { OPENSSL_TIMEOUT="$(parse_opt_equal_sign "$1" "$2")" [[ $? -eq 0 ]] && shift ;; + --mtls|--mtls=*) + MTLS="$(parse_opt_equal_sign "$1" "$2")" + [[ $? -eq 0 ]] && shift + ;; --connect-timeout|--connect-timeout=*) CONNECT_TIMEOUT="$(parse_opt_equal_sign "$1" "$2")" [[ $? -eq 0 ]] && shift @@ -23885,6 +23919,17 @@ parse_cmd_line() { grep -q 'BEGIN CERTIFICATE' "$fname" || fatal_cmd_line "\"$fname\" is not CA file in PEM format" $ERR_RESOURCE done + # Check if mTLS has been selected, and if the correct client auth PEM file has been provided by user + if [[ ! -z "$MTLS" ]]; then + if [[ -f $MTLS ]]; then + grep -q 'BEGIN CERTIFICATE' "$MTLS" || fatal_cmd_line "\"$MTLS\" is not a client certificate file in PEM format" $ERR_RESOURCE + grep -q 'BEGIN PRIVATE KEY\|BEGIN RSA PRIVATE KEY' "$MTLS" || fatal_cmd_line "\"$MTLS\" the not encrypted private key is missing in the specified PEM file" $ERR_RESOURCE + MTLS=$MTLS + else + [[ -s "$MTLS" ]] || fatal_cmd_line "the specified client certificate file \"$MTLS\" does not exist" $ERR_RESOURCE + fi + fi + "$FAST" && pr_warning "\n'--fast' can have some undesired side effects thus it is not recommended to use anymore\n" "$SSL_NATIVE" && pr_warning "\nusage of '--ssl-native' is not recommended as it will return incomplete and may even return incorrect results\n" From bdab5f665c90a3696827eda0efb3e653c082d16c Mon Sep 17 00:00:00 2001 From: Maurizio S <46047144+akabe1@users.noreply.github.com> Date: Sat, 20 Jan 2024 11:49:32 +0100 Subject: [PATCH 2/5] Update CREDITS.md --- CREDITS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index 9522a76..1290d9d 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -164,6 +164,9 @@ Full contribution, see git log. * Jonas Schäfer - XMPP server patch +* Maurizio Siddu + - added --mTLS feature + * Marcin Szychowski - Quick'n'dirty client certificate support From 83fb9b5b3ab7936a6419d772bab77301fec70729 Mon Sep 17 00:00:00 2001 From: Maurizio S <46047144+akabe1@users.noreply.github.com> Date: Sat, 20 Jan 2024 11:49:41 +0100 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c20aa5c..3aed6cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * Compatible to GNU grep 3.8 * Don't use external pwd command anymore * Doesn't hang anymore when there's no local resolver +* Added --mtls feature to support client authentication ### Features implemented / improvements in 3.0 From 55ef4c09fea1bd8f33f3fd79d70c6482cc9957a7 Mon Sep 17 00:00:00 2001 From: Maurizio S <46047144+akabe1@users.noreply.github.com> Date: Sat, 20 Jan 2024 11:49:50 +0100 Subject: [PATCH 4/5] Update testssl.1.md --- doc/testssl.1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/testssl.1.md b/doc/testssl.1.md index 1583536..2459d48 100644 --- a/doc/testssl.1.md +++ b/doc/testssl.1.md @@ -113,6 +113,8 @@ The same can be achieved by setting the environment variable `WARNINGS`. `--reqheader
` This can be used to add additional HTTP request headers in the correct format `Headername: headercontent`. This parameter can be called multiple times if required. For example: `--reqheader 'Proxy-Authorization: Basic dGVzdHNzbDpydWxlcw==' --reqheader 'ClientID: 0xDEADBEAF'`. REQHEADER is the corresponding environment variable. +`--mtls ` This can be set to provide a file containing a client certificatete and a private key (not encrypted) in PEM format, which is used when a mutual TLS authentication is required by the remote server. MTLS is the equivalent environment variable. + ### SPECIAL INVOCATIONS From 51ab05e651114a30149b42744706d05a2ac5ebe3 Mon Sep 17 00:00:00 2001 From: Maurizio S <46047144+akabe1@users.noreply.github.com> Date: Sat, 20 Jan 2024 11:49:56 +0100 Subject: [PATCH 5/5] Update testssl.1.html --- doc/testssl.1.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/testssl.1.html b/doc/testssl.1.html index 1584bf0..05dc144 100644 --- a/doc/testssl.1.html +++ b/doc/testssl.1.html @@ -192,6 +192,8 @@ The same can be achieved by setting the environment variable WARNINGS--reqheader <header> This can be used to add additional HTTP request headers in the correct format Headername: headercontent. This parameter can be called multiple times if required. For example: --reqheader 'Proxy-Authorization: Basic dGVzdHNzbDpydWxlcw==' --reqheader 'ClientID: 0xDEADBEAF'. REQHEADER is the corresponding environment variable.

+

--mtls <path_to_client_cert> This can be set to provide a file containing a client certificatete and a private key (not encrypted) in PEM format, which is used when a mutual TLS authentication is required by the remote server. MTLS is the is the equivalent environment variable.

+

SPECIAL INVOCATIONS

-t <protocol>, --starttls <protocol> does a default run against a STARTTLS enabled protocol. protocol must be one of ftp, smtp, pop3, imap, xmpp, sieve, xmpp-server, telnet, ldap, irc, lmtp, nntp, postgres, mysql. For the latter four you need e.g. the supplied OpenSSL or OpenSSL version 1.1.1. Please note: MongoDB doesn't offer a STARTTLS connection, IRC currently only works with --ssl-native. irc is WIP.