Filter Out Poisoned Data Sources#

Terminal Hint: Wunorse Openslae - Zeek JSON Analysis


Use the data supplied in the Zeek JSON logs to identify the IP addresses of attackers poisoning Santa's flight mapping software. Block the 100 offending sources of information to guide Santa's sleigh through the attack.
Submit the Route ID ("RID") success value that you're given.
For hints on achieving this objective, please visit the Sleigh Shop and talk with Wunorse Openslae.




Before diving into the Zeek JSON logs and finding all the bad activity we first need log in to the Sleigh Route Finder API website. The Super Sled-O-Matic quick-start guide provides information on where we can find the login credentials, stating "The default login credentials should be changed on startup and can be found in the readme in the ElfU Research Labs git repository."

Git readme files are typically named so search the logs for that particular string.

cat http.log | jq '.[] | select(.uri|test("README")) | select(.status_code==200)' Location

Download The default admin credentials are admin 924158F9522B3744F5FCD4D10FAC4356. Location

Now that we have access to the Sleigh Route Finder API we can start going through the logs to find all the IP addresses responsible for the bad activity. Wunorse tells us we might want to look for LFI, XSS, SQLi, and Shellshock. Remember to search more than just the uri field for these exploits. host, user_agent, and username can also contain interesting artifacts. Let's kick things off with Local File Inclusion (LFI). To keep things clean the output is limited to just the source IP and the field being searched.

jq -j '.[] | select(.uri|contains("../")) | ."id.orig_h", "\t", .uri, "\n"' http.log
jq -j '.[] | select(.uri|test(".*=.*passwd.*")) | ."id.orig_h", "\t", .uri, "\n"' http.log


We have our first set of 11 bad IPs. Moving on to SQL Injection (SQLi).

jq -j '.[] | select(.uri|contains("UNION")) | ."id.orig_h", "\t", .uri, "\n"' http.log
jq -j '.[] | select(.user_agent|contains("UNION")) | ."id.orig_h", "\t", .user_agent, "\n"' http.log
jq -j '.[] | select(.username|contains("1=1")) | ."id.orig_h", "\t", .username, "\n"' http.log


29 results. Quite the haul! Next up, Shellshock.

jq -j '.[] | select(.user_agent|contains(":;")) | ."id.orig_h", "\t", .user_agent, "\n"' http.log


6 additional IPs. Last but not least, Cross-Site Scripting (XSS).

jq -j '.[] | select(.host|contains("<")) | ."id.orig_h", "\t", .host, "\n"' http.log
jq -j '.[] | select(.uri|contains("<")) | ."id.orig_h", "\t", .uri, "\n"' http.log

Cross-Site Scripting

With the XSS results we now have a total of 62 IP addresses. Not enough to cover all of the malicious traffic unfortunately. To find more IPs we need to use information from the current set and match those with other, less obvious log entries. Let's take a look at the User-Agent strings for the IP addresses we found so far.

cat IPs_bad.csv | cut -d$'\t' -f 1,5 | grep -vEi '.*UNION|:;.*'


While not all of these jump out as suspicious, a few definitely do. The below Python script takes 2 CSV files with id.orig_h, host, username, uri, and user_agent fields from the Zeek logs as input. One log containing only the clearly bad IPs and the other containing all of them. If we find more than 3 matches for a specific IP address (i.e. more than 2 additional values) then chances are we're including legitimate traffic so we ignore those just to be on the safe side.

#!/usr/bin/env python3
"""2019 SANS Holiday Hack Challenge - Filter Out Poisoned Data Sources."""

def main():
    file_bad = 'IPs_bad.csv'
    file_all = 'IPs_all.csv'
    list_bad = []
    list_all = []

    # Read the full data log
    with open(file_all) as fp:
        line = fp.readline()

        while line:
            line = fp.readline()

    # Read the bad IP data and match on user_agent but only
    # keep the results if less than 4 matches are found.
    with open(file_bad) as fp:
        line = fp.readline()

        while line:
            tmp = []
            line_bad = line.split('\t')

            for line_all in list_all:
                if line_all[4] == line_bad[4]:

            # Only add if less than 4 matches
            if len(tmp) < 4:

            # Add the original IP as well
            line = fp.readline()

    # Remove duplicates
    list_bad = list(dict.fromkeys(list_bad))

    # Tadaaaaa!
    print(f'Found {len(list_bad)} IPs: {",".join(list_bad)}')

if __name__ == "__main__":

Bad IPs

We only have 97 IPs, but let's give it a shot anyway. Take the CSV data and deny it on the SRF firewall.

IPs Blocked

The RID seems to be made up of 2 dates, 08-07-1985 and 08-26-1964, but I'm not sure what they signify. Enter the Bell Tower and meet up with Santa, Krampus, and the Tooth Fairy. In the top left corner you'll also find a final note. Roll credits!

Bell Tower


RID: 0807198508261964


Congratulations on a job well done!
Oh, by the way, I won the Frido Sleigh contest.
I got 31.8% of the prizes, though I'll have to figure that out.

You did it! Thank you! You uncovered the sinister plot to destroy the holiday season!
Through your diligent efforts, we’ve brought the Tooth Fairy to justice and saved the holidays!
Ho Ho Ho!
The more I laugh, the more I fill with glee.
And the more the glee,
The more I'm a merrier me!
Merry Christmas and Happy Holidays.

The Tooth Fairy:
You foiled my dastardly plan! I’m ruined!
And I would have gotten away with it too, if it weren't for you meddling kids!