As part of the most recent ZeekWeek event a capture the flag (CTF) competition was available for attendees to play. The competition included 12 challenges, of varying difficulties, which involved tasks surrounding Zeek scripting and traffic analysis. After a participant solved a challenge, they had the option to submit write-ups describing their solution to the challenge for extra points. These extra points turned out to be the deciding factor for the top scoring players. 

The top three scoring players were:

  1. nachosauce, with 3350 points and the fastest submission times
  2. phil, with 3350 points
  3. eephillip, with 3250 points

Honorable mentions go to eck for coming in fourth place with 3100 points.

Congrats to the winners! Hopefully everyone who participated learned something new and had fun. All challenge resource files can be found here.

As a participant, the whole point of a CTF is to hone your skills, earn bragging rights (along with a $100 Amazon gift card in this case), and have fun. For additional solutions and discussion of this event, join the Zeek Slack group and find the #ctf-capture-the-flag channel. Now, on to the solutions!

The Challenges and Solutions

friendly-package

Description:
A junior sensor admin was reading through the Zeek Package Source and came across this package. The packge is named “friendly package” but is everything but friendly. The junior admin loaded the package on her zeek sensor on 31 Aug 2020. At 1598890299 the zeek sensor beaconed to a malicious command and control domain with sensitive host information. The flag is the domain name which was beaconed to. 

Flag: eja.com

Writeups:
ZeekRJuicy

  1. check the scripts to see if anything is out of order (using vi/nano)
  2. realized nothing was in the scripts, it made me suspicious
  3. using cat to show the scripts and found the contents encoded
  4. copy the encoded contents into a different file and use the “base64 -d” command to decode them into ASCII
  5. the decoded code showed a function where it uses time to calculate the C&C domain name (function GLOBAL::dga())
  6. figure out the basic zeek script structure and copy and paste the code into a zeek script. this can be done without even reading the zeek doc as the milicious code itself provides enough structure to run the scripts.
    1. modify the scripts so it prints the value t, seed and controller.
  7. ensure the dev VM has the correct time based on the descriptions given in the question. set the time just a few seconds before 1598890299. set time zone to UTC.
    1. this can be better accompolish by setting the current_time() manually but not sure how to do it as a script novice.
  8. run the zeek script to ensure it hits the proper Epoch time given – 1598890299
    1. the script seems to give the same C&C domain even befor the actual Eopch time.
  9. the script shows that the controller variable equals to eja.com

eck
The malicious code is base64 encoded in the __load__.zeek file. Easy enough to run the dga() function with “seed = 1598890299 % 864000;” and printing out “controller”

eephillip

nachosauce

phil


Other Notes:
This challenge was a lot of fun to write and exemplifies a few subtle points. First, don’t trust all code from all packages – just because it executes, or compiles, doesn’t mean it’s safe to run. Second, Zeek is a script interpreter. Which means that any code it interprets is executed with the same privileges the zeek process is running as. 

timers-and-switches

Description:
This is an odd script… The flag should be *this* once it gets to length 1000. Note that changing how fast the script goes will change the order of events.


Flag: nnnnccccrrrnnnnrcncccncrcrrrnnnnrcrncccncrcrrrnnnnrcrncccncrcnrrrnnnrcrncccncrcnrrrnnnrcrncccncrcrrrnnnnrcrncccncrcnrrrnnnrcrncccncrcnrrrnnnrcrncccnrccnrrrnnnrcrncccnrccnrrrnnnrcrncccnrccnrrrnnnrcrncccnrccnrrrnnnrcrcccnnrccrrrnnnnrcrcccnnrccrrrnnnncrrcccnnrccrrrnnnnrcrcccnnrccrrrnnnncrrcccnnrccrrrnnnnrcrcccnnrcrrrcnnnnrcrcccnnrccrrrnnnnrcrcccnnrcrrrcnnnnrcrcccnnrcnrrrcnnncrcccrnnrcrrrcnnnnrcrncccnrcnrrrcnnnrcnrcccnrcnrrrcnnncrcccrnnrcnrrrcnnnrcrncccnrcrrrcnnnnrcrcccnnrcrrrcnnnnrcrcccnnrcrrrcnnnnrcrcccnnrcrrrcneeennnrcrcccnnrcnnnrrrncnnncccrcnrcccnrrrrcnnnnrrrcnnnrccccnrcccncrrrrnnnncrrrnnnrccccnrcccnrrrrcnnnncrrrnnnrccccrncccnrrrrcnnnnrrrcnnnrccccncccrncrrrrrrnnnncnnrccccnrccnrrrrcrrnnnncnnrccccnccrnrrrrcnnnnrrcnnrccccnrccncrrrrnnnncrrnnrccccnrccncrrrrnnnncrrnnrccccnrccncrrrrcnnnnrrnnrccccnrccnrrrrccrrnnnnnnrccccrnccnrrrrcnnnnrrcnnrccccnrccncrrrrrrnnnnnnrccccccnrrrrcnnnnrrnnccccrnccrrrrcnnnnrrnnrccccncccrrrrnnnnrrnnrccccncccrrrrnnnnrrnnrccccncccrrrrnnnnrrnnrccccncccrrrrnnnnrrnnrccccncccrr


Writeups: None

Other Notes:
This challenge was also fun to write. Since Zeek is an event driven system, combining different timers is a sure way to complicate execution flow. Massive thanks to Justin Azoff for noticing that this challenge is actually impossible to solve. Since some of the intervals will eventually overlap, for example one timer of 300 and three timers in a row of 100, there’s no deterministic way of knowing which event will be handled first. To fix this challenge I should have used numbers which would not overlap, i.e. prime numbers like 103. We apologize to all participants who may have sunk cycles into analyzing this challenge.

backdoored-source

Description:
Someone introduced a bug in one of the source files from release/2.0. The flag for this challenge is the path to the file that changed. For example, “testing/btest/Baseline/BiFs.count_to_addr/out”.

Flag: scripts/base/protocols/conn/main.bro

Writeups:
eck

eephillips

phil

nachosauce

hash-yourself

Description:
This Zeek script calculates a levenshtein distance between two strings but doesn’t print it 🙁 See if you can figure out what line 42 evaluates to. But be careful, you might not want to edit the script.

Flag: 23

Writeups:
eck

eephillips

phil

nachosauce


Other Notes:
This script demonstrates some odd things that can happen with module names in scripts. All of the export and module statements are meant to be a distraction. The inability to modify the original script is meant to force players to think outside of the one-script-mentality and realize that an invocation of Zeek pulls many script files into a single process. Sometimes how these scripts interact and layer on top of each other can be complicated to debug.

vector-fun

Description:
Vectors are my favorite Zeek type. Vectors of any are full of surprises. Place your guess for the flag in the your_guess variable and invoke the script to see if you guessed correctly.

Flag: ThisIsTheFlag223

Writeups:
eck

eephillips

phil

nachosauce


Other Notes:
The any type is meant to escape Zeek’s strong typing in scriptland. This challenge highlighted some of the interesting things that can be done with variables or type any.

man-or-machine

Description:
This one is simple. There’s a pcap which contains 100 SSH connections. Only 1 of the connections was human driven. The rest weren’t. All we want to know is the source port number for that 1 connection. You ONLY have 2 attempts, so don’t bruteforce guess! All the connections used the same client, server, and configurations. If everything is the same and the payload contents are encrypted, what else could you compare?


Flag: 54712

Writeups:
ZeekRJuicy
answer is the id.orig_p in uid CtCU2Eqt7k66iGcU9; when ecnrypted, i’d compare number of packets; 

  1. ran pcap through zeek to generate a different log files; 
  2. parse conn log using zeek-cut to display uid id.orig_h id.orig_p resp_pkts; 
  3. find the one with the most number of responding packets; 
  4. confirm the same uid in ssh log

eck
tcpdump -r 100-ssh.pcap  |grep -o ‘localhost\.[0-9][0-9]* >’ |sort |uniq -c |sort -rn |head -1

eephillips

phil

nachosauce

check-your-alignment 

Description: This script doesn’t do much. Maybe there’s a secret in the file itself.
Flag: HowsThisForAFlag
Writeups:

BKirk
After trying to decode all the characters I realized the # marked the start of the flag going down

eck
Not much to say here except that the 4th char in the var names spelled out the flag

eephillips

phil

nachosauce


Other Notes:

After trying a few challenges, which require a deep understanding of the scripting language or some esoteric feature, it can be difficult to take a step backwards and think abstractly. This challenge was worth a low number of points as an attempt to signal to players not to overthink the problem.

wiat-wat

Description:
?

Flag: watagreatflag983740234

Writeups:
eck
Stop the scripts from unlinking the files is creates and look at the contants of the “flag” file.

eephillips

phil

nachosauce


Other Notes:
Script files which delete themselves are nasty little things. This challenge was meant to demonstrate how Zeek can delete files on disk and how scriptland includes a pcap_packet type. Additionally, since Zeek is able to execute system commands it is possible for one zeek process to call another zeek process.

make-your-own-simpson-episode

Description:
The writers for the Simpsons have basically done every comedy sketch conceivable. To help them construct more creative episode scripts, Matt Groening wrote this Zeek script. The script randomly choses characters from the show and assigns them common sitcom roles. But, there’s a catch… D’oh! For some reason Homer’s name never gets chosen. Figure out why.

Flag: This was a manually assessed challenge. 

Writeups:
eck

Justin Azoff
The global set is initialized containing 0x00, this makes the while ( i in loop always skip index 0 as a valid item. fix is to just initialize the set to be empty

asharma
This user submitted a handful of submissions. Considering thehm all together, it seems they solved this one.

Aashish also sent me a few Slack messages explaining his solution further.

eephillips

kobajin

The character name is stored in an vector and the index is generated randomly. To prevent the same character from being selected over and over again, “llil: set[count]” is used to store the index once selected, but contains 0 as the initial value. Homer’s index is 0, so he will never be selected!

phil

nachosauce

rdp-auth

Description:
Only one of these RDP connections sucessfully authenticated to the server. The answer to this challenge is the source port the client used in the connection which successfully logged in.

Flag: 36190

Writeups:
ZeekRJuicy
answer is the id.orig_p in uid CHCAoo1XWPGxgwNqWe;  

  1. ran the pcap through zeek to generate the log files; 
  2. check conn log for rdp activities; 
  3. the one with sucessful connection has more bytes than the rest; 
  4. confirm by checking the rdp log using the same uid

eck
All the other connection were between 18-23 packets, this one had 54.  Which one of these things is not like the other.    tcpdump -r rdp-auth_challenge_rdp-bruteforce.pcap | grep -o ‘192.168.57.9.[0-9][0-9]*’ | sort | uniq -c | sort -n | tail -1

neslog
The concept is that failed attempts will all have similar data transfer sizes.  A successful login will result in larger data transfer size as the server will continue to establish the session with the client.  to analyze this I reviewed the resp_bytes for a larger treansfer. 
$ grep -v ^#  conn.log | awk -F’  ‘ ‘{print $11}’ | sort | uniq -c | sort -nr   62 1720    1 3081 
From the output you can see that 1 session had a larger resp_bytes than the other sessions.  This indicates successful login. Below is the session of the successful login. 
1583786327.880795       CMpfPb4LvHa14Kk5Qj      192.168.57.9    36190   192.168.57.8    3389    tcp     ssl     0.360920        3896    3081    RSTO    –       –       0       ShADdatFR       28      5048    26      4202- 
This can also be verified by the connection state analysis.  There is one session where the Originator aborted the session with a reset. 
$ cat conn.log | zeek-cut conn_state | sort | uniq -c | sort -nr   62 RSTR – RSTR: Responder sent a RST.    1 RSTO – RSTO: Connection established, originator aborted (sent a RST).

eephillips

phil

nachosauce

fly-off-the-handle

Description:
I ran `zeek fly.zeek` to generate all the files in tmp/. However, I then deleted the flag.zeek file. See if you can recover the flag string.


Flag: 3s..AWildFlagAppeared..3Sx[ylmnop::GLOBAL::

Writeups:
eck
I did this the hard way.  The zip files had creating timestamps of Sep 16th, so I used the fact that srand is deterministic and worked backwards from that timestamp looking a seed that would produce the output of the tmp/0 file.  Turns out that was Wed 26 Aug 2020 10:55:44 PM EDT.  Once I had the seed that was used that seed to discover “winner”.  Once I found that winner was 705 I could then just decode that file.  (In hindsight I could have just decoded all of the files and done a case insensitive grep “flag”.  My way would have worked if the string “flag” wasn’t in the flag at least.

eephillips

phil

nachosauce

sudo-su

Description:
This is another easy one. The pcap contains a single ssh session. The user authenticated with a public key. The user was then provided a pseudo-terminal on the server. The user entered the “sudo su” command. The user then typed their passowrd and successfully elevated to root. The user then pressed CTL+D twice which exited first the root and then the user’s ssh session. All we want to know is the length of the user’s password. It’s a number. YOU ONLY GET 2 ATTEMPTS. DON’T WASTE THEM.

Flag: 6

Writeups:
eephillips

phil

Thank you again to all those who participated in the ZeekWeek 2020 CTF event.

If you would like to know more about this CTF, Zeek or getting involved in the Zeek Community please check out Zeek.org.

%d