Lunchtime CTF at Deja

This CTF and its writeup was done by James Premo, one of Deja vu Security’s highly talented Senior Security Consultants.


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:

  1. Introduce the basic concept of port knocking

  2. Introduce the basics of network testing through the Scapy tool chain

  3. Force the creation of a script to automate testing

  4. 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:

Hi all,

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!

Rules:

- 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

Hints:
- 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

Good luck!

James


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:

# scapy
>>> packet = IP(dst=’<IP>’,src=’192.168.1.1’)
>>> sendp(packet)

However, the “port knocking” is over TCP so the sender also needs to send a TCP packet as well:

# scapy
>>> packet = IP(dst=’<IP>’,src=’192.168.1.1’)/TCP(dport=1234)
>>> sendp(packet)

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 = []
Q.extend(q1)
Q.extend(q2)
Q.extend(q3)
Q.extend(q4)
Q.extend(q5)
 
len(Q)
 
for i in Q:
    for j in list(i):
        send(j)

The winner ran the above script and added a check for the flag on port 9999. They were able to successfully retrieve the flag!

The winner of the challenge was none other than our architect-turned-security-consultant Ryan Harasimowicz. We had a delicious ramen lunch at a local favorite, Ramen Danbo.


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 * 
import socket
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