To welcome Deja staff back from the holidays I created a little challenge for everyone. The prize for completing the challenge was a free lunch!
Something had come up the first day back from the new year that reminded me of port knocking. Port knocking is a technique employed by networks–usually firewalls–to open up network ports via a series of “knocks.” A knock is a connection over the network to a predetermined closed port, and these knocks are performed in sequence to unlock a port.
I spent about thirty minutes crafting a script to simulate port knocking, and to teach folks about network testing. The goals of the challenge/CTF were to:
Introduce the basic concept of port knocking
Introduce the basics of network testing through the Scapy tool chain
Force the creation of a script to automate testing
Have the challenge be easy enough that someone can do it over lunch
After creating and launching the script, I sent out the following challenge to the team:
I have created a simple CTF based around port knocking.
If you can be the first to get the flag and send it to me, I will take you out to lunch tomorrow or any day that works for both of us (my treat!).
I will have two ports on my box (<MY IP>) open (1234 and 1235). If you connect to them via a special sequence of TCP packets, port 9999 will open. You can then connect via nc to recover the flag. The first person to get me the flag wins!
- This cannot interfere with any work you have to get done! Do it over lunch!
- No Denial of Service (DOS)
- No port-scanning my box
- I can turn it off at any time, and I’ll probably shut it down EOD.
- No attacks against any network devices. It should just be simple TCP packets
- It has to come from a trusted IP: 3232235777
- There are no more than five steps in the sequence (but there might be less ;)...) I recommend Scapy
And the challenge began. Initially, folks were connecting directly to the port; however, this proved useless as there was an IP check looking for a trusted IP of 192.168.1.1. Luckily, no one had to guess because I’d provided a hint. I didn’t want to make it too easy, though, so I decimal-encoded the IP. To get the script to accept the packet, the sender had to set the source of the packet to the trusted IP. It can be done in Scapy this way:
>>> packet = IP(dst=’<IP>’,src=’192.168.1.1’)
However, the “port knocking” is over TCP so the sender also needs to send a TCP packet as well:
>>> packet = IP(dst=’<IP>’,src=’192.168.1.1’)/TCP(dport=1234)
Now we have a potential knock, but based on the challenge, the sequence could be anywhere from one to five knocks, and it could be on either TCP port 1234 or 1235. Participants had to iterate through potential knock sequences in order to find the right one, and to see if port 9999 was open in order to capture the flag.
The CTF winner created the following script to accomplish just this task:
from itertools import *
from netcat import Netcat
A = '192.168.1.1'
B = '10.0.1.103'
prt0 = 11113
prt1 = 1234
prt2 = 1235
prt3 = 9999
payload = "knock"
sp1 = IP(src=A, dst=B) / TCP(sport=prt0, dport=prt1) / payload
sp2 = IP(src=A, dst=B) / TCP(sport=prt0, dport=prt2) / payload
b = [sp1, sp2]
p1 = product(b, repeat=1)
p2 = product(b, repeat=2)
p3 = product(b, repeat=3)
p4 = product(b, repeat=4)
p5 = product(b, repeat=5)
q1 = list(p1)
q2 = list(p2)
q3 = list(p3)
q4 = list(p4)
q5 = list(p5)
Q = 
for i in Q:
for j in list(i):
The following is the script I used to run the challenge. Please note it was done very quickly, so it’s not the most beautiful code.
from scapy.all import *
from threading import Thread
knock_state = 'knock_0'
port_1 = 1234
port_2 = 1235
def knock_1(): global port_1 port = port_1 try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('',port)) s.listen(5) while 1: conn, addr = s.accept() received = conn.recv(1024) except OSError: print("can't bind") exit(1)
def knock_2(): global port_2 port = port_2 try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('',port)) s.listen(1) while 1: conn, addr = s.accept() received = conn.recv(1024) except OSError: print("can't bind") exit(1)
def open_sesame(): port = 9999 try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('',port)) s.listen(1) while 1: conn, addr = s.accept() received = conn.recv(1024) conn.send(b'FLAG: Knock knock. Who is there? Free Lunch!') print(addr) except OSError: print("can't bind") exit(1)
def knock_sequence(state): global knock_state if state == 'knock_1': print('knock 1') knock_state = 'knock_1' elif state == 'knock_2' and knock_state == 'knock_1': print('knock 2') knock_state = 'knock_2' elif state == 'knock_3' and knock_state == 'knock_2': print('knock_3') knock_state = 'knock_0' open_sesame() else: print('invalid state') knock_state='knock_0'
def callback(packet): source_ip = '192.168.1.1' global port_1 global port_2 if packet.haslayer("TCP"): pkt = packet[TCP].payload if packet[IP].dport == port_1: print(packet[IP].src) if packet[IP].src == source_ip: if knock_state == 'knock_2': knock_sequence('knock_3') else: knock_sequence('knock_1') else: print('Not from trusted IP!') elif packet[IP].dport == port_2: print(packet[IP].src) if packet[IP].src == source_ip: knock_sequence('knock_2') else: print('Not from trusted IP!')
def main(): Thread(name='knock_1',target=knock_1).start() Thread(name='knock_2',target=knock_2).start() sniff(prn=callback) if __name__ == '__main__': main()
Potential next steps to improve this challenge include:
Don’t actually open the ports. Allow for calls to closed ports
Enable dynamic configuration of the knock sequence
Create sessions instead of having a global state
Increase the knock complexity. For example, set specific IP/TCP packet flags for a step in the knock sequence.
Thanks for playing!
-James Premo, Senior Security Consultant