diff --git a/cipherscan b/cipherscan index d995355..8b539ae 100755 --- a/cipherscan +++ b/cipherscan @@ -63,6 +63,32 @@ fi # RSA ciphers are put at the end to force Google servers to accept ECDSA ciphers # (probably a result of a workaround for the bug in Apple implementation of ECDSA) CIPHERSUITE="ALL:COMPLEMENTOFALL:+aRSA" +# some servers are intolerant to large client hello, try a shorter list of +# ciphers with them +SHORTCIPHERSUITE=('ECDHE-ECDSA-AES128-GCM-SHA256' + 'ECDHE-RSA-AES128-GCM-SHA256' + 'ECDHE-RSA-AES256-GCM-SHA384' + 'ECDHE-ECDSA-AES256-SHA' + 'ECDHE-ECDSA-AES128-SHA' + 'ECDHE-RSA-AES128-SHA' + 'ECDHE-RSA-AES256-SHA' + 'ECDHE-RSA-DES-CBC3-SHA' + 'ECDHE-ECDSA-RC4-SHA' + 'ECDHE-RSA-RC4-SHA' + 'DHE-RSA-AES128-SHA' + 'DHE-DSS-AES128-SHA' + 'DHE-RSA-CAMELLIA128-SHA' + 'DHE-RSA-AES256-SHA' + 'DHE-DSS-AES256-SHA' + 'DHE-RSA-CAMELLIA256-SHA' + 'EDH-RSA-DES-CBC3-SHA' + 'AES128-SHA' + 'CAMELLIA128-SHA' + 'AES256-SHA' + 'CAMELLIA256-SHA' + 'DES-CBC3-SHA' + 'RC4-SHA' + 'RC4-MD5') # as some servers are intolerant to large client hello's (or ones that have # RC4 ciphers below position 64), use the following for cipher testing in case # of problems @@ -119,6 +145,8 @@ unset known_certs declare -A known_certs unset cert_checksums declare -A cert_checksums +# array with results of tolerance scans (TLS version, extensions, etc.) +declare -A tls_tolerance # because running external commands like sleep incurs a fork penalty, we # first check if it is necessary @@ -255,6 +283,20 @@ c_hash() { done } +crude_grep() { + while read line; do + if [[ $line =~ $1 ]]; then + return 0 + fi + done + return 1 +} + +check_option_support() { + $OPENSSLBIN s_client -help 2>&1 | crude_grep "$1" + return $? +} + parse_openssl_output() { # clear variables in case matching doesn't hit them current_ocspstaple="False" @@ -685,6 +727,17 @@ display_results_in_terminal() { echo "Curves ordering: $curvesordering" echo "Curves fallback: $fallback_supported" fi + + echo + echo "Fallbacks required:" + for test_name in "${!tls_tolerance[@]}"; do + if [[ ${tls_tolerance[$test_name]} == "False" ]]; then + echo "$test_name config not supported, connection failed" + else + local res=(${tls_tolerance[$test_name]}) + echo "$test_name no fallback req, connected: ${res[1]} ${res[2]}" + fi + done | sort } display_results_in_json() { @@ -722,7 +775,22 @@ display_results_in_json() { if [ $TEST_CURVES == "True" ]; then echo -n ",\"curves_fallback\":\"$fallback_supported\"" fi - echo '}' + echo -n ',"configs":{' + ctr=0 + for test_name in "${!tls_tolerance[@]}"; do + local result=(${tls_tolerance[$test_name]}) + [ $ctr -gt 0 ] && echo -n "," + echo -n "\"$test_name\":{" + if [[ ${result[0]} == "False" ]]; then + echo -n "\"tolerant\":\"False\"" + else + echo -n "\"tolerant\":\"True\",\"proto\":\"${result[1]}\"," + echo -n "\"cipher\":\"${result[2]}\",\"trusted\":\"${result[3]}\"" + fi + echo -n "}" + ctr=$((ctr+1)) + done + echo '}}' } test_serverside_ordering() { @@ -977,6 +1045,229 @@ test_curves_fallback() { done } +test_tls_tolerance() { + + # + # first test general version tolerance with all we've got (full list of + # curves, full list of ciphers, NPN, ALPN + # + declare -A tls_vers_tests + tls_vers_tests['big-TLSv1.2']="" + tls_vers_tests['big-TLSv1.1']="-no_tls1_2" + tls_vers_tests['big-TLSv1.0']="-no_tls1_2 -no_tls1_1" + tls_vers_tests['big-SSLv3']="-no_tls1_2 -no_tls1_1 -no_tls1" + + local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client" + sslcommand+=" -status -nextprotoneg 'http/1.1'" + sslcommand+=" $SCLIENTARGS -connect $TARGET -cipher $CIPHERSUITE" + + for version in "${!tls_vers_tests[@]}"; do + ratelimit + verbose "Testing fallback with $sslcommand ${tls_vers_tests[$version]}" + local tmp=$(echo Q | $sslcommand ${tls_vers_tests[$version]} 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance[$version]="False" + else + tls_tolerance[$version]="True $current_protocol $current_cipher $current_trusted" + fi + done + + # if TLS1.2 didn't succeeded, try different fallbacks + if [[ ${tls_tolerance['big-TLSv1.2']} == "False" ]]; then + # + # Try big client hello, but with a version 2 compatible format + # (openssl automatically does that when there are SSLv2 ciphers in + # cipher string and no options are specified) + # + local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client" + if [ -n "$CAPATH" ]; then + sslcommand+=" -CApath $CAPATH -showcerts" + elif [ -e "$CACERTS" ]; then + sslcommand+=" -CAfile $CACERTS" + fi + sslcommand+=" -connect $TARGET -cipher $CIPHERSUITE" + + ratelimit + verbose "Testing fallback with $sslcommand" + local tmp=$(echo Q | $sslcommand 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['v2-big-TLSv1.2']="False" + else + tls_tolerance['v2-big-TLSv1.2']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # try a smaller, but still v2 compatible Client Hello + # + OLDIFS="$IFS" + IFS=":" + local ciphers="${SHORTCIPHERSUITE[*]}" + IFS="$OLDIFS" + + local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client" + if [ -n "$CAPATH" ]; then + sslcommand+=" -CApath $CAPATH -showcerts" + elif [ -e "$CACERTS" ]; then + sslcommand+=" -CAfile $CACERTS" + fi + sslcommand+=" -connect $TARGET -cipher $ciphers" + + ratelimit + verbose "Testing fallback with $sslcommand" + local tmp=$(echo Q | $sslcommand 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['v2-small-TLSv1.2']="False" + else + tls_tolerance['v2-small-TLSv1.2']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # v2, small but with TLS1.1 as max version + # + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2" + local tmp=$(echo Q | $sslcommand -no_tls1_2 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['v2-small-TLSv1.1']="False" + else + tls_tolerance['v2-small-TLSv1.1']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # v2, small but with TLS1.0 as max version + # + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2 -no_tls1_1" + local tmp=$(echo Q | $sslcommand -no_tls1_2 -no_tls1_1 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['v2-small-TLSv1.0']="False" + else + tls_tolerance['v2-small-TLSv1.0']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # v2, small but with SSLv3 as max version + # + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2 -no_tls1_1 -no_tls1" + local tmp=$(echo Q | $sslcommand -no_tls1_2 -no_tls1_1 -no_tls1 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['v2-small-SSLv3']="False" + else + tls_tolerance['v2-small-SSLv3']="True $current_protocol $current_cipher $current_trusted" + fi + + + # + # use v3 format TLSv1.2 hello, small cipher list + # + OLDIFS="$IFS" + IFS=":" + local ciphers="${SHORTCIPHERSUITE[*]}" + IFS="$OLDIFS" + + local sslcommand="$TIMEOUTBIN $TIMEOUT $OPENSSLBIN s_client" + if [ -n "$CAPATH" ]; then + sslcommand+=" -CApath $CAPATH -showcerts" + elif [ -e "$CACERTS" ]; then + sslcommand+=" -CAfile $CACERTS" + fi + sslcommand+=" $SCLIENTARGS -connect $TARGET -cipher $ciphers:!SSLv2" + + ratelimit + verbose "Testing fallback with $sslcommand" + local tmp=$(echo Q | $sslcommand 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['small-TLSv1.2']="False" + else + tls_tolerance['small-TLSv1.2']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # v3 format TLSv1.1 hello, small cipher list + # + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2" + local tmp=$(echo Q | $sslcommand -no_tls1_2 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['small-TLSv1.1']="False" + else + tls_tolerance['small-TLSv1.1']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # v3 format TLSv1.0 hello, small cipher list + # + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2 -no_tls1_1" + local tmp=$(echo Q | $sslcommand -no_tls1_2 -no_tls1_1 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['small-TLSv1.0']="False" + else + tls_tolerance['small-TLSv1.0']="True $current_protocol $current_cipher $current_trusted" + fi + + # + # v3 format TLSv1.0 hello, small cipher list, no extensions + # + if check_option_support "-no_tlsext"; then + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2 -no_tls1_1 -no_tlsext" + local tmp=$(echo Q | $sslcommand -no_tls1_2 -no_tls1_1 -no_tlsext 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['small-TLSv1.0-notlsext']="False" + else + tls_tolerance['small-TLSv1.0-notlsext']="True $current_protocol $current_cipher $current_trusted" + fi + fi + + # + # v3 format SSLv3 hello, small cipher list + # + ratelimit + verbose "Testing fallback with $sslcommand -no_tls1_2 -no_tls1_1 -no_tls1" + local tmp=$(echo Q | $sslcommand -no_tls1_2 -no_tls1_1 -no_tls1 2>/dev/null) + parse_openssl_output <<<"$tmp" + verbose "Negotiated proto: $current_protocol, cipher: $current_cipher" + if [[ -z $current_protocol || $current_cipher == "(NONE)" \ + || $current_cipher == '0000' ]]; then + tls_tolerance['small-SSLv3']="False" + else + tls_tolerance['small-SSLv3']="True $current_protocol $current_cipher $current_trusted" + fi + fi +} + # If no options are given, give usage information and exit (with error code) if [ $# -eq 0 ]; then usage; @@ -1120,6 +1411,8 @@ if [[ ${#cipherspref[@]} -eq 0 ]] || [[ ${pref[1]} == "SSLv2" ]]; then get_cipher_pref "$CIPHERS" fi +test_tls_tolerance + test_serverside_ordering if [[ $TEST_CURVES == "True" ]]; then diff --git a/top1m/parse_results.py b/top1m/parse_results.py index 00f7342..e4ba944 100644 --- a/top1m/parse_results.py +++ b/top1m/parse_results.py @@ -13,6 +13,7 @@ path = "./results/" import json import sys from collections import defaultdict +import operator import os import re @@ -94,6 +95,46 @@ eccfallback = defaultdict(int) eccordering = defaultdict(int) ecccurve = defaultdict(int) ocspstaple = defaultdict(int) +fallbacks = defaultdict(int) +# array with indexes of fallback names for the matrix report +fallback_ids = defaultdict(int) +i=0 +fallback_ids['big-SSLv3'] = i +i+=1 +fallback_ids['big-TLSv1.0'] = i +i+=1 +fallback_ids['big-TLSv1.1'] = i +i+=1 +fallback_ids['big-TLSv1.2'] = i +i+=1 +# padding space +fallback_ids[' '] = i +i+=1 +fallback_ids['small-SSLv3'] = i +i+=1 +fallback_ids['small-TLSv1.0-notlsext'] = i +i+=1 +fallback_ids['small-TLSv1.0'] = i +i+=1 +fallback_ids['small-TLSv1.1'] = i +i+=1 +fallback_ids['small-TLSv1.2'] = i +i+=1 +# 2nd padding space +fallback_ids[' '] = i +i+=1 +fallback_ids['v2-small-SSLv3'] = i +i+=1 +fallback_ids['v2-small-TLSv1.0'] = i +i+=1 +fallback_ids['v2-small-TLSv1.1'] = i +i+=1 +fallback_ids['v2-small-TLSv1.2'] = i +i+=1 +fallback_ids['v2-big-TLSv1.2'] = i +i+=1 +# 3rd padding space +fallback_ids[' '] = i dsarsastack = 0 total = 0 for r,d,flist in os.walk(path): @@ -111,6 +152,7 @@ for r,d,flist in os.walk(path): tempeccfallback = "unknown" tempeccordering = "unknown" tempecccurve = {} + tempfallbacks = {} """ supported ciphers by the server under scan """ tempcipherstats = {} ciphertypes = 0 @@ -165,8 +207,31 @@ for r,d,flist in os.walk(path): except ValueError: continue + """ discard files with empty results """ if len(results['ciphersuite']) < 1: + # if there are no results from regular scan but there are + # from fallback attempts that means that the scan of a host + # is inconclusive + if 'configs' in results: + tolerance = [' '] * len(fallback_ids) + for entry in results['configs']: + config = results['configs'][entry] + if config['tolerant'] == "True" and \ + config['trusted'] == "True": + + # save which protocols passed + if entry in fallback_ids: + tolerance[fallback_ids[entry]] = 'v' + else: + fallback_ids[entry] = len(fallback_ids) + tolerance.insert(fallback_ids[entry], 'v') + + # analysis of host won't be continued, so we have to add + # results to the permanent, not temporary table, but + # do that only when there actually were detected values + if "".join(tolerance).strip(): + fallbacks["".join(tolerance).rstrip()] += 1 continue """ save ECC fallback (new format) """ @@ -184,6 +249,56 @@ for r,d,flist in os.walk(path): if len(results['curve']) == 1: tempecccurve[curve + ' Only'] = 1 + if 'configs' in results: + tolerance = [' '] * len(fallback_ids) + for entry in results['configs']: + config = results['configs'][entry] + + if not entry in fallback_ids: + fallback_ids[entry] = len(fallback_ids) + tolerance.insert(fallback_ids[entry], ' ') + + if config['tolerant'] == "True": + tolerance[fallback_ids[entry]] = 'v' + else: + tolerance[fallback_ids[entry]] = 'X' + tempfallbacks["".join(tolerance).rstrip()] = 1 + configs = results['configs'] + try: + if configs['big-TLSv1.1']['tolerant'] != "True" and \ + configs['big-TLSv1.2']['tolerant'] != "True" and \ + configs['small-TLSv1.1']['tolerant'] != "True" and \ + configs['small-TLSv1.2']['tolerant'] != "True": + if configs['v2-small-TLSv1.1']['tolerant'] != "True" and \ + configs['v2-small-TLSv1.2']['tolerant'] != "True": + tempfallbacks['TLSv1.1+ strict Intolerance'] = 1 + else: + tempfallbacks['TLSv1.1+ Intolerant'] = 1 + if configs['big-TLSv1.1']['tolerant'] == "True" and \ + configs['big-TLSv1.2']['tolerant'] != "True" and \ + configs['small-TLSv1.1']['tolerant'] == "True" and \ + configs['small-TLSv1.2']['tolerant'] != "True": + if configs['v2-small-TLSv1.2']['tolerant'] != "True": + tempfallbacks['TLSv1.2 strict Intolerance'] = 1 + else: + tempfallbacks['TLSv1.2 Intolerant'] = 1 + if configs['big-TLSv1.2']['tolerant'] != "True" and \ + configs['big-TLSv1.1']['tolerant'] == "True" and \ + configs['small-TLSv1.2']['tolerant'] == "True": + tempfallbacks['TLSv1.2 big Intolerance'] = 1 + if configs['big-TLSv1.2']['tolerant'] != "True" and \ + configs['small-TLSv1.0']['tolerant'] != "True" and \ + configs['small-TLSv1.0-notlsext']['tolerant'] == "True": + tempfallbacks['TLS extension Intolerance'] = 1 + if configs['big-TLSv1.2']['tolerant'] != "True" and \ + configs['big-TLSv1.1']['tolerant'] != "True" and \ + configs['big-TLSv1.0']['tolerant'] != "True" and \ + (configs['small-TLSv1.2']['tolerant'] == "True" or + configs['v2-small-TLSv1.2']['tolerant'] == "True"): + tempfallbacks['Big handshake intolerance'] = 1 + except KeyError: + pass + """ loop over list of ciphers """ for entry in results['ciphersuite']: @@ -392,6 +507,9 @@ for r,d,flist in os.walk(path): client_RC4_Pref[client_name] = True break + for s in tempfallbacks: + fallbacks[s] += 1 + for s in tempsigstats: sigalg[s] += 1 @@ -650,3 +768,17 @@ print("-------------------------+---------+-------") for stat in sorted(protocolstats): percent = round(protocolstats[stat] / total * 100, 4) sys.stdout.write(stat.ljust(25) + " " + str(protocolstats[stat]).ljust(10) + str(percent).ljust(4) + "\n") + +print("\nRequired fallbacks Count Percent") +print("----------------------------------------+---------+-------") +print("big small v2 ") +print("----+-----+-----+------------------------+---------+-------") +for stat in sorted(fallbacks): + percent = round(fallbacks[stat] / total * 100, 4) + sys.stdout.write(stat.ljust(40) + " " + str(fallbacks[stat]).ljust(10) + str(percent).ljust(4) + "\n") + +print("\nFallback column names") +print("------------------------") +fallback_ids_sorted=sorted(fallback_ids.items(), key=operator.itemgetter(1)) +for touple in fallback_ids_sorted: + print(str(touple[1]+1).rjust(3) + " " + str(touple[0]))