Learning meets fun at the 2022 SANS Holiday Hack Challenge – strap yourself in for a crackerjack ride at the North Pole as I foil Grinchum’s foul plan and recover the five golden rings
This is my first year participating in the SANS Holiday Hack Challenge and it was a blast. Through a series of 16 challenges ranging from easy to difficult, I practiced analyzing suspicious network traffic and PowerShell logs, writing Suricata rules, breaking out of a Docker container, finding leaked keys to exploit a GitLab CI/CD pipeline and an AWS user, conducting XML External Entity attacks, and hacking a smart contract to buy a non-fungible token.
The best part of this experience was that it introduced me to new tools and technologies, thus stretching my cybersecurity knowledge that much further. Here, I share a few highlights from solving the challenges.
Orientation
Every participant receives an avatar to navigate an in-browser video game environment set at the North Pole:
During orientation, you receive a cryptocurrency wallet that the game uses to award KringleCoins for completing challenges and that you use in the last challenge to hack a smart contract. Interestingly, the game keeps track of all KringleCoin transactions in an Ethereum blockchain, meaning a complete record of your progress is stored in this blockchain too.
On to the first ring of the game.
1. Tolkien Ring
Finding the Tolkien Ring required flexing my logs analysis muscles.
Wireshark phishing
First, I used Wireshark to analyze the provided .pcap file that revealed a server at adv.epostoday[.]uk downloading the file Ref_Sept24-2020.zip to a computer:
Peeking inside the ZIP file, I found an executable called Ref_Sept24-2020.scr that triggered two detections in ESET Endpoint Security: BAT/Runner.ES and Generik.TAGTBG. This malware eventually lead to a malicious executable running in memory called config.dll and detected by ESET’s Advanced Memory Scanner as Win32/Dridex.DD.
Windows event logs
Next, I analyzed the provided .evtx file containing PowerShell logs with Event Viewer. While there are other tools to analyze PowerShell logs, if attackers know how to use living-off-the-land binaries to stay under the radar, defenders should also be well-versed in the native tools an operating system provides.
Since the logs contained 10,434 events, I grouped the events by date and then ran the Find action to look for any events containing the $ character. In PowerShell, $ is used to create and reference variables. I found an attack happening on December 24, 2022, when the attacker ran the following script:
It looks like the attacker found a secret recipe, switched out the secret ingredient of honey for fish oil, and then created a new recipe file. This triggered an event with an ID of 4104, which stands for the execution of remote PowerShell commands. So, I filtered the events by this ID, helping me to find additional malicious events more quickly.
Suricata Regatta
The last exercise for the Tolkien Ring was writing four Suricata rules to monitor network traffic for a Dridex infestation:
alert dns $HOME_NET any -> any any (msg:”Known bad DNS lookup, possible Dridex infection”; dns.query; content:”adv.epostoday.uk”; nocase; sid:1; rev:1;)
alert http 192.185.57.242 any <> any any (msg:”Investigate suspicious connections, possible Dridex infection”; sid:2; rev:1;)
alert tls any any -> any any (msg:”Investigate bad certificates, possible Dridex infection”; tls.cert_subject; content:”CN=heardbellith.Icanwepeh.nagoya”; sid:3; rev:1;)
alert http any any -> any any (msg:”Suspicious JavaScript function, possible Dridex infection”; file_data; content:”let byteCharacters = atob”; sid:4; rev:1;)
In order, these rules catch DNS lookups for adv.epostoday[.]uk, connections to the IP address 192.185.57[.]242, the use of the malicious server heardbellith.Icanwepeh[.]nagoya identified via the common name (CN) in a TLS certificate, and the use of the JavaScript atob() function to decode a binary string containing base64-encoded data on the client.
Completing these three challenges earned me the Tolkien Ring:
On to the second ring.
2. Elfen Ring
The most prominent challenges for the Elfen Ring were Prison Escape and Jolly CI/CD.
Prison Escape
Prison Escape was a stern reminder that granting root privileges to a user in a Docker container is just as good as granting root privileges on the host system. The challenge was to break out of the container. Well, easily done when you are root:
As the root user, I listed the partition tables for the device and then mounted the host filesystem, granting me full access to the host. Now I could search for the key, which should be located in the home directory as revealed by the in-game hints:
Jolly CI/CD
While that was quick, Jolly CI/CD took me the longest of any challenge to figure out. First, we were given a Git repository to clone over HTTP:
From the URL, I could see that the name of the repository was wordpress.flag.net.internal, so I moved to the repository and found a WordPress website. I checked if the website was live:
Yup, the website was functional. I was curious if there were any leaked keys in the source code history. If yes, I should be able to push edits to the source code. So I ran git log:
From the commit messages, it looks like a commit was made after adding assets to fix a whoops. Time to check out the pre-whoops commit:
Excellent, I found a .ssh directory with keys. Let’s copy those keys over and configure an SSH agent and a Git user to see if I can impersonate the owner of those keys:
Now let’s return to the main branch and test if we can push a trivial change to the source code (using nano, I simply added a space to one of the files):
So, I achieved the first part of the challenge by impersonating one of the WordPress developers, but did the website still work after my push?
My push changed something because now the website redirected to port 8080.
Until now, I had ignored the CI/CD portion of the challenge, which should be the key to completing it. The repository contains a .gitlab-ci.yml file, which provides the configuration for a GitLab CI/CD pipeline. Every time you push to the repository, the CI/CD system kicks in, and a GitLab Runner executes the scripts in this YML file. That’s as good as achieving remote code execution on the server where GitLab Runner is installed, I thought.
Looking closer, I saw an rsync script copying all the files from the Git repository to the directory on the web server from which the website was being served. At first, I tried to use rsync to reverse the data flow by copying all the files from the web server to the Git repository, but without success.
After a lot of hypothesis testing, I eventually had my breakthrough insight: Instead of trying to “fix” the WordPress website or run malicious scripts via the build system, serve a website that leaks information from the web server. Inside index.php (located at the top level of the repository), I can comment out the code that loads the WordPress website and run PHP commands that probe the web server.
Indeed, I can even run shell commands with PHP. I found that passthru() worked easily.
In index.php, I used // to comment out two lines and I added passthru(‘ls -la /’); on the last line. This creates a website that lists all the files in the root directory of the web server:
Then I pushed this change to the Git repository and the GitLab CI/CD system took care of updating the website for me:
Ah, the Elfen Ring must be in flag.txt! I repeated the previous steps, but this time using passthru(‘cat /flag.txt’); revealing the Elfen Ring the next time I requested the website:
On to the third ring.
3. Web Ring
The most fun challenge for me was Open Boria Mine Door, although Glamtariel’s Fountain was interesting while also presenting riddles.
Open Boria Mine Door
In Open Boria Mine Door, we were presented with six pins or mini-challenges to bypass input validation or a Content Security Policy to connect the entry and exit pipes between the pins, including matching the pipe colors. For most pins, I used HTML to write a list of connecting letter ‘o’s. Here is my final solution:
Pin 1
There was no validation for Pin 1, so it was a simple matter of HTML and inline CSS:
<p style=“letter-spacing: -4px; margin: 0; padding: 0;”>oooooooooooooo</p> |
Pin 2
Pin 2 had a Content Security Policy that disallowed JavaScript but allowed inline CSS, so that was no problem for my method:
1 2 3 4 5 6 7 8 9 10 11 12 |
<ul style=“list-style: none; line-height:0.5; letter-spacing: -4px; margin: 0; padding: 0;”> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>oooooooooooooo</li> </ul> |
Pin 3
Pin 3 had a Content Security Policy that disallowed CSS but allowed inline JavaScript, so I used JavaScript to change the styles instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<script> document.write(“” + “<ul id=’o’>” + “<li>o</li>” + “<li>oooooooooooooooooooooo</li>” + “<li>o</li><li>o</li><li>o</li><li>o</li>” + “</ul>”); const o = document.getElementById(‘o’); o.style.color = “blue”; o.style.listStyle = “none”; o.style.lineHeight = “0.5”; o.style.letterSpacing = “-4px”; o.style.margin = “0”; o.style.padding = “0”; </script> |
Pin 4
Pin 4 had no Content Security Policy, but it had a sanitizeInput function on the client side that would strip double quotes, single quotes, left angle brackets, and right angle brackets. The trick here was to realize that this function wasn’t triggered by submitting the form, but by the onblur event. In other words, moving the mouse away from the input field triggered the onblur event, sanitizing any input. The solution was to submit the form by pressing the Enter key, while taking care not to move the mouse cursor outside the bounds of the input field:
1 2 3 4 5 6 7 8 9 10 11 |
<ul style=“list-style: none; line-height:0.5; letter-spacing: -4px; margin: 0; padding: 0;”> <li>o</li> <li>o</li> <li>oooooooooooooo</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li>o</li> <li style=“color: blue;”>oooooooooooooo</li> </ul> |
Pin 5
Pin 5 had the same sanitizeInput function and bypass along with a Content Security Policy forbidding inline CSS, but allowing inline JavaScript:
const o = document.getElementById(‘o’);
o.style.listStyle = “none”;
o.style.lineHeight = “0.5”;
o.style.letterSpacing = “-4px”;
o.style.margin = “0”;
o.style.padding = “0”;
o.style.fontSize = “xx-large”;
const reds = document.getElementsByClassName(“red”);
for (let red of reds) {
red.style.color = “red”;
}
const blues = document.getElementsByClassName(“blue”);
for (let blue of blues) {
blue.style.color = “blue”;
}
</script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<script> document.write(“” + “<ul id=’o’>” + “<li>o</li>” + “<li>o</li>” + “<li class=’red’>oooooooooooooo</li>” + “<li><span class=’red’>o</span></li>” + “<li><span class=’red’>o</span></li>” + “<li><span class=’red’>o</span>” + “<span class=’blue’> ooooooooooo</span></li>” + “<li><span class=’red’>o</span>” + “<span class=’blue’> oo</span></li>” + “<li><span class=’red’>o</span>” + “<span class=’blue’> oo</span></li>” + “<li><span class=’red’>o</span>” + “<span class=’blue’> oo</span></li>” + “<li>o<span class=’blue’> oo</span></li>” + “<li>o<span class=’blue’> B</span></li>” + “</ul>”); const o = document.getElementById(‘o’); o.style.listStyle = “none”; o.style.lineHeight = “0.5”; o.style.letterSpacing = “-4px”; o.style.margin = “0”; o.style.padding = “0”; o.style.fontSize = “xx-large”; const reds = document.getElementsByClassName(“red”); for (let red of reds) { red.style.color = “red”; } const blues = document.getElementsByClassName(“blue”); for (let blue of blues) { blue.style.color = “blue”; } </script> |
Pin 6
Finally, Pin 6 didn’t sanitize the input, but it used a stricter Content Security Policy forbidding both inline CSS and JavaScript. My solution was to use deprecated HTML to get the styles I needed and use a table instead of a list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<table border=“0” frame=“void” rules=“none” cellpadding=“0” cellspacing=“0” width=“100%”> <tr bgcolor=“#00FF00”> <td><font color=“#00FF00” size=“7”>o</font></td> <td><font color=“#00FF00” size=“7”>o</font></td> <td><font color=“#00FF00” size=“7”>o</font></td> <td><font color=“#00FF00” size=“7”>o</font></td> </tr> <tr bgcolor=“red”> <td><font color=“red” size=“5”>o</font></td> <td><font color=“red” size=“5”>o</font></td> <td><font color=“red” size=“5”>o</font></td> <td><font color=“red” size=“5”>o</font></td> </tr> <tr bgcolor=“blue”> <td><font color=“blue” size=“9”>o</font></td> <td><font color=“blue” size=“9”>o</font></td> <td><font color=“blue” size=“9”>o</font></td> <td bgcolor=“red”><font color=“red” size=“9”>o</font></td> </tr> <tr bgcolor=“blue”> <td><font color=“blue” size=“9”>o</font></td> <td><font color=“blue” size=“9”>o</font></td> <td><font color=“blue” size=“9”>o</font></td> <td><font color=“blue” size=“9”>o</font></td> </tr> </table> |
Glamtariel’s Fountain
Glamtariel’s Fountain was an opportunity to practice XML External Entity (XXE) attacks. Figuring out how to define a custom XML entity, defining an entity that requests a file from the server, and adding that entity as a payload to an HTTP request was not hard. The hardest part was figuring out the in-game riddles to divine the path to the files that the server would leak. Here is the breakthrough request revealing the location of the gold ring:
I would offer two lessons learned from this challenge. First, use the Content Type Converter extension in Burp to convert JSON payloads to XML. Second, try placing the XXE payload in different tags – it took me a long time to figure out that all I had to do was place the &xxe; payload in the reqType tag instead of the imgDrop tag.
On to the fourth ring.
4. Cloud Ring
Playing for the Cloud Ring was a beginner’s foray into the Amazon Web Services (AWS) Command Line Interface (CLI).
The highlight of this set of challenges was using trufflehog to find AWS credentials in a Git repository and then exploiting them to authenticate as an AWS user. An attacker that gets to this position can use aws iam commands to query the policies that apply to the user, and thus which cloud assets can be accessed and abused.
On to the fifth ring.
5. Burning Ring of Fire
The most instructive part of this set of challenges was learning about Merkle Trees to exploit a smart contract and get on the presale list for purchasing a non-fungible token (NFT). Here the challenge was to discover the proof values that, along with my wallet address and the root value of a Merkle Tree, proved my inclusion on the presale list.
After a few unsuccessful attempts to provide proof values, I realized that I would never be able to figure out the proof values for the provided root value because there was no way to know all the leaf values used to calculate it. I needed to change the root value so that I could provide a valid Merkle Tree.
Using Professor QPetabyte’s tool, I created a Merkle Tree from two leaves consisting of my wallet address and the address for the BSRS_nft smart contract, which I found using the in-game Blockchain Explorer in block two of the game’s Ethereum blockchain. The tool generated the root value of this tree and the proof value for my wallet address. Then I used Burp to intercept the request to the server and changed the default root value so that I could submit a valid Merkle Tree. Here is my NFT sporc bought at a fixed price of 100 KringleCoins:
An ugly specimen indeed.
Finale
A big thank you to the organizers of the SANS Holiday Hack Challenge for stretching my mind in new ways and helping to deepen my cybersecurity knowledge. Not only am I looking forward to next year’s challenge, but I will even be trying out the 2020 and 2021 editions of this challenge. And if you haven’t participated in this challenge before, I hope these highlights have piqued your interest.