Spammers, Golang and More

During routine monitoring of my emails, I noticed an interesting spam email posing to be the usual Microsoft O365 style phish. In this instance, the pretext was that we had an invoice and needed to sign in to view it. The more interesting part is how some of the technical side of the phish worked.

During routine monitoring of my emails, I noticed an interesting spam email posing to be the usual Microsoft O365 style phish. In this instance, the pretext was that we had an invoice and needed to sign in to view it. The more interesting part is how some of the technical side of the phish worked.

Phishing Files

Following the common trend of html smuggling, the actor sent us a blank email with an HTML attachment. The HTML attachmet takes our email address as a parameter to be rendered in the page DOM. The site hosting the phish is cp9aim3evr4lndtwakr72x[.]goldmanmeds[.]com.

L5Nll.png

As you can probably see, the phish is being hosted on a non-standard port, 2087. More on this in just a bit.

HTML Attachement Analysis

The screenshot above is ultimately what the HTML attachment loads, but let's take a step back and see what's actually going on. We are "researchers" after all. We can see that the title is "Microsoft 365 | Billing". A bit simple for me, but hey, simple works.

LT4bT.png

Scrolling down to the main part of the content, we can see that the HTML follows a very common phishing pattern where a HTTP parameter is used to pre-populate the DOM with a target's email.

LTNl9.png

Specifically this section:

LTQfy.png

Once the the DOM is populated with the document.getElementById("content").innerHTML call, we're redirected to the phish landing page.

Inspecting the Landing Page

When we loaded the landing page, we can actually see that the developers left comments in the HTML to explain how it works. Bonus points for code comments.

LTip8.png

For those that may be unable to see everything in the image, here is the JavaScript with comments:

        document.addEventListener('DOMContentLoaded', function() {
            // Get the current URL
            var url = new URL(window.location.href);
            
            // Get the 'impact' parameter from the URL
            var impactValue = url.searchParams.get('username');
            
            // Find the input field by ID
            var inputField = document.getElementById('usrn');
            
            // If the impact value is found, replace the input field's value with it
            if (impactValue) {
                inputField.value = impactValue;
            }
        });

What would a phishing page be without obfuscated JavaScript so we can't actually figure out what's going on...

LTny2.png

But fret not, they used free stuff that's easily deobfuscated.

LTsAi.png

For those that aren't super familiar with JavaScript, the code is doing the following:

  1. Preventing Default Behavior:

    • The function submitForm starts by preventing the default behavior of the form submission. This means that when the button is clicked, the form will not be submitted in the traditional way (i.e., reloading the page).
  2. Getting Form Elements:

    • The function retrieves the username and password input fields from the form.
  3. Attempt Counter:

    • The code keeps track of how many times the form submission has been attempted using a variable called tryCount.
  4. Sending Data:

    • If the number of attempts is 4 or less, the function creates a new request to send the form data to a specific URL.
    • The data is sent in a format that the server expects.
  5. Handling the Response:

    • The function waits for the server to respond.
    • If the server responds successfully (status 200), it does nothing specific in this code snippet.
    • If the server response is not successful, it also does nothing specific in this code snippet.
    • After each attempt, the tryCount is incremented.
  6. Too Many Attempts:

    • If the number of attempts exceeds 4, it alerts the user to check their internet connection and redirects them to the top of the page.
  7. Clearing the Password:

    • After attempting to send the data, the password field is cleared.
  8. Event Listener:

    • The function is linked to the "Continue" button so that it runs when the button is clicked.

In this case, the data is being sent within the usrn and psrd parameters to hxxps://grandcars[.]net/app/exl0.php. Or, as a full usrl: hxxps://grandcars[.]net/app/exl0.php?usrn=victim%40email.com&psrd=password.

Sadly, we don't get a glimpse into what server does once it receives a username and password. One can assume that the credentials are likely then sent over something like Telegram, email, or other such communication medium.

Deployable Landing Pages

When checking out the landing page, I found that directory indexing was left enabled. After all, configuring web servers is hard.

LTKNS.png

Sadly, we were unable to download some of the "fun" files, like .bash_history. We see what is likely the private key and public key for the SSL certificate, however, we see main.go on the server. The inclusion of the go linux tar tells us that main.go is likely running. But, what's actually in it?

LTPGC.png

Interstingly enough, they are using go to run a portible web server to serve content rather than spining up an Apache or NGINX server. Nor did they leverage just uploading content to an already-compromised-site directory.

There are also several user agents that are being monitored for in order to restrict crawling:

LTTbe.png

Again, outstanding use of code comments. Consistent with many other phish kits, this also blocks a handful of IP ranges:

LTlM0.png

We also see the section where the HTTPS server is configured with the non-standard port I mentioned in the beginning of this post:

fmt.Println("Server started at https://localhost:2087")
    if err := http.ListenAndServeTLS(":2087", "domain.pem", "domain.key", nil); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }

It's interesting seeing the use go for deploying a web server, but I guess it's a smaller footprint and significantly more portable. I'll add the full code to the bottom of this post :)

While this phish isn't anything particularly advanced, it's fascinating to see the use of go for serving resources rather than a traditional http server. I'm curious if anyone else has come across such usage.

Full Go Code

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "strings"
    "time"
)

func main() {
    // Serve files from the /root/aflaton directory
    fs := http.FileServer(http.Dir("/root/aflaton"))

    // IP blocking middleware
    blockIPs := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            clientIP := getClientIP(r)
            if isBlockedIP(clientIP) {
                http.Error(w, "Access denied", http.StatusForbidden)
                return
            }
            next.ServeHTTP(w, r)
        })
    }

    // Bot blocking middleware
    blockBots := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userAgent := r.Header.Get("User-Agent")
            if isBot(userAgent) {
                http.Error(w, "Bots are not allowed", http.StatusForbidden)
                return
            }
            next.ServeHTTP(w, r)
        })
    }

    // Custom logging middleware to log request details
    logRequest := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            clientIP := getClientIP(r)
            next.ServeHTTP(w, r)
            log.Printf(
                "%s %s %s %s %s",
                clientIP,
                r.Method,
                r.RequestURI,
                r.UserAgent(),
                time.Since(start),
            )
        })
    }

    // Combine middlewares and file server handler
    handler := blockIPs(blockBots(logRequest(fs)))

    // Serve index.html for the root URL
    http.Handle("/", handler)
    http.HandleFunc("/impact", serveImpact)

    // Configure the HTTPS server
    fmt.Println("Server started at https://localhost:2087")
    if err := http.ListenAndServeTLS(":2087", "domain.pem", "domain.key", nil); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}

// serveImpact sets a cookie with the email ID and redirects to the root URL
func serveImpact(w http.ResponseWriter, r *http.Request) {
    emailID := r.URL.Query().Get("impact")
    if emailID == "" {
        http.Error(w, "Email ID not provided", http.StatusBadRequest)
        return
    }

    // Read the content from codehtml.txt
    content, err := os.ReadFile("/root/aflaton/codehtml.txt")
    if err != nil {
        http.Error(w, "Failed to read file", http.StatusInternalServerError)
        log.Fatalf("Failed to read file: %v", err)
        return
    }

    // Replace the placeholder with the email address
    replacedContent := strings.ReplaceAll(string(content), "EMailID", emailID)

    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, replacedContent)
}

// isBot checks if the User-Agent belongs to a bot
func isBot(userAgent string) bool {
    botList := []string{
        "bot", "crawler", "spider", "curl", "wget", "scrapy",
        "facebookexternalhit", "bingbot", "Googlebot", "Yahoo! Slurp",
        "DuckDuckBot", "Baiduspider", "YandexBot", "Sogou", "msnbot",
        "Applebot", "LinkedInBot", "PetalBot", "SemrushBot", "AhrefsBot",
        "MJ12bot", "rogerbot", "dotbot", "BLEXBot",
    }
    userAgent = strings.ToLower(userAgent)
    for _, bot := range botList {
        if strings.Contains(userAgent, bot) {
            return true
        }
    }
    return false
}

// getClientIP extracts the client IP address from the request headers
func getClientIP(r *http.Request) string {
    // Check Cloudflare headers first
    if ip := r.Header.Get("CF-Connecting-IP"); ip != "" {
        return ip
    }
    // Check X-Forwarded-For header
    if ips := r.Header.Get("X-Forwarded-For"); ips != "" {
        ipList := strings.Split(ips, ",")
        return strings.TrimSpace(ipList[0])
    }
    // Fallback to remote address
    ip := r.RemoteAddr
    if idx := strings.LastIndex(ip, ":"); idx != -1 {
        ip = ip[:idx]
    }
    return ip
}

// isBlockedIP checks if the client IP is within the blocked ranges
func isBlockedIP(ipStr string) bool {
    blockedRanges := []string{
        "20.0.0.0/8",
        "40.0.0.0/8",
    }

    ip := net.ParseIP(ipStr)
    if ip == nil {
        return false
    }

    for _, cidr := range blockedRanges {
        _, network, err := net.ParseCIDR(cidr)
        if err != nil {
            log.Printf("Error parsing CIDR %s: %v", cidr, err)
            continue
        }
        if network.Contains(ip) {
            return true
        }
    }
    return false
}