diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f25d1e3..a321318 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,7 +7,7 @@ assignees: '' --- -_Feel free to remove this line but please stick to the template. We would like to reproduce the bug and therefore need concise information. _ +_Feel free to remove this line but please stick to the template. We would like to reproduce the bug and therefore need concise information. Depending on the completeness of your information provided we might close your issue otherwise. _ **Please check this repo whether this is a known issue** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 540ad04..e3963ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ ### Contributions / participation -is always welcome! +is always welcome, here @ gihub or via e-mail. -Note please the following: +Note please the following * Please read at least the [coding convention](https://github.com/drwetter/testssl.sh/Coding_Convention.md). * One PR per feature or bug fix or improvement. Please do not mix issues. @@ -13,5 +13,9 @@ Note please the following: * If it's a new feature please consider writing a unit test for it. You can use e.g. `t/20_baseline_ipv4_http.t` as a template. The general documentation for [Test::More](https://perldoc.perl.org/Test/More.html) is a good start. * If it's a new feature it would need to be documented in the appropriate section in `help()` and in `~/doc/testssl.1.md` -For questions just open an issue. +For questions just open an issue or feel free to send me an e-mail. +#### Patches via e-mail + +Of course it is fine when you want to send in patches to use e-mail. For the address please grep for SWCONTACT in testssl.sh . +Let me know how you like them to be attributed. diff --git a/Dockerfile b/Dockerfile index 624a5f5..08514dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 +FROM alpine:3.13 RUN apk update && \ apk upgrade && \ diff --git a/testssl.sh b/testssl.sh index 7ec39ea..36fb9c4 100755 --- a/testssl.sh +++ b/testssl.sh @@ -351,6 +351,8 @@ HAS_ZLIB=false HAS_UDS=false HAS_UDS2=false HAS_DIG=false +HAS_DIG_R=true +DIG_R="-r" HAS_HOST=false HAS_DRILL=false HAS_NSLOOKUP=false @@ -634,7 +636,9 @@ pr_bold() { tm_bold "$1"; [[ "$COLOR" -ne 0 ]] && html_out "/dev/null && HAS_IDN=true type -p idn2 &>/dev/null && HAS_IDN2=true + # Old dig versions don't have an option to ignore $HOME/.digrc + if dig -r 2>&1 | grep -qiE 'invalid|usage'; then + HAS_DIG_R=false + DIG_R="" + fi + OPENSSL_CONF="" # see https://github.com/drwetter/testssl.sh/issues/134 if ! "$HAS_DIG" && ! "$HAS_HOST" && ! "$HAS_DRILL" && ! "$HAS_NSLOOKUP"; then fatal "Neither \"dig\", \"host\", \"drill\" or \"nslookup\" is present" $ERR_DNSBIN fi if "$HAS_DIG"; then - if dig +noidnout -t a 2>&1 | grep -Eq 'Invalid option: \+noidnout|IDN support not enabled'; then + if dig $DIG_R +noidnout -t a 2>&1 | grep -Eq 'Invalid option: \+noidnout|IDN support not enabled'; then : else HAS_DIG_NOIDNOUT=true @@ -20063,13 +20074,13 @@ get_a_record() { if "$HAS_AVAHIRESOLVE"; then ip4=$(filter_ip4_address $(avahi-resolve -4 -n "$1" 2>/dev/null | awk '{ print $2 }')) elif "$HAS_DIG"; then - ip4=$(filter_ip4_address $(dig @224.0.0.251 -p 5353 +short -t a +notcp "$1" 2>/dev/null | sed '/^;;/d')) + ip4=$(filter_ip4_address $(dig $DIG_R @224.0.0.251 -p 5353 +short -t a +notcp "$1" 2>/dev/null | sed '/^;;/d')) else fatal "Local hostname given but no 'avahi-resolve' or 'dig' available." $ERR_DNSBIN fi fi if [[ -z "$ip4" ]] && "$HAS_DIG"; then - ip4=$(filter_ip4_address $(dig +short +timeout=2 +tries=2 $noidnout -t a "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }')) + ip4=$(filter_ip4_address $(dig $DIG_R +short +timeout=2 +tries=2 $noidnout -t a "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }')) fi if [[ -z "$ip4" ]] && "$HAS_HOST"; then ip4=$(filter_ip4_address $(host -t a "$1" 2>/dev/null | awk '/address/ { print $NF }')) @@ -20107,12 +20118,12 @@ get_aaaa_record() { if "$HAS_AVAHIRESOLVE"; then ip6=$(filter_ip6_address $(avahi-resolve -6 -n "$1" 2>/dev/null | awk '{ print $2 }')) elif "$HAS_DIG"; then - ip6=$(filter_ip6_address $(dig @ff02::fb -p 5353 -t aaaa +short +notcp "$NODE")) + ip6=$(filter_ip6_address $(dig $DIG_R @ff02::fb -p 5353 -t aaaa +short +notcp "$NODE")) else fatal "Local hostname given but no 'avahi-resolve' or 'dig' available." $ERR_DNSBIN fi elif "$HAS_DIG"; then - ip6=$(filter_ip6_address $(dig +short +timeout=2 +tries=2 $noidnout -t aaaa "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }')) + ip6=$(filter_ip6_address $(dig $DIG_R +short +timeout=2 +tries=2 $noidnout -t aaaa "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }')) elif "$HAS_HOST"; then ip6=$(filter_ip6_address $(host -t aaaa "$1" | awk '/address/ { print $NF }')) elif "$HAS_DRILL"; then @@ -20148,7 +20159,7 @@ get_caa_rr_record() { # caa_property then has key/value pairs, see https://tools.ietf.org/html/rfc6844#section-3 OPENSSL_CONF="" if "$HAS_DIG"; then - raw_caa="$(dig +short +timeout=3 +tries=3 $noidnout type257 "$1" 2>/dev/null | awk '{ print $1" "$2" "$3 }')" + raw_caa="$(dig $DIG_R +short +timeout=3 +tries=3 $noidnout type257 "$1" 2>/dev/null | awk '{ print $1" "$2" "$3 }')" # empty if no CAA record elif "$HAS_DRILL"; then raw_caa="$(drill $1 type257 | awk '/'"^${1}"'.*CAA/ { print $5,$6,$7 }')" @@ -20219,7 +20230,7 @@ get_mx_record() { if "$HAS_HOST"; then mx="$(host -t MX "$1" 2>/dev/null | awk '/is handled by/ { print $(NF-1), $NF }')" elif "$HAS_DIG"; then - mx="$(dig +short $noidnout -t MX "$1" 2>/dev/null | awk '/^[0-9]/ { print $1" "$2 }')" + mx="$(dig $DIG_R +short $noidnout -t MX "$1" 2>/dev/null | awk '/^[0-9]/ { print $1" "$2 }')" elif "$HAS_DRILL"; then mx="$(drill mx $1 | awk '/IN[ \t]MX[ \t]+/ { print $(NF-1), $NF }')" elif "$HAS_NSLOOKUP"; then @@ -20246,7 +20257,7 @@ get_txt_record() { if "$HAS_HOST"; then record="$(host -t TXT "$1" 2>/dev/null | awk -F\" '/descriptive text/ { print $(NF-1) }')" elif "$HAS_DIG"; then - record="$(dig +short $noidnout -t TXT "$1" 2>/dev/null)" + record="$(dig $DIG_R +short $noidnout -t TXT "$1" 2>/dev/null)" elif "$HAS_DRILL"; then record="$(drill txt $1 | awk -F\" '/^[a-z0-9].*TXT/ { print $(NF-1) }')" elif "$HAS_NSLOOKUP"; then @@ -20346,11 +20357,11 @@ determine_rdns() { if "$HAS_AVAHIRESOLVE"; then rDNS=$(avahi-resolve -a $nodeip 2>/dev/null | awk '{ print $2 }') elif "$HAS_DIG"; then - rDNS=$(dig -x $nodeip @224.0.0.251 -p 5353 +notcp +noall +answer +short | awk '{ print $1 }') + rDNS=$(dig $DIG_R -x $nodeip @224.0.0.251 -p 5353 +notcp +noall +answer +short | awk '{ print $1 }') fi elif "$HAS_DIG"; then # 1+2 should suffice. It's a compromise for if e.g. network is down but we have a docker/localhost server - rDNS=$(dig -x $nodeip +timeout=1 +tries=2 +noall +answer +short | awk '{ print $1 }') # +short returns also CNAME, e.g. openssl.org + rDNS=$(dig $DIG_R -x $nodeip +timeout=1 +tries=2 +noall +answer +short | awk '{ print $1 }') # +short returns also CNAME, e.g. openssl.org elif "$HAS_HOST"; then rDNS=$(host -t PTR $nodeip 2>/dev/null | awk '/pointer/ { print $NF }') elif "$HAS_DRILL"; then @@ -21353,7 +21364,7 @@ nmap_to_plain_file() { local target_fname="" local oneline="" local ip hostdontcare round_brackets ports_specs starttls - local tmp port host_spec protocol dontcare dontcare1 + local tmp port host_spec protocol ssl_hint dontcare dontcare1 #FIXME: IPv6 is missing here # Ok, since we are here we are sure to have an nmap file. To avoid questions we make sure it's the right format too @@ -21368,17 +21379,12 @@ nmap_to_plain_file() { else fatal "Nmap file $FNAME is not in grep(p)able format (-oG filename.g(n)map)" $ERR_FNAMEPARSE fi - # strip extension and create output file *.txt in same folder + # create ${FNAME%.*}.txt in $TEMPDIR target_fname="${FNAME%.*}.txt" - > "${target_fname}" - if [[ $? -ne 0 ]]; then - # try to just create ${FNAME%.*}.txt in the same dir as the gnmap file failed. - # backup is using one in $TEMPDIR - target_fname="${target_fname##*\/}" # strip path (Unix) - target_fname="${target_fname##*\\}" # strip path (Dos) - target_fname="$TEMPDIR/$target_fname" - > "${target_fname}" || fatal "Cannot create \"${target_fname}\"" $ERR_FCREATE - fi + target_fname="${target_fname##*\/}" # strip path (Unix) + target_fname="${target_fname##*\\}" # strip path (Dos) + target_fname="$TEMPDIR/$target_fname" + > "${target_fname}" || fatal "Cannot create \"${target_fname}\"" $ERR_FCREATE # Line x: "Host: AAA.BBB.CCC.DDD () Status: Up" # Line x+1: "Host: AAA.BBB.CCC.DDD () Ports: 443/open/tcp//https///" @@ -21399,11 +21405,15 @@ nmap_to_plain_file() { while read -r oneline; do # 25/open/tcp//smtp///, [[ "$oneline" =~ '/open/tcp/' ]] || continue # no open tcp for this port on this IP --> move on - IFS=/ read -r port dontcare protocol dontcare1 <<< "$oneline" - starttls="$(ports2starttls $port)" - [[ $? -eq 1 ]] && continue # nmap got a port but we don't know how to speak to - [[ "$DEBUG" -ge 1 ]] && echo "${starttls}$host_spec:$port" - echo "${starttls}${host_spec}:${port}" >>"$target_fname" + IFS=/ read -r port dontcare protocol ssl_hint dontcare1 <<< "$oneline" + if [[ "$ssl_hint" =~ ^(ssl|https) ]] || [[ "$dontcare1" =~ ^(ssl|https) ]]; then + echo "${host_spec}:${port}" >>"$target_fname" + else + starttls="$(ports2starttls $port)" + [[ $? -eq 1 ]] && continue # nmap got a port but we don't know how to speak to + [[ "$DEBUG" -ge 1 ]] && echo "${starttls}$host_spec:$port" + echo "${starttls}${host_spec}:${port}" >>"$target_fname" + fi done < <(tr ',' '\n' <<< "$ports_specs") done < "$FNAME" [[ "$DEBUG" -ge 1 ]] && echo diff --git a/utils/gmap2testssl.sh b/utils/gmap2testssl.sh index e4275dc..1ea583c 100755 --- a/utils/gmap2testssl.sh +++ b/utils/gmap2testssl.sh @@ -1,43 +1,167 @@ #!/usr/bin/env bash -set -e +# Utility which converts grepable nmap outout to testssl's file input +# It is just borrowed from testssl.sh +# License see testssl.sh -# utility which converts grepable nmap outout to testssl's file input + +echo A | sed -E 's/A//' >/dev/null 2>&1 && \ +declare -r HAS_SED_E=true || \ +declare -r HAS_SED_E=false usage() { cat << EOF usage: - "$0 filename<.gmap>": looks for filename/filename.gmap and converts into basename \$(filename)-testssl.txt" - "$0 filename<.gmap>" "scan option": same as before, only adds testssl.sh scan option in front of IPs" + "$0 ": looks for (nmap gmap format) and converts into basename \$(filename)-testssl.txt" EOF exit 0 } -[ -z "$1" ] && usage -FNAME="$1" -OPT2ADD="${2:-}" +fatal () { + echo "$1" >&2 + exit $2 +} -if ! grep -q gmap <<< "$FNAME"; then - FNAME="$FNAME.gmap" -fi -[ ! -e $FNAME ] && echo "$FNAME not readable" && exit 2 +is_ipv4addr() { + local octet="(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])" + local ipv4address="$octet\\.$octet\\.$octet\\.$octet" + [[ -z "$1" ]] && return 1 -TARGET_FNAME=${FNAME%.*}-testssl.txt + # Check that $1 contains an IPv4 address and nothing else + [[ "$1" =~ $ipv4address ]] && [[ "$1" == $BASH_REMATCH ]] && \ + return 0 || \ + return 1 +} -# test whether there's more than one "open" per line -while read -r oneline; do - if [ $(echo "${oneline}" | tr ',' '\n' | grep -wc 'open') -gt 1 ]; then - # not supported currently - echo "$FNAME contains at least on one line more than 1x\"open\"" - exit 3 +filter_ip4_address() { + local a + + for a in "$@"; do + if ! is_ipv4addr "$a"; then + continue + fi + if "$HAS_SED_E"; then + sed -E 's/[^[:digit:].]//g' <<< "$a" | sed -e '/^$/d' + else + sed -r 's/[^[:digit:].]//g' <<< "$a" | sed -e '/^$/d' + fi + done +} + +# arg1: a host name. Returned will be 0-n IPv4 addresses +# watch out: $1 can also be a cname! --> all checked +get_a_record() { + local ip4="" + local noidnout="" + + ip4=$(filter_ip4_address $(dig -r +short +timeout=2 +tries=2 -t a "$1" 2>/dev/null | awk '/^[0-9]/ { print $1 }')) + if [[ -z "$ip4" ]]; then + ip4=$(filter_ip4_address $(host -t a "$1" 2>/dev/null | awk '/address/ { print $NF }')) fi -done < "$FNAME" + if [[ -z "$ip4" ]]; then + ip4=$(filter_ip4_address $(drill a "$1" | awk '/ANSWER SECTION/,/AUTHORITY SECTION/ { print $NF }' | awk '/^[0-9]/')) + fi + echo "$ip4" +} + +ports2starttls() { + local tcp_port=$1 + local ret=0 + + # https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers + case $tcp_port in + 21) echo "-t ftp " ;; + 23) echo "-t telnet " ;; + 119|433) echo "-t nntp " ;; # to come + 25|587) echo "-t smtp " ;; + 110) echo "-t pop3 " ;; + 143) echo "-t imap " ;; + 389) echo "-t ldap ";; + 3306) echo "-t mysql " ;; + 5222) echo "-t xmpp " ;; # domain of jabber server maybe needed + 5432) echo "-t postgres " ;; + 563) ;; # NNTPS + 636) ;; # LDAP + 1443|8443|443|981) ;; # HTTPS + 465) ;; # HTTPS | SMTP + 631) ;; # CUPS + 853) ;; # DNS over TLS + 995|993) ;; # POP3|IMAP + 3389) ;; # RDP + *) ret=1 ;; # we don't know this ports so we rather do not scan it + esac + return $ret +} + +nmap_to_plain_file () { + + local fname="$1" + local target_fname="" + local oneline="" + local ip hostdontcare round_brackets ports_specs starttls + local tmp port host_spec protocol ssl_hint dontcare dontcare1 + + # Ok, since we are here we are sure to have an nmap file. To avoid questions we make sure it's the right format too + if [[ "$(head -1 "$fname")" =~ ( -oG )(.*) ]] || [[ "$(head -1 "$fname")" =~ ( -oA )(.*) ]] ; then + # yes, greppable + if [[ $(grep -c Status "$fname") -ge 1 ]]; then + [[ $(grep -c '\/open\/' "$fname") -eq 0 ]] && \ + fatal "Nmap file $fname should contain at least one open port" 250 + else + fatal "strange, nmap grepable misses \"Status\"" 251 + fi + else + fatal "Nmap file $fname is not in grep(p)able format (-oG filename.g(n)map)" 250 + fi + target_fname="${fname%.*}"-testssl.txt + [[ -e $target_fname ]] && fatal "$target_fname already exists" 3 + > "${target_fname}" || fatal "Cannot create \"${target_fname}\"" 252 + + # Line x: "Host: AAA.BBB.CCC.DDD () Status: Up" + # Line x+1: "Host: AAA.BBB.CCC.DDD () Ports: 443/open/tcp//https///" + # (or): Host: AAA.BBB.CCC.DDD () Ports: 22/open/tcp//ssh///, 25/open/tcp//smtp///, 443/open/tcp//ssl|http// + while read -r hostdontcare ip round_brackets tmp ports_specs; do + [[ "$ports_specs" =~ "Status: " ]] && continue # we don't need this + [[ "$ports_specs" =~ '/open/tcp/' ]] || continue # no open tcp at all for this IP --> move + host_spec="$ip" + fqdn="${round_brackets/\(/}" + fqdn="${fqdn/\)/}" + if [[ -n "$fqdn" ]]; then + tmp="$(get_a_record "$fqdn")" + if [[ "$tmp" == "$ip" ]]; then + host_spec="$fqdn" + fi + fi + while read -r oneline; do + # 25/open/tcp//smtp///, + [[ "$oneline" =~ '/open/tcp/' ]] || continue # no open tcp for this port on this IP --> move on + IFS=/ read -r port dontcare protocol ssl_hint dontcare1 <<< "$oneline" + if [[ "$ssl_hint" =~ ^(ssl|https) ]] || [[ "$dontcare1" =~ ^(ssl|https) ]]; then + echo "${host_spec}:${port}" >>"$target_fname" + else + starttls="$(ports2starttls $port)" + [[ $? -eq 1 ]] && continue # nmap got a port but we don't know how to speak to + echo "${starttls}${host_spec}:${port}" >>"$target_fname" + fi + done < <(tr ',' '\n' <<< "$ports_specs") + done < "$fname" + + [[ -s "$target_fname" ]] || fatal "Couldn't find any open port in $fname" 253 + echo "$target_fname written successfully" + return 0 +} + + +[[ -z "$1" ]] && usage +FNAME="$1" +[[ ! -e $FNAME ]] && echo "$FNAME not readable" && exit 2 + +nmap_to_plain_file "$FNAME" -awk '/\/ { print "'"${OPT2ADD}"' " $2":"$5 }' "$FNAME" | sed 's/\/open.*$//g' >"$TARGET_FNAME" exit $? # vim:ts=5:sw=5:expandtab